Performance

Course 3 · Ch 9
Performance: Opcache, Profiling, Caching Strategies
Making PHP fast in the ways that actually matter, and finding out where time is really being spent before optimising anything

Every chapter so far has focused on correctness, structure, and security. This chapter covers the other axis: making an application fast enough — and, more importantly, knowing exactly where it's slow before changing anything, rather than guessing.

OPcache — Caching Compiled PHP, Not Application Data

Every time a plain PHP script runs, PHP first compiles the source code into "opcodes" before executing them — a real cost, repeated on every single request, for files that haven't changed at all between requests.

PHP source file Compile → opcodes without OPcache: every request Execute
OPcache stores the compiled opcodes in shared memory, skipping recompilation on every later request
# php.ini opcache.enable=1 opcache.memory_consumption=128 opcache.max_accelerated_files=10000 opcache.revalidate_freq=2
OPcache caches COMPILED CODE, never your application's data
A genuinely common confusion: OPcache has nothing to do with caching database query results, API responses, or rendered HTML — it only avoids recompiling PHP source files that haven't changed. Application-level caching (covered below) is an entirely separate, additional concern.

Profiling — Finding Out Where Time Actually Goes

Guessing at performance problems is genuinely unreliable — the code that "feels slow" while reading it is frequently not where the real time is spent. A profiler measures actual execution time per function call, across a real run of the application.

# Xdebug's profiler (one common option), enabled via php.ini: xdebug.mode=profile xdebug.output_dir=/tmp/profiles # produces a cachegrind file, viewable with a tool like KCachegrind/QCacheGrind, # showing exactly which function calls consumed the most cumulative time
A simple, dependency-free profiling technique: manual timing
<?php $start = microtime(true); $posts = $post->all(); $elapsed = microtime(true) - $start; echo "Query took: " . round($elapsed * 1000, 2) . "ms"; ?>
Wrapping a suspected slow section with microtime(true) before and after gives a quick, genuinely useful measurement without installing any tooling — a reasonable first step before reaching for a full profiler.

The N+1 Query Problem — A Classic Performance Bug

<?php // BAD — one query to get posts, then ANOTHER query per post inside the loop $posts = $pdo->query("SELECT * FROM posts")->fetchAll(); foreach ($posts as $post) { $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); $stmt->execute(['id' => $post['user_id']]); $author = $stmt->fetch(); // for 100 posts, this runs 100 EXTRA queries } // GOOD — one query total, using a JOIN $posts = $pdo->query(" SELECT posts.*, users.name AS author_name FROM posts JOIN users ON users.id = posts.user_id ")->fetchAll(); ?>

"N+1" refers to the pattern: 1 initial query, then N more queries (one per row) inside a loop — for 100 posts, that's 101 total database round-trips instead of 1. This is one of the single most common real-world PHP performance problems, and it's a structural fix (combining queries), not a "make PHP faster" fix.

Application-Level Caching

<?php function getPopularPosts(PDO $pdo): array { $cacheFile = '/tmp/popular_posts_cache.json'; if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 300) { return json_decode(file_get_contents($cacheFile), true); // cache is fresh (under 5 min old) } $posts = $pdo->query("SELECT * FROM posts ORDER BY views DESC LIMIT 10")->fetchAll(); // genuinely slow query file_put_contents($cacheFile, json_encode($posts)); return $posts; } ?>

A simple file-based cache here; real applications typically use Redis or Memcached for this purpose instead. The core idea is identical regardless of storage: run an expensive operation once, store the result, and serve the stored result to subsequent requests until it's considered stale.

LayerCachesExample
OPcacheCompiled PHP opcodesAutomatic, set in php.ini
Application cacheExpensive computed results (queries, API responses)Redis, Memcached, file-based
HTTP cache headersWhole responses, at the browser/CDN levelCache-Control, ETag headers
Caching introduces a new problem: stale data
A cache that's too "sticky" can show outdated information after the underlying data has genuinely changed — cache invalidation (clearing or updating a cache entry exactly when the relevant data changes) is often the harder half of any caching strategy, not the caching itself.

Coding Challenges

Challenge 1

Explain in your own words the difference between what OPcache caches and what an application-level cache (like Redis) caches, using a concrete example of each kind of content being cached.

📄 View solution
Challenge 2

Identify the N+1 query problem in this code, and rewrite it using a single JOIN query instead: $comments = $pdo->query("SELECT * FROM comments")->fetchAll(); foreach ($comments as $c) { $stmt = $pdo->prepare("SELECT name FROM users WHERE id = :id"); $stmt->execute(['id' => $c['user_id']]); $author = $stmt->fetch(); }

📄 View solution
Challenge 3

Write a function getCachedOrCompute(string $cacheFile, int $ttlSeconds, callable $computeFn) that generalises the chapter's file-cache example — if a fresh cache file exists, return its decoded contents; otherwise call $computeFn(), cache its result, and return it. Show it being used to cache the result of an expensive function getExpensiveReport().

📄 View solution

Chapter 9 Quick Reference

  • OPcache — caches COMPILED PHP code (opcodes), not application data; configured in php.ini
  • Profiling — measure before optimising; microtime(true) for quick checks, Xdebug for full profiles
  • N+1 query problem — one query, then one MORE per row in a loop; fix with a JOIN
  • Application-level caching — store an expensive result once, serve it until stale (Redis, Memcached, file-based)
  • Cache invalidation — often the hardest part: deciding exactly when cached data becomes stale
  • Three caching layers: OPcache (code), application cache (data), HTTP cache headers (whole responses)
  • Next chapter: capstone — a small REST API with auth, database, and tests