Journal
PHP Performance: From OPcache to JIT Compilation
PHP’s reputation for being slow hasn’t been accurate for years. With proper OPcache configuration and the JIT compiler, PHP handles workloads that would have required a different language a decade ago. Here’s how to tune it.
OPcache: The Biggest Win
Every PHP request parses source files into an AST, compiles them to opcodes, then executes them. OPcache eliminates the first two steps by caching compiled opcodes in shared memory.
Enabling and Configuring
OPcache ships with PHP but may need enabling. In your php.ini:
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.revalidate_freq=0
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=1
The critical settings:
- memory_consumption — how much shared memory for cached scripts (256MB handles large apps)
- max_accelerated_files — must exceed your total PHP file count; round up to the next prime number
- validate_timestamps=0 — don’t check if files changed on disk. Essential for production. Restart PHP-FPM after deployments instead
- revalidate_freq — irrelevant when validate_timestamps is off, but set to 0 for dev environments where you want instant refreshes
Checking OPcache Status
$status = opcache_get_status();
$config = opcache_get_configuration();
echo "Used memory: " . $status['memory_usage']['used_memory'];
echo "Cached scripts: " . $status['opcache_statistics']['num_cached_scripts'];
echo "Hit rate: " . $status['opcache_statistics']['opcache_hit_rate'] . "%";
If your hit rate is below 99% in production, you either have too many files (raise max_accelerated_files) or too little memory (raise memory_consumption).
Preloading (PHP 7.4+)
Preloading loads specific files into OPcache at server start, before any request. They stay in memory permanently — no per-request stat calls, no cache misses.
opcache.preload=/var/www/app/preload.php
opcache.preload_user=www-data
A typical preload script for Laravel:
// preload.php
require __DIR__ . '/vendor/autoload.php';
$files = require __DIR__ . '/vendor/composer/autoload_classmap.php';
foreach ($files as $file) {
try {
opcache_compile_file($file);
} catch (Throwable) {
// Some files can't be preloaded (interfaces extending missing classes, etc.)
}
}
In benchmarks, preloading can give a 5-15% improvement on framework-heavy applications.
JIT Compilation (PHP 8.0+)
The JIT compiler takes things further by compiling hot opcodes into machine code. It sits on top of OPcache.
Configuration
opcache.jit=1255
opcache.jit_buffer_size=128M
The jit value 1255 means:
- 1 — use CPU-specific optimizations
- 2 — use tracing JIT (recommended over function JIT)
- 5 — JIT trigger based on relative usage
- 5 — optimization level (highest)
For simpler configuration, PHP 8+ also accepts:
opcache.jit=tracing
When JIT Helps
JIT shines on CPU-bound workloads:
- Image processing
- Mathematical computations
- Data transformation pipelines
- Template rendering engines
For typical I/O-bound web apps (database queries, API calls, file reads), JIT adds minimal improvement because the bottleneck is waiting on external systems, not executing PHP.
Measuring the Impact
Benchmark before and after. A simple approach:
$start = hrtime(true);
// Your workload here
for ($i = 0; $i < 1000000; $i++) {
$result = array_map(fn($x) => $x * 2, range(1, 100));
}
$elapsed = (hrtime(true) - $start) / 1e6;
echo "Elapsed: {$elapsed}ms\n";
Profiling with Xdebug
Before optimizing, measure. Xdebug’s profiler generates cachegrind files you can analyze:
xdebug.mode=profile
xdebug.output_dir=/tmp/xdebug
xdebug.profiler_output_name=callgrind.%R.%t
Trigger profiling per-request with a cookie or query parameter:
xdebug.start_with_request=trigger
Then pass XDEBUG_TRIGGER=1 as a GET parameter or cookie. Analyze the output with KCachegrind, QCachegrind, or Webgrind.
Important: never run Xdebug in production. The profiler adds significant overhead. Use it in staging or local environments only.
The Performance Stack
For maximum PHP performance in production:
- OPcache with
validate_timestamps=0— non-negotiable - Preloading for framework classes — measurable improvement
- JIT in tracing mode — free performance for CPU-bound paths
- PHP-FPM tuning — proper
pm.max_childrenbased on available memory - Restart PHP-FPM on deploy —
sudo systemctl reload php8.3-fpm
Most PHP apps become fast enough after step 1. The rest is optimization for specific bottlenecks you’ve measured and identified.