Everything waits on TTFB
Time to First Byte is the floor under every other metric. Nothing renders, nothing downloads, nothing is discovered until the HTML starts arriving. Good is 800 ms; over 1.8 s is poor — and unlike the other vitals, this one is almost entirely yours to fix.
What is inside it: redirects (a full round trip each), DNS and connection setup, and then server think time — the part the code is responsible for.
The N+1 query
Each query is a network round trip to the database, typically 1–20 ms. One hundred of them is the whole page budget spent on a join the database would have done for free. ORMs hide this behind a property access, which is exactly why it keeps happening — log your query count per request and the problem becomes visible.
The two fixes are a join, or a batch: collect the ids and fetch them all in one where id in (…). And behind both: an index on the column you filter by, or the database is scanning the table every time.
/* 1 query for the list, then one more for every row: 101 round trips */
const orders = await db.query("select * from orders limit 100");
for (const order of orders) {
order.customer = await db.query("select * from customers where id = ?", order.customer_id);
}
/* one query */
const orders = await db.query(`
select orders.*, customers.name
from orders join customers on customers.id = orders.customer_id
limit 100`);
Caching, in layers
stale-while-revalidate is the setting most worth knowing: serve the cached copy immediately, refresh it in the background. Users get the fast path and never wait for the slow one.
| Layer | Saves | Hard part |
|---|---|---|
| CDN / edge cache | the whole request, including the trip to your region | invalidation, and anything personalised |
| Full-page / fragment cache | render time | knowing when it is stale |
| Query result cache | the database | the same, one layer down |
| Connection pool | the handshake per request | sizing it |
A cache is not a fix
If a page takes 2 s to build and you cache it at 80 %, the average looks like 400 ms and the dashboard turns green. But one user in five still waits 2 s, and it is disproportionately the first visitor to each page, the one who searched for something new, the one whose cache entry just expired.
Field metrics are reported at the 75th percentile precisely because averages hide this. Fix the page, then cache it — in that order.
Send something early
Streamed HTML turns server think time into parallel work: the stylesheet is already downloading while the database is still answering. 103 Early Hints does the same thing one step earlier, before the response has started at all.
And move the work closer: an edge function in the user’s region removes an intercontinental round trip from every single request.
<!-- flushed immediately -->
<!doctype html><html><head>
<link rel="stylesheet" href="/app.css">
<link rel="preload" href="/hero.avif" as="image">
</head><body>
<!-- … flushed as the data resolves … -->