The fastest code is the code that does not run. Many slow pages repeat the same work again and again.
1. Move invariant work out of loops
<?php
$prices = [100, 250, 400];
$taxRate = 0.18;
// Repeats the same calculation on every pass
$slow = [];
foreach ($prices as $price) {
$multiplier = 1 + (18 / 100);
$slow[] = $price * $multiplier;
}
// Computed once
$multiplier = 1 + $taxRate;
$fast = array_map(fn($p) => $p * $multiplier, $prices);
echo $slow === $fast ? "same result" : "different";
same result
2. Remember results (memoization)
If a function is called many times with the same input, store the answer.
<?php
function fibonacci(int $n): int
{
static $memo = [0 => 0, 1 => 1];
return $memo[$n] ??= fibonacci($n - 1) + fibonacci($n - 2);
}
echo fibonacci(50);
12586269025
Without the memo, fibonacci(50) would make billions of calls. With it, each value is computed once.
3. Build a lookup instead of searching repeatedly
<?php
$orders = [["id" => 1, "user" => 7], ["id" => 2, "user" => 9], ["id" => 3, "user" => 7]];
$users = [["id" => 7, "name" => "Ana"], ["id" => 9, "name" => "Ben"]];
// Slow idea: search the users list for every order (a loop inside a loop)
// Fast idea: index the users by id once
$byId = array_column($users, "name", "id");
foreach ($orders as $order) {
echo "Order {$order['id']}: {$byId[$order['user']]}\n";
}
Order 1: Ana
Order 2: Ben
Order 3: Ana
An array lookup by key is nearly instant. Searching a list with in_array() or a nested loop gets slower as the data grows.
4. The N+1 query problem
The classic performance bug. You run one query to get a list, then one more query per item.
<?php
$pdo = require __DIR__ . "/db.php";
$posts = $pdo->query("SELECT id, name AS title, category FROM products")->fetchAll();
$queries = 1;
// N+1: a query for each product's category count
foreach ($posts as $p) {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM products WHERE category = ?");
$stmt->execute([$p["category"]]);
$stmt->fetchColumn();
$queries++;
}
echo "N+1 style: $queries queries\n";
// Better: ONE query for all the counts
$counts = $pdo->query("SELECT category, COUNT(*) AS n FROM products GROUP BY category")->fetchAll(PDO::FETCH_KEY_PAIR);
echo "Grouped: 2 queries in total, categories: " . count($counts);
N+1 style: 6 queries
Grouped: 2 queries in total, categories: 3
With five products it is a few queries. With 5,000 it is 5,001 queries, and the page crawls. Fix it with a JOIN, a GROUP BY or WHERE id IN (...) that fetches everything at once.
5. Batch remote calls
Do not call an API once per item if it has a bulk endpoint. Do not send one email per row inside a web request. Queue it, or send in one batch.
6. Do not compute what you will not show
Fetch only the columns and rows you need, calculate totals in SQL when possible, and paginate. A page that builds a list of 10,000 items to show 10 does 1,000 times more work than needed.
7. Reuse expensive objects
Create the database connection once per request and share it. Build a heavy object (a mailer, an HTTP client) once, not inside a loop.
8. Cache the results
If the answer does not change between requests, compute it once and store it: Caching Concepts.
A checklist
- Is anything inside a loop that does not depend on the loop variable?
- Is a query or API call inside a loop?
- Am I searching the same list many times? Could an indexed array help?
- Am I loading more than I show?
- Could this be cached?