Performance Optimization

By the end of this lesson you'll know how to measure a PHP app, find the real bottleneck, and fix it — killing N+1 queries, turning on OPcache, caching expensive work, and keeping memory flat with generators — so your pages stay fast under real traffic.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Measure First — Never Guess

The golden rule of performance is measure before you optimise . Your instinct about what's slow is wrong more often than it's right, and "optimised" code that you never measured is just code that's now harder to read. The quickest measuring tool is microtime(true) , which returns the current time as a float — take one reading before a block and one after, and the difference is how long it took. Pair it with memory_get_peak_usage(true) to see the most RAM the script ever used.

For real apps you'll graduate from manual timers to a proper profiler — a tool that records how long every function and query took in a request. Xdebug 's profiler ships with PHP and writes a cachegrind file you open in a viewer; Blackfire.io is a hosted profiler with beautiful call graphs. Both answer the only question that matters: where is the time actually going?

2️⃣ Don't Repeat Work Inside Loops

A loop that runs a million times amplifies any waste inside it a million-fold. The classic mistake is calling something like count($items) in the loop condition , so it's recomputed on every single pass. Anything whose answer can't change inside the loop is invariant — compute it once above the loop and reuse the variable. Compare these two:

The output is identical — but the "after" version does the expensive part once instead of N times. With five items it's invisible; with a database call or a heavy calculation in the loop body, hoisting it out is the difference between snappy and unusable.

3️⃣ The N+1 Query Problem (the big one)

Most PHP slowness lives in the database , and the worst offender is the N+1 query problem : one query to fetch a list, then one more query per row in that list. Fetch 100 users, loop over them asking for each one's orders, and you've fired 101 separate queries — each a round-trip to the database. Here's the trap:

The fix is eager loading : gather the IDs, fetch every related row in one WHERE user_id IN (...) query, then group the results in PHP with an array. 101 queries become 2 — and they stay 2 whether you have 100 users or 100,000.

Two more database essentials: an index is a lookup structure that lets the database jump straight to matching rows instead of scanning the whole table — always index the columns you filter or join on (like a foreign key). And a value used in a query should be parameterised or cast (here intval ) so it's safe. ORMs like Laravel's Eloquent expose eager loading as User::with('orders') — same fix, less typing.

4️⃣ Cache Expensive Work

If a result is slow to compute and rarely changes, cache it: do the work once, store the answer, and serve the stored copy next time. APCu is the simplest cache — an in-memory store local to one server. Redis works identically but is a shared service across many servers. The pattern never changes: check the cache → on a miss, compute and store with a TTL → return . A TTL (time-to-live) is how many seconds the cached value stays valid before it's recomputed.

5️⃣ Keep Memory Flat with Generators & Lazy Loading

Speed isn't the only resource — memory matters too. Building a giant array to loop over it holds every element in RAM at once. A generator (a function that uses yield ) produces values lazily , one at a time, so peak memory stays flat no matter how many items you process. This is the same idea as lazy loading : don't fetch or build something until you actually need it.

Use generators whenever you read a large file line-by-line, page through a big query result, or transform a long stream — anything where you don't need every item in memory simultaneously.

6️⃣ Turn On OPcache (and Know When JIT Helps)

Every request, PHP normally re-reads and re-compiles your source files into bytecode before running them. OPcache stores that compiled bytecode in shared memory so the parse-and-compile step is skipped — commonly a 30-70% speed-up for free . It ships with PHP; you just enable it. In production set opcache.validate_timestamps=0 so PHP never checks file timestamps per request, and clear the cache on each deploy so new code is picked up.

JIT (Just-In-Time compilation), added in PHP 8, goes one step further by compiling hot code paths to native machine code. It's a big win for CPU-bound work — maths, image processing, simulations — but typical web apps spend most of their time waiting on the database, so JIT often helps them only a little. Turn it on with opcache.jit=tracing , then measure ; if the database is your bottleneck, fix that first.

7️⃣ Autoloader, Output Buffering & gzip

Three quick production wins. First, optimise the Composer autoloader : composer dump-autoload --classmap-authoritative pre-builds one complete class-to-file map, so PHP stops hitting the filesystem to locate classes on every request. Second, output buffering with ob_start() collects your output in memory and sends it in one efficient chunk instead of many small writes. Third, enable gzip compression at the web server (or with ob_start("ob_gzhandler") ) so HTML, CSS, and JS travel to the browser much smaller — often a 70%+ reduction in bytes over the wire.

8️⃣ Your Turn — Time a Block

Now you try. The script below is almost complete — fill in each ___ using the 👉 hint, then run it and compare against the Output panel. You only need two microtime(true) readings.

One more — hoist the invariant count() out of the loop so it's computed once, not on every pass.

📋 Quick Reference — PHP Performance

No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is exactly the measure → cache → re-measure loop you'll use on every real bottleneck.

Practice quiz

What is the golden rule before optimising PHP performance?

  • Rewrite every loop first
  • Add caching everywhere
  • Measure (profile) before you change anything
  • Switch to a faster framework

Answer: Measure (profile) before you change anything. Your instinct about what's slow is often wrong; measure with microtime() and profile with Xdebug or Blackfire before optimising.

Which function gives the current time as a float to time a block of code?

  • microtime(true)
  • time()
  • date()
  • sleep()

Answer: microtime(true). microtime(true) returns the current time as a float; subtract two readings to measure how long a block took.

What is the N+1 query problem?

  • Running one query that returns N+1 rows
  • Adding one index too many
  • Caching N+1 results
  • One query for a list, then one more query per row in that list

Answer: One query for a list, then one more query per row in that list. N+1 is one query to fetch a list plus one query per row — 100 users becomes 101 round-trips, which crawls.

How do you fix the N+1 query problem?

  • Add more web servers
  • Eager-load with one WHERE id IN (...) query, then group in PHP
  • Run each query in a separate thread
  • Increase the database timeout

Answer: Eager-load with one WHERE id IN (...) query, then group in PHP. Eager loading gathers the ids and fetches all related rows in a single IN(...) query, turning 101 queries into 2.

In a loop, where should an invariant like count($items) be computed?

  • Once, above the loop, into a variable
  • In the loop condition every pass
  • After the loop
  • Inside the loop body twice

Answer: Once, above the loop, into a variable. Anything whose answer can't change inside the loop should be hoisted above it and reused, so it runs once instead of N times.

What does OPcache store to speed up PHP, commonly 30-70%?

  • Database query results
  • Rendered HTML pages
  • Compiled bytecode in shared memory
  • User session data

Answer: Compiled bytecode in shared memory. OPcache keeps compiled bytecode in shared memory so PHP skips re-parsing and re-compiling files on every request.

What should opcache.validate_timestamps be set to in production?

  • 1, so PHP checks files every request
  • 0, so PHP never stats files per request (clear the cache on deploy instead)
  • It must be left unset
  • -1 to disable OPcache

Answer: 0, so PHP never stats files per request (clear the cache on deploy instead). Set validate_timestamps=0 in production so PHP never checks file timestamps per request; clear the cache on each deploy.

What is the check-miss-store caching pattern?

  • Always recompute, then store
  • Store first, then check
  • Cache only on the second request
  • Check the cache; on a miss, compute and store with a TTL; return

Answer: Check the cache; on a miss, compute and store with a TTL; return. Try the cache first; only on a miss do the slow work and store it with a sensible TTL, then return — the same pattern for APCu or Redis.

How does a generator (yield) keep memory flat over a huge dataset?

  • It compresses the array
  • It produces values lazily, one at a time, instead of building the whole array
  • It moves the data to Redis
  • It runs in a separate process

Answer: It produces values lazily, one at a time, instead of building the whole array. A generator yields values lazily one at a time, so peak memory stays flat no matter how many items you process.

When does PHP 8's JIT give the biggest speed-up?

  • On database-bound web requests
  • On every request equally
  • On CPU-bound work like maths, image processing, or simulations
  • Only when OPcache is disabled

Answer: On CPU-bound work like maths, image processing, or simulations. JIT compiles hot paths to native machine code, a big win for CPU-bound work; typical web apps waiting on the database benefit only a little.