La RFC de optimizaciones de closures para PHP 8.6 fue aprobada con dos cambios casi invisibles para el código de aplicación. El motor puede inferir cuándo una closure no usa $this y tratarla como static. Las closures estáticas sin variables capturadas pueden almacenarse en cache en vez de recrearse.
Las closures aparecen en callbacks, middleware, collections de Laravel y event listeners. Una mejora pequeña del motor toca una parte amplia de una aplicación moderna. En la prueba del autor con Symfony Demo, 68 de 87 closures se convirtieron automáticamente, cerca del 78 por ciento. Menos objetos significan menos asignaciones y menor overhead por request.
El atractivo es que el equipo no reescribe esas funciones. Quien ya marca closures como static deja de cargar una microoptimización manual; quien nunca pensó en ella hereda la mejora del compilador al actualizar el runtime.
8.6 entrega cache; la inferencia se quedó atrás
La votación cubrió las dos optimizaciones. Después de cerrar el voto, el propio texto de la RFC ganó un errata. Algunos caminos internos todavía disparan una llamada de instancia desde un callable nominal: array_map con 'Foo::instanceCall', callables en array y otras funciones internas que reutilizan el frame anterior. El comportamiento es esotérico y, mientras no se deprecie y elimine, asumir que la closure "no usa $this" sigue siendo inseguro.
Por eso solo entró el cache de closures sin estado. Las closures que ya son static, no capturan variables y no declaran static interno pueden reutilizarse. La inferencia automática de static no llegó con ellas.
Eso cambia la cuenta de la actualización. El 78 por ciento de Symfony Demo medía la inferencia, con los static explícitos quitados para la prueba. Ese número no es una meta de producción en 8.6. La ganancia del runtime ahora depende de closures que el código ya declara estáticas y sin captura.
Los equipos que escriben static function () {} en los callbacks recogen el cache. Los que esperaban que el compilador adivinara siguen asignando en el camino viejo. La idea de "cero reescritura de aplicación" sigue valiendo: nadie tiene que tocar la lógica de negocio. Deja de valer si el plan interno era "actualizamos a 8.6 y las closures se vuelven static solas".
Dónde aparece el cache en el request
Laravel, Symfony y WordPress crean closures en el boot, rutas, middlewares, listeners y collections. El microbenchmark de la RFC, diez millones de instanciaciones de la misma closure sin estado, mejoró cerca del 80 por ciento en la máquina del autor. En el template de Laravel, las dos optimizaciones juntas evitarían 2384 de 3637 instanciaciones, cerca del 3 por ciento. Con solo el cache, el techo es menor y depende de cuántas closures del hot path ya son estáticas.
Tres por ciento en un template describe un orden de magnitud. El SLA de la aplicación sigue dependiendo del hot path que ustedes midan, con el opcache que usan.
El efecto colateral que importa para los tests es la identidad. Dos closures sin estado creadas en el mismo punto léxico pasan a ser el mismo objeto. test() === test() puede volverse verdadero. Las suites que usan la identidad como prueba de "instancia nueva en cada llamada" se rompen. Comparen comportamiento, no el handle del objeto Closure.
La RFC también listaba destructores disparándose antes porque los ciclos con $this dejarían de existir. Ese punto pesaba en la inferencia. Como la inferencia no entró, el riesgo baja. El cache todavía cambia cuándo se crea el objeto Closure, y eso puede afectar tests que cuentan asignaciones.
Closure::bind() y bindTo() tenían una excepción planeada para closures solo inferidas como static, para no romper código que de pronto perdiera $this. Sin la inferencia, esa excepción pesa menos. Las closures explícitamente static siguen rechazando bind de objeto como antes.
Benchmark de la minor, no del framework
Actualizar a 8.6 mezcla parser, stdlib, JIT, extensiones y esta RFC. Atribuir cualquier baja de p95 a las closures es un tiro al aire. Un ensayo útil aísla el runtime:
- la misma aplicación y el mismo opcache, PHP 8.5 versus 8.6;
- un recorte de rutas y jobs que de verdad construyen closures;
- memoria por request y tiempo, no solo throughput sintético;
- PHPUnit y tests de integración que crean closures en un loop o comparan identidad.
Si la ganancia no aparece, la minor todavía puede valer por correcciones y otras features. No debería venderse internamente como "performance automática de closures". Sin una serie temporal de 8.5 en el mismo hardware, no hay línea de base. Sin el recorte de rutas, un benchmark de php -r no habla de la aplicación.
Yo también miraría el reporte de asignaciones solo después de calentar opcache. Comparar request fría con fría, caliente con caliente. Mezclar las dos esconde el cache del lenguaje detrás del cache de opcode.
Marcar static sigue siendo trabajo de review
La RFC ya decía que marcar static sigue siendo preferible. Después del errata, eso dejó de ser un nitpick. El cache solo toma el caso estático sin captura. Las arrow functions y las closures que cierran $this o variables del alcance siguen en el camino viejo.
Yo pediría en el review el static en callbacks de collection, rutas y listeners que no usan la instancia. Es una palabra. Evita el ciclo con $this incluso sin la inferencia. También deja el código listo si la inferencia vuelve en una versión futura, después de la deprecación que pide el errata.
No marquen static en una closure que usa $this de verdad. El motor lo rechaza. El hábito útil es el caso obvio, el mismo que el autor midió en el Demo cuando quitó los modificadores para el experimento.
Un segundo hábito barato: evitar use innecesario. Cada variable capturada saca a la closure del cache. Capturar $logger "por las dudas" en un callback que no lo usa cuesta una asignación en cada request. El review que ya pide static puede pedir captura vacía en la misma pasada.
La actualización a 8.6 sigue siendo, para la mayoría de los equipos, la forma más barata de recoger mejoras del motor. El recorte honesto de esta RFC es más estrecho de lo que sugería el voto: cache para quien ya escribe una closure sin estado, regresión de identidad para quien afirma sobre el objeto, y ningún milagro para el callback que cierra media docena de variables del controller.
Si tu empresa está evaluando algo parecido, escríbeme por LinkedIn o por WhatsApp.