The closure optimizations RFC for PHP 8.6 was approved with two changes that are mostly invisible to application code. The engine can infer when a closure does not use $this and treat it as static. Static closures that capture no variables can then be cached instead of recreated repeatedly.
Closures appear in callbacks, middleware, Laravel collections, and event listeners, so a small engine improvement can touch a broad portion of a modern application. In the RFC author's Symfony Demo test, 68 of 87 closures were converted automatically, about 78 percent. Fewer closure objects mean fewer allocations and less per-request overhead.
The attractive part is that teams do not need to rewrite those functions. Developers who already mark closures static stop carrying a manual micro-optimization everywhere, while teams that never considered it inherit the compiler improvement during the runtime upgrade.
8.6 ships caching; inference stayed behind
The vote covered both optimizations. After the poll closed, the RFC text itself gained an errata. Some internal paths still fire an instance call from a nominal callable: array_map with 'Foo::instanceCall', array callables, and other internal functions that reuse the previous frame. The behavior is esoteric, and until it is deprecated and removed, assuming that a closure "does not use $this" remains unsafe.
That is why only the cache for stateless closures landed. Closures that are already static, capture no variables, and declare no inner static variables can be reused. Automatic static inference did not ship with them.
This changes the upgrade arithmetic. The 78 percent figure from Symfony Demo measured inference, with explicit static markers removed for the test. That number is not a production target on 8.6. The runtime gain now depends on closures the code already declares static and capture-free.
Teams that write static function () {} on callbacks collect the cache. Teams that expected the compiler to guess keep allocating on the old path. The "no application rewrite" claim still holds: nobody has to touch business logic. It stops holding if the internal plan was "upgrade to 8.6 and closures become static on their own".
Where caching shows up in a request
Laravel, Symfony, and WordPress create closures during boot, routing, middleware, listeners, and collections. The RFC microbenchmark, ten million instantiations of the same stateless closure, improved by about 80 percent on the author's machine. On the Laravel template, both optimizations together would avoid 2384 of 3637 instantiations, about 3 percent. With caching only, the ceiling is lower and depends on how many hot-path closures are already static.
Three percent on a template is an order of magnitude. Your application's SLA still depends on the hot path you measure, with the opcache you run.
The test side effect that matters is identity. Two stateless closures created at the same lexical site become the same object. test() === test() can become true. Suites that use identity as proof of "a new instance on every call" will fail. Compare behavior, not the Closure object handle.
The RFC also listed destructors firing earlier because cycles with $this would disappear. That point belonged to inference. Because inference did not land, the risk drops. Caching still changes when the Closure object is created, which can affect tests that count allocations.
Closure::bind() and bindTo() had a planned exception for closures only inferred as static, so code that suddenly lost $this would not break. Without inference, that exception matters less. Explicitly static closures still refuse an object bind as they did before.
Benchmark the minor, not the framework
Upgrading to 8.6 mixes parser, stdlib, JIT, extensions, and this RFC. Blaming any p95 drop on closures is a guess. A useful trial isolates the runtime:
- the same application and the same opcache, PHP 8.5 versus 8.6;
- a slice of routes and jobs that actually build closures;
- memory per request and time, not only synthetic throughput;
- PHPUnit and integration tests that create closures in a loop or compare identity.
If the gain does not show up, the minor can still be worth it for fixes and other features. It should not be sold internally as "automatic closure performance". Without a 8.5 time series on the same hardware, there is no baseline. Without a route slice, a php -r benchmark does not speak for the application.
I would also read allocation reports only after warming opcache. Compare cold with cold, warm with warm. Mixing the two hides the language cache behind the opcode cache.
Marking static remains a review job
The RFC already said that marking static stays preferable. After the errata, that stopped being a nitpick. The cache only hits the static, capture-free case. Arrow functions and closures that close over $this or scope variables stay on the old path.
I would ask for static in review on collection callbacks, routes, and listeners that do not use the instance. It is one word. It avoids the $this cycle even without inference. It also leaves the code ready if inference returns in a later version, after the deprecation the errata asks for.
Do not mark static on a closure that actually uses $this. The engine rejects it. The useful habit is the obvious case, the same one the author measured on the Demo when the modifiers were stripped for the experiment.
A second cheap habit: skip unnecessary use. Each captured variable takes the closure out of the cache. Capturing $logger "just in case" in a callback that never uses it costs an allocation on every request. The review that already asks for static can ask for an empty capture in the same pass.
Upgrading to 8.6 remains, for most teams, the cheapest way to pick up engine improvements. The honest slice of this RFC is narrower than the vote suggested: cache for whoever already writes a stateless closure, identity regressions for whoever asserts on the object, and no miracle for the callback that closes over half a controller.
If your company is weighing something similar, reach me on LinkedIn or WhatsApp.