Frontendfrontendrumfield databudgetuser-perceived latency

The Half of the Budget You Cannot See From the Server

Your p99 is 80ms and users still call the app slow. Server time is one line item in a budget that also contains DNS, TLS, render-blocking CSS, JavaScript parse and execute, and an image decode on a phone three years older than your laptop.

Follow the diagnosis

Frame the diagnosis

Performance work starts from a symptom and a signal — never from a resource dashboard.

Diagnostic question
The server-side numbers are healthy and users still say the app is slow — where in the browser is the time actually going?
Symptom
Support tickets and survey answers say "slow" while every backend dashboard is green. The complaint clusters on mobile, on first visits, and on the marketing-heavy routes.
Signal
Real-user monitoring (field data): the distribution of LCP and INP per route, split by device class and connection. The misleading signal is your own lab run — a fast laptop on office fibre reproduces none of it.
SymptomSignalMeasurementHypothesisEvidenceRoot CauseChangeValidationRegression Check

One request, two budgets

Web-specific · Browser-side accounting; the specific phases exist in the Navigation Timing and Resource Timing APIs

Backend observability stops at the response byte. The user's clock starts before DNS resolves and stops when the page is *usable* — which can be seconds after your handler returned. Between those two points sit phases your service has no visibility into: connection setup, render-blocking stylesheets, script download, script parse and compile, script execution, layout, and the decode of whatever image the user actually came to see.

The budget below is an accounting device, not a timeline: in a real load several of these overlap, and the browser starts speculative fetches before the parser reaches them. Treat it the way you would treat a Latency Budgets: Spending 200 Milliseconds on Purpose on the server — a way to see which line item you can plausibly move, and which one is already small.

The important structural fact: TTFB is the only line your backend dashboards can see, and in this budget it is 16% of the total. Halving it — a heroic backend project — buys 200ms of a 2500ms experience. Removing 300KB of JavaScript that nothing on the first screen needs buys more, faster, and shows up in the field data within a day.

A 2.5s LCP target on a mid-range phone, spent line by line (serial worst case; real loads overlap)ILLUSTRATIVE
DNS + TCP + TLSFirst visit only; connection reuse removes most of it afterwards300 ms
TTFB (server work + network)The only line item visible on a backend dashboard400 ms
HTML download + parse150 ms
Render-blocking CSSNothing paints until this lands and parses250 ms
JavaScript parse + compile + executeScales with the device, not the connection600 ms
Remaining0 ms left

Lab data and field data answer different questions

Web-specific · Synthetic testing tools vs real-user monitoring

A synthetic run in CI on a fixed machine and a fixed network is a *regression detector*: it is repeatable, so a change in the number means a change in your code. It is not a description of your users, because it has one device, one connection and no cache states.

Field data is the opposite. It describes your users exactly and is useless for pinpointing a cause, because it mixes a flagship phone on 5G with a four-year-old Android on a congested cell in the same percentile. It also arrives late — a meaningful field percentile needs days of traffic, not the twelve minutes your deploy pipeline takes.

Use both, for the jobs they are each good at. The failure mode is running only one: teams with only lab data ship regressions their users feel and their CI never sees; teams with only field data know they got slower last Tuesday and cannot say which of the eleven merges did it. This is the same "no single signal explains everything" argument as Metrics, Logs, Traces, Profiles, applied at the browser.

What each source can and cannot tell you
Synthetic / labField / RUM
Answers"Did this commit make it slower?""How slow is it for real people?"
Device and networkOne you chose — repeatable, unrepresentativeThe real mix, uncontrollable
Cache stateUsually cold, by constructionMostly warm; cold visits are a minority you must segment out
Feedback speedMinutes — fits in CIDays — a percentile needs volume
Attributes a causeYes: one variable changedRarely: everything changed at once
Blind toYour actual usersWhich change caused the shift

Segment before you conclude

Web-specific · Browser field telemetry; device and connection classes are reported by web platform APIs with varying support

An aggregate "LCP p75 = 3.1s" is a number you cannot act on. The same aggregate can mean *everyone is mediocre* or *desktop is excellent and a third of mobile is terrible* — and those have different fixes. Before forming any hypothesis, split by device class, connection type, route, and first-visit versus repeat-visit.

The segmentation almost always finds the shape: one route with an unoptimised hero image, one device class hitting a JavaScript execution wall, or cold visits paying a connection cost that warm visits never see. That is the point at which a hypothesis becomes worth writing down and testing, in the sense From Symptom to Root Cause means.

Beware the survivorship bias built into field data: users whose experience is bad enough to abandon the page may never report a metric at all. A route that "improved" after a change sometimes only lost its slowest users. Cross-check with a business metric (completion rate, bounce) before declaring victory.

The same aggregate, segmentedILLUSTRATIVE
SignalValueWhat it tells youVerdict
LCP p75, all traffic3.1sAbove target, cause unknown — not actionable on its ownsuspect
LCP p75, desktop1.4sComfortably fine; not where the problem livesnormal
LCP p75, mobile4.6sThe aggregate was an average of two different productssmoking gun
LCP p75, mobile, repeat visit2.2sWarm cache hides it; the pain is concentrated in first visitssuspect
INP p75, mobile420msInteraction is also bad — points at main-thread work, not just bytessmoking gun
Server p99, same routes78msBackend is not the constraint; stop looking therenormal

Key points

  • Backend dashboards see one line of the user's budget; TTFB is frequently well under a quarter of perceived load time.
  • Lab data detects regressions and attributes causes; field data describes reality. Running only one leaves a specific, predictable blind spot.
  • An unsegmented percentile is not actionable — split by device, connection, route and cache state before forming a hypothesis.
  • JavaScript execution cost scales with the device; network cost scales with the connection. They need different fixes.
  • Verify improvements against a business metric too: a percentile can improve because the slowest users left.

Progressive depth

Overview

The user's clock starts before your server hears about the request and stops long after it responds. Frontend performance is the accounting for everything outside that window.

Practical

Measure field LCP/INP/CLS per route segmented by device and connection; measure lab metrics per commit for regressions. Find the biggest actionable line item and attack that one, not the one you have the most tooling for.

Advanced

Separate network-bound cost (bytes, round trips, priority) from CPU-bound cost (parse, compile, execute, layout). They scale with different things — connection versus device — so the same fix does not help both, and a change can trade one for the other.

Internals

The browser is a pipeline: the HTML parser discovers resources, the preload scanner speculatively fetches ahead, render-blocking stylesheets gate the first paint, and script execution occupies the same main thread as layout and input handling. Performance work is scheduling work on that shared thread and controlling what the parser discovers, and when.

Follow the diagnosis

The causal chain, hop by hop — and the readings that invite the wrong conclusion.

  1. 1
    User → page: taps a link on a mid-range phone over a congested mobile connection; the clock the user cares about starts now.
  2. 2
    Browser → network: DNS, TCP and TLS cost ~300ms before a single application byte moves; on a repeat visit this is near zero.
  3. 3
    Server → browser: handler returns in 78ms, TTFB lands at 400ms — the backend has now spent its entire share of the budget and is finished.
  4. 4
    Browser → main thread: render-blocking CSS and a 400KB script must download, parse, compile and execute before the page is interactive.
  5. 5
    Main thread → LCP: the hero image is discovered late, fetched, decoded, and painted at 2.9s; the user has been looking at blank space for the whole interval.
What this evidence makes people conclude — wrongly
  • "Server p99 is 78ms, so the app is fast." The server is one line item; the user's clock ran for three seconds.
  • "It is fast on my machine." A developer laptop on office fibre with a warm cache is not a device class any of your users have.
  • "The aggregate p75 improved, so the change worked." Check the segments — desktop improving can hide mobile getting worse.
  • "It is a network problem, we cannot fix it." Parse and execute cost is CPU on the device and has nothing to do with the connection.
  • "Lighthouse says 95, we are done." A lab score on a synthetic profile is a regression signal, not a description of your users.

Measure, fix, validate

An optimization is not finished until the metric that motivated it has moved.

How to measure it
  • • Field: LCP, INP and CLS as p75 distributions per route, segmented by device class, effective connection type and first-vs-repeat visit.
  • • Lab: a synthetic run per commit on a pinned device and throttling profile, tracking the same metrics plus total transferred bytes and main-thread time.
  • • Navigation Timing phases (`responseStart`, `domContentLoadedEventEnd`, `loadEventEnd`) to split network from processing.
  • • Long-task count and total blocking time as the main-thread proxy for interaction readiness.
  • • Server-side p99 for the same routes, purely to rule the backend in or out before spending time on it.
What actually fixes it
  • • Find the largest actionable line item in the field-segmented budget first — usually script execution on mobile or a late-discovered LCP image, not TTFB.
  • • Remove or defer JavaScript the first screen does not need; this attacks download, parse, compile and execute simultaneously (see [[javascript-bundle-cost]]).
  • • Make the LCP resource discoverable in the initial HTML and prioritised, so its fetch does not queue behind script (see [[image-performance]]).
  • • Eliminate render-blocking work that is not needed for the first paint; inline what is critical, defer the rest (see [[browser-waterfall]]).
  • • Only then look at TTFB — and when you do, treat it as a normal backend latency investigation with the rest of this domain.
How you know it worked
  • • Compare field p75 for the affected segment (device class × route) across a window long enough to be stable — typically 7 days before against 7 days after, not the afternoon of the deploy.
  • • Confirm the lab run moved in the same direction on the same metric; if lab moved and field did not, you optimised something your users were not paying for.
  • • Check that no other metric regressed: shrinking a bundle by deferring work can push cost into INP.
  • • Cross-check a business metric over the same window so you know the percentile improved for the right reason.
What it costs
  • • Real-user monitoring is per-visit telemetry: it costs bytes on the client, has privacy implications, and adds a data pipeline to run.
  • • Aggressive deferral improves the first paint and can make interaction worse — work moved is not work removed.
  • • Per-device-class segmentation multiplies dashboards and alert rules; the cardinality has to be bounded deliberately (see [[cardinality]]).
  • • A synthetic CI run adds minutes to every build and needs a pinned environment that someone has to maintain.
Stop it coming back
  • A CI budget on transferred bytes and main-thread time per route, failing the build when a merge exceeds it.
  • A synthetic run per commit on a pinned mid-range device profile, tracked as a trend rather than a pass/fail score.
  • A field-data alert on LCP/INP p75 per device class, with enough of a window that it does not fire on daily traffic-mix noise.
  • A review checklist item for any new dependency added to the first-screen bundle: what does it cost on the slowest supported device?

Accuracy

Performance numbers are conditional. These are the conditions.

What these numbers depend on
  • WEB-SPECIFICPhase names and timing APIs are browser-platform concepts; native mobile and desktop apps have an analogous budget with different instrumentation.
  • ILLUSTRATIVEThe 2.5s budget split is invented to show the shape of the accounting. Real splits vary enormously by site, device and connection — measure your own.
  • ENVIRONMENT-SPECIFICConnection setup cost depends on protocol version, connection reuse and whether the origin is behind a CDN.

Misconceptions

Claim
“Frontend performance is a design concern, not an engineering one.”
Reality
Parse and compile time, main-thread blocking, resource priority and cache strategy are engineering decisions with measurable costs. The design brief rarely says "ship 400KB of JavaScript to render a heading".
Claim
“If the server is fast, the app is fast.”
Reality
TTFB is often under a quarter of perceived load time. A 40ms handler behind a 600ms script execution produces a slow product and a green dashboard.
Claim
“A good Lighthouse score means good performance.”
Reality
A lab score describes one synthetic device on one synthetic connection with a cold cache. It is an excellent regression detector and a poor description of your user population — which is what field data is for.

Apply it

Where the depth lives

Product analytics
Abandonment and conversion by latency bucket

A percentile improving because impatient users left is indistinguishable from a real improvement unless a business metric is measured alongside it.