DEV Community

A0mineTV
A0mineTV

Posted on

PHP 8.4 Performance Optimization — A Practical, Repeatable Guide

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

  1. Baseline first (micro + end-to-end).
  2. Profile in realistic conditions (staging/prod if you can).
  3. Fix the top 1–3 hotspots (algorithm/DB/I/O/cache).
  4. Harden runtime (OPcache/JIT, Composer authoritative, realpath cache, FPM).
  5. 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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

8) Startup & autoload

  • Composer authoritative autoloader cuts dynamic lookups and file I/O:
  composer install --no-dev --classmap-authoritative --apcu-autoloader
Enter fullscreen mode Exit fullscreen mode

or

  composer dump-autoload -a --apcu
Enter fullscreen mode Exit fullscreen mode
  • realpath cache reduces repeated path resolutions; tune if your app opens lots of files:
  realpath_cache_size=4096K
  realpath_cache_ttl=300
Enter fullscreen mode Exit fullscreen mode

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 especially pm.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)

  1. Target & SLO (e.g., /api/feed p95 < 150 ms).
  2. Baseline:
    • Micro: harness or PHPBench (version results).
    • HTTP: wrk/k6 against real endpoints and payloads.
  3. Profile: Blackfire (staging/prod) or XHProf/Tideways (dev/stage).
  4. Fix top N: algorithm → DB → I/O → cache → autoload.
  5. Harden runtime: OPcache/JIT, Composer authoritative, realpath cache, FPM.
  6. Re-measure & CI perf budget: fail PRs on regressions.
  7. 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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 nothingmeasure 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)

Collapse
 
gbhorwood profile image
grant horwood

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.