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
SplFixedArray
or 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_children
according to RAM and traffic. -
pm.max_requests
helps recycle processes and contain leaks. - Enable slowlog and watch p95/p99 regularly.
11) The reusable workflow (checklist)
-
Target & SLO (e.g.,
/api/feed
p95 < 150 ms). -
Baseline:
- Micro: harness or PHPBench (version results).
- HTTP:
wrk
/k6
against 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 (1)
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.