Most PHP performance problems are not in PHP. They are in unoptimized database queries, missing indexes, N+1 patterns, and unnecessary network calls. But once you have fixed those, PHP itself has significant tuning headroom through OPcache configuration, JIT compilation, and code-level improvements.
The cardinal rule: profile first, optimize second. Never guess where the bottleneck is.
Profiling: finding the actual bottleneck
Xdebug profiler
Xdebug is the standard PHP debugger. Its profiling mode generates cachegrind files that show function-level timing.
1 | ; php.ini |
Trigger profiling by adding XDEBUG_PROFILE=1 to the request:
1 | curl -b "XDEBUG_PROFILE=1" http://localhost/api/users |
Open the resulting cachegrind file in KCachegrind (Linux), QCachegrind (macOS), or webgrind (browser-based).
Blackfire
Blackfire provides production-safe profiling with a clean web UI:
1 | blackfire curl http://localhost/api/users |
Blackfire shows call graphs, time percentages, and memory allocation per function. It is the most practical tool for identifying optimization targets because it works in production without significant overhead.
SPX (Simple Profiling Extension)
SPX is lightweight and built specifically for quick profiling:
1 | ; php.ini |
Access the profiling UI at http://localhost/?SPX_KEY=dev&SPX_UI_URI=/. SPX shows real-time flame graphs directly in the browser.
What to look for
- Functions that consume >10% of total time: These are your optimization targets
- Functions called thousands of times: Even if each call is fast, the aggregate cost matters
- Database queries: If 80% of time is in PDO/MySQLi, optimize queries, not PHP code
- Autoloader hits: If the autoloader shows up prominently, OPcache preloading helps
OPcache: the biggest free performance gain
OPcache caches compiled PHP bytecode in shared memory. Without it, PHP recompiles every file on every request. With it, the compilation step is skipped entirely.
Verifying OPcache is active
1 | var_dump(opcache_get_status()); |
Key values to check:
opcache_enabled: must betruecache_full: must befalse(increase memory if true)num_cached_scripts: should be close to your total PHP file count
Production configuration
1 | ; Maximum memory for bytecode cache (MB) |
OPcache preloading
PHP 7.4+ supports preloading: load commonly used files into OPcache at server start, avoiding autoloader overhead entirely.
1 | // preload.php |
1 | ; php.ini |
Laravel provides php artisan optimize which generates a preload file automatically.
JIT compilation
PHP 8.0+ includes a JIT (Just-In-Time) compiler that converts PHP bytecode to native machine code at runtime. This eliminates the bytecode interpretation step for hot code paths.
When JIT helps
JIT accelerates CPU-bound operations:
1 | // JIT helps here: pure computation |
JIT configuration
1 | ; Enable JIT with 128MB buffer |
JIT modes explained
The opcache.jit value is a 4-digit number CRTO:
- C (CPU optimization):
1= enable - R (register allocation):
2= use registers - T (trigger):
5= tracing (profile and compile hot paths) - O (optimization level):
5= maximum
1255 (tracing JIT with full optimization) is the recommended production setting.
Practical JIT impact
| Workload | Without JIT | With JIT | Improvement |
|---|---|---|---|
| Mathematical computation | 100ms | 35ms | 2.8x |
| String processing loop | 80ms | 45ms | 1.8x |
| JSON encoding/decoding | 50ms | 40ms | 1.25x |
| Laravel HTTP request | 12ms | 11ms | 1.1x |
| Database-heavy page | 45ms | 44ms | 1.02x |
For typical web applications, JIT provides 5-15% improvement. For compute-heavy scripts, it can be 2-3x.
Code-level optimizations
Avoid unnecessary object creation
1 | // Slow: creates a new Carbon instance on every iteration |
Use generators for large datasets
1 | // Slow: loads all 100K rows into memory |
Optimize string concatenation
1 | // Slow for large strings (creates intermediate copies) |
Cache expensive computations
1 | // Without caching: hits database every request |
Common mistakes
Optimizing without profiling: Developers often optimize code that is not the bottleneck. A 50% improvement to a function that takes 1ms is worthless when the database query above it takes 200ms.
Setting OPcache memory too low: If OPcache runs out of memory, it evicts cached scripts and recompiles them. Check opcache_get_status()['memory_usage'] to size it properly.
Enabling JIT in development: JIT makes debugging harder because stack traces may not match source lines. Use JIT in production only.
Ignoring query optimization: The biggest PHP performance gain is usually adding a database index, not tuning PHP. Run EXPLAIN on slow queries first.
FAQ
Does OPcache work with Docker?
Yes, but configure opcache.validate_timestamps=0 since files do not change in a container after build. Also consider opcache.file_cache for faster container restarts.
Should I use preloading with Laravel?
Yes, if you are on PHP 7.4+. Run php artisan optimize and configure preloading in php.ini. It reduces autoloader overhead by 10-20%.
How do I benchmark PHP performance?
Use ab (Apache Bench), wrk, or k6 for HTTP-level benchmarks. Use Xdebug or Blackfire for function-level profiling. Never benchmark with a single request—run at least 1000 requests to get stable numbers.
Next steps
Start with opcache_get_status() to verify OPcache is running and properly sized. Then profile a slow endpoint with Xdebug or Blackfire to find the actual bottleneck. Fix the biggest bottleneck first—it is almost always a database query.
For Docker-specific PHP tuning, the Dockerizing PHP apps guide covers container-level optimizations. The FrankenPHP guide covers an alternative runtime that eliminates per-request bootstrap overhead. The existing page speed guide covers frontend performance that complements server-side PHP optimization.