Skip to content
Happy Programming Guide
Start learning
PHP

PHP Performance Optimization

Where PHP applications actually spend their time, how to measure before you optimise, and the fixes that matter: OPcache, N+1 queries, indexes and caching.

A glimpse inside an opened mouse, showcasing the intricate circuitry that powers our everyday computing

Most slow PHP applications are not slow because of PHP. They are slow because of database queries — usually the same query running hundreds of times inside a loop. This guide covers how to find out where the time actually goes, and then the fixes in the order that pays best: measure, fix the queries, enable OPcache, add caching, and only then micro-optimise.

Measure before you change anything#

PHP
<?php
$start = hrtime(true);

// the code you suspect

$ms = (hrtime(true) - $start) / 1_000_000;
error_log(sprintf("Block took %.2f ms", $ms));

A rough breakdown of a page tells you where to look:

PHP
<?php
final class Timer
{
    private array $marks = [];
    private float $start;

    public function __construct() { $this->start = hrtime(true); }

    public function mark(string $label): void
    {
        $this->marks[$label] = (hrtime(true) - $this->start) / 1_000_000;
    }

    public function report(): string
    {
        $out = "";
        $previous = 0.0;
        foreach ($this->marks as $label => $at) {
            $out .= sprintf("%-28s %8.2f ms\n", $label, $at - $previous);
            $previous = $at;
        }
        return $out;
    }
}

For anything serious, install a profiler. Xdebug’s profiler or Blackfire will show you the actual call tree and where the time goes, which is far more reliable than guessing.

PHP
<?php
// How many queries ran, and how long they took
$pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, [LoggingStatement::class]);

The N+1 query problem#

This is the most common cause of a slow PHP page, by a wide margin:

PHP
<?php
// 1 query for the posts
$posts = $pdo->query("SELECT id, title, author_id FROM posts LIMIT 50")->fetchAll();

foreach ($posts as $post) {
    // ...then 50 more, one per post
    $stmt = $pdo->prepare("SELECT name FROM authors WHERE id = ?");
    $stmt->execute([$post["author_id"]]);
    $post["author"] = $stmt->fetchColumn();
}

Fifty-one queries. Each is fast; the round trips are what cost you. Two ways to fix it:

PHP
<?php
// Option 1: a join
$sql = "SELECT p.id, p.title, a.name AS author
        FROM posts p
        JOIN authors a ON a.id = p.author_id
        LIMIT 50";
$posts = $pdo->query($sql)->fetchAll();
PHP
<?php
// Option 2: two queries, joined in PHP
$posts = $pdo->query("SELECT id, title, author_id FROM posts LIMIT 50")->fetchAll();

$ids = array_unique(array_column($posts, "author_id"));
$placeholders = implode(",", array_fill(0, count($ids), "?"));

$stmt = $pdo->prepare("SELECT id, name FROM authors WHERE id IN ($placeholders)");
$stmt->execute($ids);
$authors = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);

foreach ($posts as &$post) {
    $post["author"] = $authors[$post["author_id"]] ?? "Unknown";
}
unset($post);

Fifty-one queries become one or two. On a page that was taking two seconds, this alone often brings it under two hundred milliseconds.

Indexes#

SQL
EXPLAIN SELECT * FROM orders WHERE customer_id = 42 AND status = 'paid';

If the output says type: ALL, the database is reading every row. Add an index on the columns you filter and sort by:

SQL
CREATE INDEX idx_orders_customer_status ON orders (customer_id, status);

Column order in a composite index matters: it can serve queries filtering on customer_id alone, or on both, but not on status alone. Put the most selective column first.

Also: select the columns you need rather than SELECT *. Pulling a large text column you never display costs bandwidth and memory on every row.

OPcache#

Without OPcache, PHP recompiles every file on every request. With it, compiled bytecode is kept in memory. This is typically the largest single improvement available, and it is a configuration change rather than a code change.

INI
opcache.enable=1
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.jit_buffer_size=64M
opcache.jit=tracing
PHP
<?php
print_r(opcache_get_status()["opcache_statistics"]);

Watch two numbers: the hit rate should be well above 95 per cent, and oom_restarts should be zero. Restarts mean the memory setting is too small.

Caching#

The fastest work is work you do not do. Cache in layers, cheapest first:

PHP
<?php
// 1. Within a single request
final class Repository
{
    private array $cache = [];

    public function find(int $id): ?array
    {
        return $this->cache[$id] ??= $this->fetchFromDatabase($id);
    }
}
PHP
<?php
// 2. Across requests, with Redis or Memcached
$key = "products:featured";
$json = $redis->get($key);

if ($json === false) {
    $products = $pdo->query("SELECT ...")->fetchAll();
    $redis->setex($key, 300, json_encode($products));
} else {
    $products = json_decode($json, true);
}
PHP
<?php
// 3. HTTP caching, so the browser does not ask again
header("Cache-Control: public, max-age=300");
header("ETag: " . md5($body));

if (($_SERVER["HTTP_IF_NONE_MATCH"] ?? "") === md5($body)) {
    http_response_code(304);
    exit;
}

The hard part of caching is invalidation. Prefer short expiry times to clever invalidation logic until you have evidence you need more.

Loops and memory#

PHP
<?php
// Loads the whole file into memory
$lines = file("huge.csv");

// Reads one line at a time
$handle = fopen("huge.csv", "r");
while (($row = fgetcsv($handle)) !== false) {
    process($row);
}
fclose($handle);

A generator gives you the same memory profile with a nicer shape:

PHP
<?php
function readRows(string $path): Generator
{
    $handle = fopen($path, "r");
    try {
        while (($row = fgetcsv($handle)) !== false) {
            yield $row;
        }
    } finally {
        fclose($handle);
    }
}

foreach (readRows("huge.csv") as $row) {
    process($row);
}

Also worth knowing: calling count() inside a loop condition re-counts every iteration. Assign it once beforehand.

Things that matter less than people think#

  • Single versus double quotes. The difference is unmeasurable in real applications.
  • echo versus print. Same.
  • Removing all comments. Comments are stripped at compile time and cached by OPcache.
  • Micro-tuning a function called ten times. Optimise what runs ten thousand times.

These come up constantly in older performance advice. Time spent on them is time not spent on the queries.

Infrastructure#

  • Run a current PHP version. PHP 8 is several times faster than PHP 5, and each release adds more.
  • Use PHP-FPM with a process count matched to your memory.
  • Enable gzip or brotli at the web server.
  • Serve static files from the web server, never through PHP.
  • Put a CDN in front of images, CSS and JavaScript.
Terminal
composer dump-autoload --optimize --classmap-authoritative

That last command converts Composer’s autoloader into a flat map, removing a filesystem check on every class load.

Questions people ask#

Is PHP slow?

Modern PHP is fast enough that the database and network dominate almost every real application. Performance complaints about PHP usually date from version 5.

Should I turn on JIT?

It helps CPU-heavy work such as image processing and mathematics. For typical web requests, which are dominated by I/O, the gain is small. Measure with it on and off.

Redis or Memcached?

Redis, in most cases — it supports more data types, can persist to disk, and does everything Memcached does. Memcached is marginally simpler if you only need string caching.

How do I know if a page is slow enough to matter?

Look at real request timings in your server logs rather than at your local machine. A page that takes 300 ms locally may take two seconds under production load with a cold cache.

Where to go next#

PHP and databases, where most of the time goesRead next

Keep reading

PHP

Working with APIs in PHP

Call a REST API from PHP with cURL or Guzzle, handle JSON and errors properly, add authentication, and build a small API…

4 min read

PHP

PHP PDO Explained With Examples

PDO is the standard way to talk to a database from PHP. Connecting, prepared statements that stop SQL injection, fetching rows, transactions…

5 min read

PHP

Introduction to PHP

What PHP is, how it differs from JavaScript, how a request becomes a page, and your first working script — with the…

5 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *