The first rule of performance: measure, do not guess. Programmers are often wrong about what is slow. Time spent speeding up code that was not the problem is wasted, and it can make the code harder to read.
Time a piece of code
<?php
function timeIt(callable $fn, int $times = 1): float
{
$start = hrtime(true); // nanoseconds, monotonic
for ($i = 0; $i < $times; $i++) {
$fn();
}
return (hrtime(true) - $start) / 1_000_000; // milliseconds
}
$ms = timeIt(fn() => array_sum(range(1, 100000)), 20);
echo is_float($ms) && $ms >= 0 ? "measured something" : "bad";
measured something
hrtime(true) is better than microtime(true) for measuring, because it does not jump when the system clock changes. Print $ms yourself to see the real number on your machine. It will differ on every computer and every run.
Memory
<?php
$before = memory_get_usage();
$data = range(1, 50000);
$after = memory_get_usage();
echo $after > $before ? "the array used memory\n" : "?\n";
echo memory_get_peak_usage() >= $after ? "peak is at least the current use" : "?";
the array used memory
peak is at least the current use
memory_get_peak_usage() gives the highest amount used so far, the number you compare with memory_limit.
Compare two approaches fairly
<?php
$data = range(1, 20000);
$a = hrtime(true);
$viaLoop = 0;
foreach ($data as $n) {
$viaLoop += $n;
}
$loopTime = hrtime(true) - $a;
$b = hrtime(true);
$viaBuiltin = array_sum($data);
$builtinTime = hrtime(true) - $b;
echo $viaLoop === $viaBuiltin ? "same result\n" : "different!\n";
echo "Both timed: " . (($loopTime > 0 && $builtinTime > 0) ? "yes" : "no");
same result
Both timed: yes
Always check that two versions give the same result, then compare times. Repeat the test several times, because the first run includes warm-up effects.
Log slow requests
In production, you cannot watch every page. Record the slow ones.
<?php
$start = hrtime(true);
register_shutdown_function(function () use ($start): void {
$ms = (hrtime(true) - $start) / 1_000_000;
if ($ms > 500) {
error_log(sprintf("Slow request: %s took %.0f ms, %.1f MB", $_SERVER["REQUEST_URI"] ?? "cli", $ms, memory_get_peak_usage() / 1048576));
}
});
Profilers
A profiler shows where time and memory go, function by function.
| Tool | Notes |
|---|---|
| Xdebug profiler | Writes files you open in a viewer (QCachegrind, Webgrind) |
| Blackfire | A hosted profiler with comparisons |
| XHProf / Tideways | Light profilers for production sampling |
| Laravel Telescope, Symfony Profiler, Query Monitor (WordPress) | Show queries and timings in the browser |
Open the report and look at the top entries. Usually a few calls (database queries, HTTP requests, big loops) account for most of the time.
What usually makes PHP pages slow
- Database queries: too many, slow, or unindexed (see Query Optimization).
- Remote calls: APIs without timeouts or caching.
- Missing caches (OPcache, data caches).
- Large loops over large data (see Efficient Loops).
- Too much memory: loading everything at once.
- Heavy front-end: huge images, unminified JavaScript. This is not PHP, but it is what the visitor feels.
PHP itself is rarely the bottleneck. A page that takes two seconds is almost always waiting for something.
The optimisation process
- Decide on a goal (“the product page loads in under 300 ms”).
- Measure the current state.
- Find the biggest cost with a profiler.
- Change one thing.
- Measure again. Keep it only if it helped.
Do not micro-optimise
++$i or $i++, makes no measurable difference. Spend your effort on queries, caching and design, and keep the code readable.