PerformanceGENERALSPEC-EVOLVINGNETWORK-SPECIFICBROWSER-SPECIFIC

Loading: Why Content Arrives Late

The main content has to be discovered, requested, delivered and unblocked before it can paint. Late is usually a discovery or a blocking problem, not a byte problem.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

The main content of this page appears late — which of the steps between the request and the pixel is holding it up?

The user intent

A person followed a link because they want the thing at the other end of it. They expect to see that thing, not a spinner, a skeleton, or a header above an empty space.

The obvious build

The page is slow to appear because it is too big. Compress the images, minify the JavaScript, and it will get faster.

Why it breaks

A page can halve its total bytes and paint at exactly the same moment, because the bytes removed were never on the path to the first meaningful pixel.

How it breaks in a real browser
  • A page can halve its total bytes and paint at exactly the same moment, because the bytes removed were never on the path to the first meaningful pixel.
  • The hero image is often discovered late rather than downloaded slowly: it is referenced from a stylesheet, or from a component that only exists after the bundle has executed, so its request cannot start until several other things have finished (The Preload Scanner).
  • Marking every image loading="lazy" because it is the modern default makes the most important image on the page load later than it otherwise would.
  • A single render-blocking stylesheet on a third-party origin delays first paint by a DNS lookup, a connection and a TLS handshake before a byte of CSS moves (Render-Blocking Resources).
  • A client-rendered page adds a full round trip after the bundle executes: HTML, then JavaScript, then the data request the JavaScript makes, then the content (Client-Side Rendering).
  • The server can simply be slow to respond, in which case nothing the client does helps and every client-side optimisation is measuring the wrong half of the system (The Lifecycle of One HTTP Request in Systems).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • For the main content to appear, five gates must open in order: it must be discovered, requested, delivered, unblocked for rendering, and painted. "Slow loading" is always one of those five, and each has a different fix.
  • Discovery is a property of document structure. The preload scanner can see resources referenced in the HTML byte stream; it cannot see resources referenced from inside a stylesheet, injected by script, or chosen by a component that has not run yet.
  • Priority is assigned by the browser from context: a stylesheet in the head is critical, an image in the viewport is high, an image below the fold is low, an async script is low. fetchpriority lets you correct it when the browser guesses wrong (Resource Hints).
  • Blocking is the browser refusing to paint content it might immediately have to restyle. CSS in the document blocks rendering by design; a synchronous script blocks the parser because it might write to the document (Why a Script Tag Stops the Parser).
  • The field metric for this concern is largest contentful paint: the render time of the largest content element visible in the viewport. It is deliberately about the element a user would call "the content", not about the first pixel of anything.
  • Where the HTML comes from changes the shape entirely. Server-rendered or statically generated HTML carries content in the first response; a client-rendered shell carries a container and a promise (Choosing a Rendering Strategy).

What this makes the browser do

And which of it is avoidable.

  • Speculative parsing: while the main parser is blocked on a script, the preload scanner continues through the byte stream requesting subresources it can see. Anything it cannot see is discovered strictly later.
  • Connection setup per origin: each new origin on the critical path costs a DNS lookup, a connection and a TLS handshake, which is why a stylesheet moved to a separate host can be slower than the same stylesheet served from the document origin (CDN Delivery).
  • Decoding: an image is not painted when it arrives, it is painted when it has been decoded. Large images cost decode work that competes for a thread even when the bytes were cheap (Images and Fonts).
  • Re-work: content that arrives after first paint — a font, a late image, an injected banner — can force style, layout and paint to run again over content already on screen (Visual Stability).
  • Avoidable work in this list: everything caused by discovery order and blocking structure. Unavoidable: the round trips your dependency graph genuinely requires.

Five gates between a request and a pixel

It is tempting to treat loading as one number that goes up or down. It is more useful as five gates in sequence, because "the content is late" has five distinct causes and each one has a different fix. A team that can say "the image is late because it is discovered after the bundle executes" is most of the way to a fix; a team that says "loading is slow" is about to compress some images.

Walk them in order for the specific element a user would call the content of the page — usually the hero image, the headline, or the first block of body text.

The five gates, in order
  1. 1
    Discovered

    The browser learns the resource exists, from the HTML byte stream, from a stylesheet, or from script that has run.

    fails by Referenced from CSS, injected by a component, or chosen by a bundle that has not executed yet — so the request cannot even be queued.

  2. 2
    Requested

    The request is issued, with a priority derived from element type, position and attributes.

    fails by Queued behind higher-priority work, blocked on a new origin's connection setup, or deprioritised because it was marked lazy.

  3. 3
    Delivered

    Bytes arrive, subject to bandwidth, latency, compression and how much else is in flight.

    fails by A large uncompressed asset, an unnecessary redirect chain, or a slow server thinking before the first byte.

  4. 4
    Unblocked

    The browser is willing to render: pending render-blocking stylesheets have parsed and the parser is not stalled.

    fails by A render-blocking stylesheet on a slow third-party origin, or a synchronous script in the head that stops the parser.

  5. 5
    Painted

    Style, layout, paint and composite run and the content is on screen — after decode, for images.

    fails by The main thread is busy executing script, so the bytes are present and the frame still cannot be produced (Long Tasks).

Compression helps gate three. Nothing else in this list gets faster because a file got smaller.

The discovery staircase

The characteristic shape of a bad loading waterfall is not a long bar. It is a staircase: each request starts only after the previous one finished, because each was discovered inside the previous one. That structure is created by the page, which means it can be removed by the page.

The timeline below is schematic, in relative units. What transfers is the shape — the hero image starting late not because it is large but because nothing knew about it until the component that renders it had executed.

A client-rendered page, with the hero discovered lastrelative units — proportions, not measurements
HTML request + response
HTML parsing
CSS (render-blocking)
JS bundle
Parse + compile + execute bundle
Data request
Render + hero image request
Decode + paint hero
  • HTML request + responseA shell: a root element and a script tag. The content is not in here.
  • CSS (render-blocking)Discovered in the first chunk by the preload scanner — this part is working correctly.
  • JS bundleAlso discovered early. Large, but early discovery means it is not the staircase.
  • Parse + compile + execute bundleThe main thread. On a slow device this bar grows faster than the network bars above it.
  • Data requestCould not start earlier: the code that knows the URL had not run.
  • Render + hero image requestThird step of the staircase. The image URL came from the data.
  • Decode + paint heroThis is the moment the loading metric is measuring, and it sits four dependencies deep.

Every horizontal gap between one bar ending and the next beginning is a dependency the page created. Server-rendering the hero, or emitting a preload for it in the HTML, removes two steps from this staircase without making any file smaller.

The same hero image, discovered two different ways

This is the single highest-leverage loading change on most content pages, and it is a markup change rather than an optimisation. The point is not the attributes; it is that the first version cannot be requested until JavaScript has run, and the second is visible to the preload scanner in the first chunk of the response.

Note that the better version also reserves its own space, which stops this from becoming a visual-stability problem the moment it becomes a loading win (Visual Stability).

Discovery, not size
Discovered after execution
<div id="root"></div>
<script src="/app.js" defer></script>

// inside a component, after the bundle runs:
<img src={hero.url} loading="lazy" />
Discovered in the first chunk
<img
  src="/hero-800.avif"
  srcset="/hero-400.avif 400w, /hero-800.avif 800w"
  sizes="(max-width: 600px) 100vw, 800px"
  width="800" height="450"
  fetchpriority="high"
  decoding="async"
  alt="A wide view of the harbour at dawn" />

The preload scanner can see the second one while the parser is still blocked on something else, so its request starts several dependencies earlier. width and height reserve the box before the bytes arrive, and fetchpriority stops the browser guessing low. The first version is not a larger file; it is a later discovery, and no amount of compression fixes a request that has not been made yet.

How to build it

Most important first.

  • Fix discovery first, because it is usually free. Reference the main image from the HTML rather than from CSS or from a component, and let the preload scanner find it in the first chunk of the document.
  • Never lazy-load anything in the initial viewport, and mark the main content element as high priority so the browser does not have to guess (Responsive Images).
  • Shorten the critical path: the fewest resources that must arrive before meaningful content can paint. Split the stylesheet so the part needed for the top of the page is small, and defer the rest (The Critical Rendering Path).
  • Do not block the parser with scripts that need not run before content. defer and type="module" exist precisely for this, and the difference between them matters for execution order (`defer`, `async` and `type="module"`).
  • Move content into the first response where you can. Server rendering and streaming both attack the same problem — the content exists before any JavaScript has run (Streaming Server Rendering).
  • Reduce the number of sequential hops before the content, not just the size of each hop. Removing one dependency from the chain routinely beats halving a payload on a high-latency connection (Reading a Network Waterfall).
  • Use preconnect for an origin you will certainly need early, and preload for a resource that is critical but discovered late. Both are corrections; ten of them are contention.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Progressive rendering is announced progressively. A screen reader may begin reading a partially built page, so the order content is emitted in is an accessibility decision, not only a performance one (Semantics Are Behaviour).
  • A blank page while a bundle downloads is unnavigable by every assistive technology. Server-rendered content is in the accessibility tree at first paint, which makes the rendering-strategy choice an accessibility decision too (Client-Side Rendering).
  • Loading states need to be announced, not just drawn. A spinner with no accessible name and no live region is invisible to a screen-reader user, who experiences the wait as an unexplained silence (Live Regions and Announcement).
  • After a client-side navigation the page has changed but focus has not. Fast route transitions that leave focus on the old element are a regression for keyboard users regardless of how quickly the pixels arrived (Focus Management).
  • Content that arrives late and shifts what is already on screen is worse than slow for people using a screen magnifier or a switch device: the target moves under them (Visual Stability).

What can go wrong

Failure modes
  • Preloading everything. Priority is relative, so raising everything raises nothing, and the preloads compete with the resources they were meant to accelerate.
  • A preload for a resource that is never used, or used under a different URL, which costs bandwidth and a console warning and helps nothing.
  • Inlining critical CSS that grows over time until every HTML response carries most of the stylesheet, uncached, on every navigation.
  • A skeleton screen that makes the page look fast and pushes the moment of usefulness later, because the skeleton is now on the critical path too (Loading, Error, Empty — The States You Did Not Render).
  • Code-splitting so finely that the content depends on a chain of small chunks, each discovered only when the previous one has executed (Code Splitting).
  • Optimising first paint while the main content element stays exactly as late as it was, which improves a chart and nothing a user can perceive.
What can arrive out of order
  • Subresources arrive in an order the browser chooses from priority and availability, not in document order. Anything that needs two of them present must express that dependency rather than assume it.
  • A font and the text it styles race; which arrives first decides whether the user sees fallback text, invisible text, or neither (Images and Fonts).
  • On a client-rendered page the data response and the bundle race. Whichever loses decides when content appears, and it is not always the same one.
Security
  • Every origin added to the critical path extends the page's trust boundary to that origin, and a render-blocking resource from a third party is a dependency on their availability as well as their integrity (Third-Party Scripts and the Supply Chain).
  • Subresource integrity lets you pin the exact bytes of a third-party script or stylesheet, which converts a silent compromise into a failed load. It also converts their routine cache-busting deploy into a failed load, which is the trade.
  • A Content-Security-Policy delivered with the HTML constrains everything that follows in it, including what may be preloaded and connected to (Content Security Policy).
  • Timing information about cross-origin resources is deliberately coarse unless the other origin opts in, because precise cross-origin timing leaks information about the user's state on that origin.
Misreads
  • "The page is slow because it is big." Size matters, and blocking structure and discovery order usually matter more. A small page with a long dependency chain loses to a large page with a flat one.
  • "First paint is the goal." Painting a header and a skeleton quickly while the content stays late is a measurable improvement in one number and no improvement at all for the person waiting.
  • "Lazy-loading images is always good." Lazy-loading the main image on the page delays exactly the element the loading metric is measuring.
  • "A CDN will fix it." A CDN shortens the distance for the resources it serves. It does nothing about a resource that was discovered late, blocked, or requested only after a bundle executed (CDN Delivery).
  • "HTTP/2 means request count no longer matters." Multiplexing removes the connection-per-request cost, not the dependency structure. A request that cannot start until another finishes is still serialised (HTTP/2: Streams on One Connection in Systems).

Measuring it, and what changes in the field

How you would see this
  • The Network panel waterfall, read for the staircase: which request could not begin until another finished, and why (Reading a Network Waterfall).
  • Field data for the loading concern, segmented by route and device class, so you know whether the problem is universal or lives in one segment (Vitals in the Field).
  • The element responsible for the largest contentful paint, which most tooling will name directly. Knowing *which* element it is usually collapses the investigation.
  • Server response time as a separate signal, because a slow first byte is a backend problem wearing a frontend costume (Why Is My API Slow? in Backend Engineering).
  • A repeat-visit measurement as its own number. First visit and repeat visit are two different products with two different critical paths (Browser HTTP Caching).
Slow device, slow network, large data, old tab
  • On a high-latency connection the number of sequential dependencies dominates. Removing one hop from the chain can beat halving every payload on the page.
  • On a constrained device, decode and script execution move into the critical path: the bytes arrive and the content still does not appear, because the main thread is busy (The Real Cost of JavaScript).
  • On a repeat visit with a warm cache or a service worker, most of this sequence disappears, which is why an average across visit types hides both problems (Caching Strategies).
  • On a slow connection with a client-rendered app, the gap between "something appeared" and "the content appeared" widens dramatically, because the data request cannot start until the bundle has executed.
What this costs
  • Inlining critical CSS removes a round trip and makes that CSS uncacheable, growing every HTML response. It is a first-visit optimisation paid for on every subsequent visit.
  • Server rendering gets content into the first response and moves cost onto infrastructure you now operate, plus a hydration step with its own failure modes (Hydration).
  • Preloading raises one resource's priority at the expense of everything else in flight. It is a zero-sum instrument and should be used like one.
  • Aggressive splitting reduces the initial payload and adds requests and dependency depth. On a fast connection that is a clear win; on a high-latency one it can be a loss (Code Splitting).

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe five gates — discovery, request, delivery, unblocking, paint — follow from how the specifications define parsing, render-blocking and speculative scanning, so they hold across Blink, Gecko and WebKit even though each schedules requests slightly differently.
  • SPEC-EVOLVINGThe loading metric named here has been redefined more than once, including changes to which elements are eligible and how render time is attributed, and its published rating boundaries are maintained by the web vitals working group. Treat the concern as durable and the metric as a moving reference; check the current definition before designing against it.
  • NETWORK-SPECIFICThe advice depends heavily on protocol and latency: bundling everything into one file to reduce request count was correct on HTTP/1.1 with a small connection pool per origin, and is often counterproductive on HTTP/2 and HTTP/3 where per-request overhead is small and dependency depth is what costs you (HTTP/1.1 vs HTTP/2 vs HTTP/3 in Systems).
  • BROWSER-SPECIFICPriority heuristics and the exact behaviour of fetchpriority, preload and speculative scanning differ between engines, and some diagnostics — the element attributed to the largest paint, for instance — are surfaced by Chromium tooling far more clearly than elsewhere.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Distributed Systems — a late first byte is often a queue, a cold cache or a cross-region hop that lives nowhere near the browser, and no client-side change can recover it.