The goal is to move from “it feels slow” to a reproducible process: baseline → profile → targeted fixes → re-measure. We’ll lean on PHP 8.4 features, OPcache/JIT, Composer autoloading, APCu, DB discipline, and async I/O where it matters.
TL;DR
- Baseline first (micro + end-to-end).
- Profile in realistic conditions (staging/prod if you can).
- Fix the top 1–3 hotspots (algorithm/DB/I/O/cache).
- Harden runtime (OPcache/JIT, Composer authoritative, realpath cache, FPM).
- Re-measure and enforce a perf budget in CI.
1) What “performance” actually means
- Latency: p50/p95/p99 per route/command.
- Throughput: requests/sec under load.
- Memory: peak usage and allocation churn.
- CPU cost: CPU-seconds per request/job.
Set a clear SLO (e.g., /search p95 < 150 ms).
2) Establish an honest baseline (before “optimizing”)
2.1 Micro-benchmarks (pure PHP)
Use a benchmark harness (or a tool like PHPBench) and version the results.
<?php
require __DIR__.'/vendor/autoload.php';
function work(): void {
// ... code to measure ...
}
function bench(int $n = 30): void {
$runs = [];
for ($i = 0; $i < $n; $i++) {
$t0 = hrtime(true);
work();
$runs[] = (hrtime(true) - $t0) / 1e6; // ms
}
sort($runs);
$p95 = $runs[(int) floor(0.95 * count($runs))] ?? end($runs);
printf("min=%.2fms median=%.2fms p95=%.2fms max=%.2fms
",
min($runs), $runs[(int) floor(count($runs)/2)], $p95, max($runs));
}
bench();
2.2 End-to-end HTTP benchmarks
Use a load tool (e.g., wrk, k6) to hit real endpoints with realistic payloads and concurrency.
wrk -t12 -c400 -d30s https://your-app.test
Tip: Commit raw numbers (CSV/JSON) to the repo. If it’s not versioned, it didn’t happen.
3) Profiling: find the real hotspots
- Blackfire: low overhead, readable flamegraphs, compare before/after, captures CPU and memory, good for staging/prod.
- XHProf/Tideways: modernized XHProf-style profiling, great in dev or staging.
Method: profile a representative request, sort by cumulative time, fix the top 1–3, re-measure. Repeat.
4) PHP 8.4 features worth knowing (measure their impact)
- Property Hooks: add logic on property get/set (validation, lazy compute). Great for clarity, but avoid in hot loops if profiling shows overhead.
-
New
array_*helpers:array_find,array_find_key,array_any,array_all. They reduce custom code; still measure if they sit on a hot path.
5) Algorithms & data structures
- Replace O(n²) scans (double loops) with maps/sets or a DB index.
- For very large indexed arrays, consider
SplFixedArrayor the ext-ds structures (Ds\Vector,Ds\Map) if available—only when benchmarks prove a win. - Prefer generators for streaming large volumes.
Mantra: fewer allocations, fewer copies, fewer hash lookups in hot loops.
6) Databases (where 80% of issues hide)
- Use EXPLAIN and add targeted indexes.
- Kill N+1 (eager load in your ORM).
- Batch inserts/updates, paginate realistically, use query timeouts.
- Cache hot query results: APCu (local process) or Redis (shared).
Simple APCu memoization helper:
function cached(callable $fn, string $key, int $ttl = 60) {
$val = apcu_fetch($key, $hit);
if ($hit) return $val;
$val = $fn();
apcu_store($key, $val, $ttl);
return $val;
}
7) OPcache & JIT (PHP 8.4)
- OPcache must be on in production. Size memory and accelerated files realistically.
- JIT can help CPU-bound workloads. It’s not a silver bullet for I/O-bound apps—measure.
Starter php.ini:
; OPcache
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.max_accelerated_files=60000
opcache.validate_timestamps=0
; JIT (evaluate impact!)
opcache.jit=tracing
opcache.jit_buffer_size=64M
8) Startup & autoload
- Composer authoritative autoloader cuts dynamic lookups and file I/O:
composer install --no-dev --classmap-authoritative --apcu-autoloader
or
composer dump-autoload -a --apcu
- realpath cache reduces repeated path resolutions; tune if your app opens lots of files:
realpath_cache_size=4096K
realpath_cache_ttl=300
9) I/O & networking: when to go async
- ReactPHP: event loop + HTTP/DNS/streams in pure PHP (no extension). Great for multiplexing outbound requests and I/O-heavy tasks.
- Open Swoole/Swoole: high-performance server with async I/O, coroutines/fibers, HTTP/WebSocket/gRPC. Requires an extension; excellent for long-lived connections and real-time workloads.
Rule of thumb: I/O-bound → async can unlock ×2…×10. CPU-bound → consider C/FFI or background workers/queues.
10) PHP-FPM & infrastructure basics
- Tune
pm(dynamic|ondemand|static) and especiallypm.max_childrenaccording to RAM and traffic. -
pm.max_requestshelps recycle processes and contain leaks. - Enable slowlog and watch p95/p99 regularly.
11) The reusable workflow (checklist)
-
Target & SLO (e.g.,
/api/feedp95 < 150 ms). -
Baseline:
- Micro: harness or PHPBench (version results).
- HTTP:
wrk/k6against real endpoints and payloads.
- Profile: Blackfire (staging/prod) or XHProf/Tideways (dev/stage).
- Fix top N: algorithm → DB → I/O → cache → autoload.
- Harden runtime: OPcache/JIT, Composer authoritative, realpath cache, FPM.
- Re-measure & CI perf budget: fail PRs on regressions.
- Document: keep flamegraphs and benchmark artifacts in the repo.
12) Copy-paste snippets
12.1 Composer (production)
composer install --no-dev --classmap-authoritative --apcu-autoloader
(or composer dump-autoload -a --apcu)
12.2 php.ini (starter)
; -------- OPcache --------
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.max_accelerated_files=60000
opcache.validate_timestamps=0
; -------- JIT (evaluate) --------
opcache.jit=tracing
opcache.jit_buffer_size=64M
; -------- realpath cache --------
realpath_cache_size=4096K
realpath_cache_ttl=300
12.3 APCu helper
function cached(callable $fn, string $key, int $ttl = 60) {
$val = apcu_fetch($key, $hit);
if ($hit) return $val;
$val = $fn();
apcu_store($key, $val, $ttl);
return $val;
}
12.4 ReactPHP mini HTTP server (POC)
<?php
require 'vendor/autoload.php';
$loop = React\EventLoop\Loop::get();
$server = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
return React\Http\Message\Response::plaintext("OK\n");
});
$socket = new React\Socket\SocketServer('0.0.0.0:8080');
$server->listen($socket);
echo "Listening on http://127.0.0.1:8080\n";
$loop->run();
13) Key takeaways
- Measure first (realistic baseline), profile second (hit the top 1–3).
-
PHP 8.4 adds convenience (
array_find*, Property Hooks), but assume nothing—measure hot paths. - Big levers stay classic: DB discipline, I/O, caching, optimized autoload, OPcache/JIT tuned for your workload.
Conclusion
Performance work in PHP 8.4 is a process, not a grab‑bag of tips. If you consistently
1) capture a baseline, 2) profile under realistic conditions, 3) fix the top hotspots, and
4) re‑measure with guardrails in CI, your app will trend toward fast‑by‑default without guesswork.
Start small: pick one high‑value endpoint, record its current p95, profile it (Blackfire or XHProf/Tideways),
apply a single targeted fix, then re‑run your micro and end‑to‑end benchmarks. Commit the numbers. Repeat.
As this becomes routine, tune OPcache/JIT, harden Composer autoloading, and use APCu/Redis where it counts.
Treat newer PHP 8.4 niceties (Property Hooks, array_find*, etc.) as situational—always validate with measurements.
Top comments (2)
the database really is where the vast majority of improvements usually can be made. when i work on a rescue project that has poor performance the first two things i look for are queries in loops and lack of indexes. fixing those is almost always the biggest improvement for the least amount of effort.
I've read tons of articles about speeding up PHP, but this one is different. Most guides just throw random tips at you like "enable OPcache!" or "use Redis!" without explaining when or why. This actually teaches you HOW to find and fix real problems.
The biggest thing I learned: measure first, optimize later. Sounds obvious, but I've definitely been guilty of tweaking configs without actually knowing if they helped. The baseline → profile → fix → measure again approach is so simple but makes total sense.
What I loved:
The copy-paste configs actually work (tried the OPcache settings, saw immediate improvement)
Super honest about JIT - "it's not magic, measure it" - thank you!
The database section hit home hard. Yeah, 80% of my slowness was N+1 queries...
Ready-to-use code snippets, not pseudocode nonsense
The async I/O section was eye-opening. I always wondered when to use ReactPHP vs just sticking with regular PHP-FPM. The "I/O-bound = async wins, CPU-bound = meh" rule is gold.