PHP Performance Optimization Techniques I Use in Production
Where PHP applications actually lose time in production, and the concrete fixes I reach for before considering a rewrite.
"PHP is slow" is rarely true in 2026 — PHP 8.x with OPcache and JIT is fast. Slow PHP applications are almost always slow because of I/O, not the language. Here's the order I work through when diagnosing a sluggish PHP app.
Profile before optimizing #
Guessing where time goes wastes more time than it saves. I reach for Xdebug's profiler locally and Blackfire or a lightweight custom timer in production:
$start = hrtime( true );
$result = $expensive_operation();
$duration_ms = ( hrtime( true ) - $start ) / 1_000_000;
if ( $duration_ms > 100 ) {
error_log( sprintf( 'Slow operation: %s took %.2fms', __FUNCTION__, $duration_ms ) );
}Nine times out of ten, the profile points at the database, an external API call, or serialization — not application logic.
OPcache is not automatically configured well #
OPcache is usually enabled by default on modern hosting, but the defaults are conservative. For a production app that doesn't change often:
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.jit=tracing
opcache.jit_buffer_size=64Mopcache.validate_timestamps=0 is the important one — it stops PHP from checking file
modification times on every request. It means you must clear OPcache on deploy, which
any competent deploy script should already do.
N+1 queries are the most common real bottleneck #
This shows up constantly in ORMs (Eloquent, Doctrine) and in raw WordPress loops:
// N+1: one query per post to fetch its author
foreach ( $posts as $post ) {
$author = get_user_by( 'id', $post->author_id );
}// Batched: one query for all authors
$author_ids = array_unique( array_column( $posts, 'author_id' ) );
$authors = get_users( [ 'include' => $author_ids ] );
$authors_by_id = array_column( $authors, null, 'ID' );In Laravel, this is ->with('author') instead of accessing $post->author inside a
loop — the fix pattern is the same across frameworks: batch the fetch, don't fetch
inside the iteration.
Laravel's DB::listen() and the laravel-debugbar package will surface N+1 patterns in
local development before they hit production. I keep it enabled outside of production
environments.
Autoloading and dependency resolution #
composer dump-autoload -o generates a classmap-based autoloader instead of resolving
PSR-4 paths on every request. On applications with large dependency trees, this alone
can shave meaningful time off cold-start latency in serverless or short-lived worker
environments.
Serialize less, stream more #
Building a large array in memory and json_encode-ing it at the end blocks on the
entire payload before the first byte is sent. For large exports or API responses,
streaming output (fpassthru, generators with yield, or chunked HTTP responses) keeps
memory flat and gets bytes to the client sooner:
function stream_csv_rows( iterable $rows ): Generator {
foreach ( $rows as $row ) {
yield implode( ',', $row ) . "\n";
}
}Lessons learned #
The fastest optimization is almost never rewriting application code — it's fixing how data moves in and out of the app: batching queries, configuring OPcache correctly, and not holding entire response payloads in memory before sending the first byte.