Why the PHP Runtime Layer Still Matters
Most Laravel performance conversations stop at query optimization or Redis caching. The runtime layer — how PHP itself boots, compiles, and executes your code — is often left at defaults. FrankenPHP, OPcache JIT, and preloading each attack a different slice of that cost.
OPcache and JIT: What They Actually Do
OPcache caches the compiled opcode of every PHP file so subsequent requests skip parsing and compilation. JIT (Just-In-Time compilation) goes one step further: it compiles hot opcode paths to native machine code at runtime.
For Laravel, the practical gain from JIT is modest on I/O-bound routes (most web requests) but measurable on CPU-bound work — think report generation, data transformation pipelines, or heavy collection operations.
; php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; production only
; JIT — tracing mode is the most effective for web workloads
opcache.jit=tracing
opcache.jit_buffer_size=128M
opcache.validate_timestamps=0 is critical in production. With it enabled, OPcache stat-checks every file on every request, which defeats much of the benefit.
Preloading: Boot Laravel Once, Serve Forever
Preloading (PHP 7.4+) lets you load and compile classes into shared memory before any request is served. Every worker process inherits that memory map, skipping file I/O and compilation for preloaded classes entirely.
Laravel ships with a preload script you can publish:
php artisan vendor:publish --tag=laravel-preload
This generates storage/framework/preload.php. Point OPcache at it:
opcache.preload=/var/www/html/storage/framework/preload.php
opcache.preload_user=www-data
The generated script uses Illuminate\Foundation\Application::preload() to walk the framework's class map. You can extend it with your own hot paths:
// storage/framework/preload.php
require __DIR__ . '/../../../vendor/autoload.php';
$app = require __DIR__ . '/../../../bootstrap/app.php';
// Preload your most-hit domain classes
require_once __DIR__ . '/../../../app/Domain/Pricing/PriceCalculator.php';
require_once __DIR__ . '/../../../app/Http/Resources/ProductResource.php';
Keep the preload list focused. Preloading everything inflates shared memory and can actually hurt cold-start time on low-traffic servers.
FrankenPHP: Eliminating the FPM Boundary
FrankenPHP is a PHP app server written in Go that embeds the PHP interpreter directly. It replaces Nginx + PHP-FPM with a single binary. The key architectural difference: no Unix socket or TCP hop between the web server and PHP.
Worker Mode — The Real Win
FrankenPHP's worker mode is analogous to Laravel Octane's approach: PHP boots once, then handles requests in a loop without re-bootstrapping the framework.
# Dockerfile (production)
FROM dunglas/frankenphp:latest-php8.3
COPY . /app
WORKDIR /app
RUN install-php-extensions pdo_pgsql redis intl
CMD ["frankenphp", "run", "--config", "/app/Caddyfile"]
# Caddyfile
{
frankenphp
order php_server before file_server
}
localhost {
root * /app/public
php_server {
worker /app/public/index.php
num 4
}
}
The worker directive tells FrankenPHP to keep index.php alive. Laravel's bootstrap runs once per worker, not once per request.
State Leakage in Worker Mode
The same caution that applies to Octane applies here. Singletons bound in the container survive across requests. Reset them explicitly:
// public/index.php (worker entry point)
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
while ($request = \FrankenPHP\handle_request()) {
$response = $kernel->handle(
$illuminateRequest = Illuminate\Http\Request::createFromGlobals()
);
$response->send();
$kernel->terminate($illuminateRequest, $response);
}
If you use Laravel Octane's RequestHandled flush hooks, those translate cleanly here.
Combining All Three
The layers are additive:
- OPcache — eliminates parse/compile overhead on every request.
- Preloading — moves framework class loading to server boot, not request time.
- JIT — compiles hot opcode to native code for CPU-bound paths.
- FrankenPHP worker mode — eliminates framework bootstrap cost per request entirely.
Key Takeaways
- Set
opcache.validate_timestamps=0in production; use deployment hooks to clear the cache on deploy. - JIT's biggest gains are on CPU-bound code; don't expect dramatic wins on typical CRUD routes.
- Keep your preload list to genuinely hot classes — framework core plus your own domain hotspots.
- FrankenPHP worker mode and Octane share the same state-leakage risks; audit your singletons before enabling either.
- All three optimizations are orthogonal and compose cleanly — enable them incrementally and profile between each change.