Web Performance · 7 min read

Full-Stack Web Performance: From Next.js SSR to Database Query Budgets

Optimizing Core Web Vitals, server component rendering, edge caching, and query budgets across modern stacks.

By Praful Patel · Last updated
Full-Stack Web Performance: From Next.js SSR to Database Query Budgets - article cover
Full-Stack Web Performance: From Next.js SSR to Database Query Budgets - article cover

Short answer

Core Web Vitals are the symptom; the query budget is usually the disease. Front-end optimisation on top of an unprofiled backend produces numbers that look good in the lab and regress the moment real traffic and real data volumes arrive. Profile the data layer first, fix rendering strategy second, and treat asset delivery as the last step rather than the first.

Most performance work starts at the wrong end. Someone runs Lighthouse, sees a poor LCP, and starts compressing images. The images were 8% of the problem. The template was issuing 340 database queries and the server took 1.8 seconds to send the first byte.

This is the order I work in, and why each step comes where it does.

The metrics, and what each one is actually measuring

Metric Target Dominated by
LCP < 2.5s Time to first byte, then the loading path of the largest element
INP < 200ms Long JavaScript tasks blocking the main thread during interaction
CLS < 0.1 Elements without reserved space: images, ads, late-loading fonts
TTFB < 800ms Server work – queries, uncached API calls, template rendering

TTFB is not one of the Core Web Vitals, which is why it gets ignored. It is also a floor under LCP: if the server takes 1.5 seconds to respond, no amount of front-end work will produce a 2.5-second LCP. Start here.

1. The query budget

Set a number and enforce it. Something like: no template renders more than 30 database queries, and no single query exceeds 50ms. The specific numbers matter less than having them written down and checked automatically, because query counts do not grow through one bad decision – they grow through fifty reasonable ones.

The patterns that produce runaway counts are consistent:

  • N+1 in loops. A listing that fetches 20 items and then makes a separate call per item for an author, a term or a meta value. Prime the caches in one query before the loop.
  • Counting rows nobody displays. Pagination queries that calculate a total for a page that never shows one. In WordPress, 'no_found_rows' => true removes an expensive SQL_CALC_FOUND_ROWS pass.
  • Unindexed meta lookups. Querying by a meta value across a large table without an index is a full scan. It is fast on a development database with 200 rows and catastrophic at 200,000.
  • Uncached HTTP calls in the render path. A third-party API call inside a template makes your page as slow as their worst day. Cache the response and serve stale on failure.

Profile with real data volumes. A query plan on a development dataset tells you almost nothing about the plan the database will choose in production.

2. Caching layers, and knowing which one you are in

Each layer serves a different miss:

  • Object cache (Redis or Memcached) – avoids repeating the same query within and across requests. The highest-leverage layer for a dynamic application, and the one most often missing.
  • Page cache – avoids running the application at all for anonymous traffic.
  • Edge / CDN – avoids crossing the network to your origin.

The question worth asking about each is not “is it enabled” but “what is the hit rate, and what happens on a miss?” A page cache with a 40% hit rate and a two-second miss is a slow site for nearly half its visitors. Measure the miss path, because that is the path real users hit after every deploy and every cache purge.

WordPress VIP’s platform makes most of this mandatory rather than optional, which is a large part of why building against those constraints improves a codebase – I have written about that in Engineering for WPVIP: High-Concurrency Standards & VIP Go Compliance.

3. Rendering strategy in Next.js

Choosing a rendering mode is choosing where you pay the cost:

Strategy Right when Cost
Static (SSG) Content changes rarely and is the same for everyone Build time grows with page count
Incremental (ISR) Content changes on a predictable cadence Some users see stale content
Server-rendered Content is personalised or must be current Every request pays for server work
Client-fetched Below the fold, or genuinely interactive Adds a round trip after hydration

The common mistake is rendering a whole page on the server because one component needs live data. Split it: static or incremental shell, with the dynamic fragment streamed in. Server components make that boundary explicit rather than implicit.

4. JavaScript is what INP is made of

INP measures how long the main thread is busy when a user interacts. Reducing it is mostly about doing less work, not about doing the same work faster:

  • Audit the bundle before optimising it. A date library imported for one format call, a full icon set for six icons, an analytics SDK loaded eagerly – these are the usual findings, and they are removals rather than optimisations.
  • Break up long tasks. Anything over 50ms blocks input. Yield between chunks of work rather than processing a large array synchronously.
  • Defer third-party scripts. Tag managers, chat widgets and session recorders are frequently the largest contributors to INP, and they are the easiest to move off the critical path.
  • Hydrate selectively. A static marketing section does not need to become interactive.

5. Images, fonts and layout stability

Last, and genuinely worth doing once the above is handled:

  • Serve modern formats, size to the layout rather than to the source file, and always set explicit dimensions so the browser reserves space before the image loads.
  • The LCP image must not be lazy-loaded. This is the most common self-inflicted LCP regression – a blanket lazy-loading rule applied to the hero image.
  • Self-host fonts or preconnect early, use font-display: swap, and match fallback metrics so the swap does not shift the layout.
  • Reserve space for anything that arrives late: embeds, ads, banners injected by scripts.

Measure in the field, not only in the lab

Lighthouse runs on your machine with your network and no third-party scripts blocked by an ad blocker. Real users are on worse networks and older devices, and their distribution is what Google measures. Collect real-user metrics, look at the 75th percentile rather than the median, and segment by device – a p75 that looks fine in aggregate frequently hides a mobile experience that is twice as slow.

Questions people actually ask

My Lighthouse score is 95 but users say the site is slow. Why?
Lighthouse is a lab test on a simulated network from one location, with none of the conditions real users have – cold caches, slower devices, third-party scripts, and distance from your origin. Check field data at the 75th percentile segmented by device type. The gap is usually mobile, and usually TTFB or main-thread blocking rather than anything Lighthouse’s synthetic run exercises.

What is a reasonable query budget?
Thirty queries per template render, with no single query over 50ms, is a defensible starting point for a content-driven application. The number matters less than enforcing it automatically in CI, because query counts grow gradually through individually reasonable additions and nobody notices the aggregate until it is a problem.

Should I use SSR or static generation?
Static or incremental wherever the content is the same for everyone, server-rendered only where it is personalised or must be current. The useful move is usually splitting the page rather than choosing one mode for all of it: a static shell with the dynamic fragment streamed in gives you both.

Does a CDN fix a slow site?
It fixes network distance for cacheable assets, which is real but narrow. It does nothing for a slow origin on a cache miss, and nothing at all for INP, which is entirely determined by JavaScript on the user’s device. If TTFB is slow on a miss, a CDN moves the problem rather than solving it.

How do I stop performance regressing after it is fixed?
Enforce it in CI. Budgets on bundle size and query count that fail the build, plus field monitoring with an alert on the 75th percentile. Performance achieved once and not defended degrades within a couple of release cycles – not through one bad change, but through many small ones.

← Back to all insights