DeliveryGENERALNETWORK-SPECIFICSIMPLIFIED

The Critical Rendering Path

The set of resources that must arrive and be processed before the browser can paint meaningful content — a dependency graph, not a byte count.

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

Which resources must arrive before the browser can paint anything meaningful, and what put them on that list?

The user intent

A person taps a link expecting the thing they came for. Not a spinner, not a white rectangle, not a skeleton that is still a skeleton three seconds later.

The obvious build

Making a page load faster means making the page smaller. Compress the images, minify the JavaScript, drop a dependency, and the first paint arrives sooner.

Why it breaks

You delete a 400 KB hero image that sits below the fold and first paint does not move by a hair. It was never on the path — nothing was waiting for it.

How it breaks in a real browser
  • You delete a 400 KB hero image that sits below the fold and first paint does not move by a hair. It was never on the path — nothing was waiting for it.
  • You add a 3 KB stylesheet from a font provider and first paint gets dramatically later, because that tiny file needs its own DNS lookup, its own connection and its own TLS negotiation before its first byte exists.
  • Two pages ship identical bytes. One paints in a single round trip; the other takes four, because its font is referenced from a stylesheet that is referenced from a stylesheet that is referenced from the HTML. Bytes are equal; depth is not.
  • A build that "reduced the bundle by 40%" produces no measurable change in the field, because the bundle was already deferred and the page was blocked on CSS the whole time (Render-Blocking Resources).
  • "Make it smaller" gives an engineer nothing to do on Monday. "The font is discovered two hops deep and blocks the text it styles" gives them a specific edit.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • The critical rendering path is the longest chain of dependent steps between the navigation request and the first paint of meaningful content. It is a graph property, not a size property.
  • The HTML document is always on it. Nothing else can even be discovered until bytes of it arrive and the parser tokenizes them (Streaming HTML).
  • CSS is on it by default. The browser refuses to paint content it might immediately have to restyle, so the CSSOM must be complete for every render-blocking stylesheet before the first frame (The CSSOM).
  • Synchronous scripts are on it, but for a different reason: they stop the parser rather than the painter, so the tree the painter needs never finishes being built (Why a Script Tag Stops the Parser).
  • Discovery is a hop. A resource referenced from inside another resource cannot start until that other resource has arrived and been parsed. A font in a stylesheet is two hops from the document; a chunk requested by application code is three or more.
  • The preload scanner shortens discovery by looking ahead in the HTML byte stream while the main parser is blocked — but it can only see markup, so it cannot see anything referenced from inside CSS or JavaScript (The Preload Scanner).
  • A path therefore has three separate measurements: its depth in dependent round trips, its weight in bytes that must transfer, and its count of distinct resources and origins. They respond to completely different fixes.

What this makes the browser do

And which of it is avoidable.

  • Streaming and tokenizing HTML as it arrives, dispatching subresource requests the moment each reference is seen.
  • Fetching and parsing every render-blocking stylesheet into the CSSOM, then running the cascade to produce a computed value per property per element (Style Calculation).
  • Executing any parser-blocking script before it can continue building the tree — including compiling it, which is main-thread work that scales with source size (The Real Cost of JavaScript).
  • Running layout and paint over the tree that exists so far, then compositing the first frame. Genuinely unavoidable; everything above it is negotiable.
  • Re-doing style, layout and paint when a late stylesheet, font or dimensionless image arrives after content is already on screen (Visual Stability).

What is actually on the path

Draw it once and the whole module becomes legible. The solid chain below is the critical path: every node in it must complete before the first meaningful paint. The dashed arrivals are on the same page, cost the same bytes, and are not on the path at all.

Notice where the font sits. It is small, it is important, and it is two hops from the document — the parser cannot see it, because it is named inside a stylesheet that has not arrived yet. That is what "depth" means, and it is the single most common reason a page paints later than its byte count suggests.

Critical path (solid) versus everything else
first chunkdiscovered in headparser stops herehop 2: discovered inside CSSor text swaps laternot blockingnot blockingnot blockingNavigation requestHTML (streams)Parser + preload scannerapp.css (render-blocking)Synchronous script in headBelow-fold imageDeferred bundleCSSOM completeFont (named inside app.css)Analytics beaconStyle + layoutFirst meaningful paint
UserLLMAgentToolDataDecisionHumanGuardrail

Depth, weight and count are three different problems

Every critical path has all three properties, and an engineer who only measures one of them will make changes that do nothing. The point of separating them is that each responds to a different edit, and the one that dominates depends on the network the user is actually on.

The habit worth building: when someone says "the page is slow", ask which of these three they mean. Most disagreements about loading performance are two people optimising different columns.

PropertyWhat it measuresWhat it costsDominates whenHow you shorten it
DepthDependent hops: how many things must arrive before the *next* thing can even be requestedA full round trip each, regardless of file sizeLatency is high — mobile networks, distant origins, anything cellularMove references up into the HTML, or announce them early with a hint (Resource Hints)
WeightBytes that must transfer before the first paint can happenTransfer time, plus decompression and parse on the deviceBandwidth is low, or the device is slow enough that parsing dominatesCompression, splitting by route, and deleting rules nobody uses (Minification Is Not Compression)
CountNumber of distinct path-critical resourcesWas severe on HTTP/1.1; modest on multiplexed connectionsThe connection is HTTP/1.1, or a proxy downgrades itBundle — but check the protocol first, because this advice inverts (Bundlers Compared)
OriginsDistinct hosts that must be reached before the first paintDNS, connection and TLS per origin, all before byte oneAlways, on a cold visit — this is the most under-counted cost on the listConsolidate, self-host, or preconnect the ones you truly cannot move

Getting the first screen's CSS onto the first frame

NETWORK-SPECIFICInlining wins where the round trip is expensive and loses where it is cheap: on a warm HTTP/2 connection to a nearby edge, the saved trip is small and the repeated uncacheable bytes are pure loss, while on a high-latency cellular link the same trade is strongly positive.

This is the decision the critical path forces on almost every project, and there is no default answer. The criteria are the lesson: how much CSS the first screen needs, how much it changes, how much of your traffic is returning, and whether latency or bandwidth is the binding constraint.

What all four options share is honesty about the trade. Any technique that makes a stylesheet stop blocking has decided that showing the user unstyled or differently-styled content is acceptable. Sometimes it is. Say so out loud rather than discovering it in a bug report about flashing text.

How should the CSS the first screen needs reach the browser?

What is the shortest honest route from the navigation request to correctly styled content?

One render-blocking stylesheet

when The stylesheet is small, on the same origin, and the site is mostly returning visitors who already have it cached

cost One round trip on every cold visit, paid before anything paints. Simple, cacheable, and completely predictable.

Inline the first screen's rules, load the rest without blocking

when Cold-visit paint is the metric that matters and latency is the constraint

cost Uncacheable CSS in every HTML response, a build step that can drift from the design, and a Content-Security-Policy nonce or hash requirement (Content Security Policy).

Split the stylesheet per route

when The application has genuinely distinct areas and a shared sheet would ship most of itself unused to every page

cost More files, a build that must know the route graph, and a new bug class where a route's rules were assigned to the wrong chunk (Code Splitting).

Non-blocking load with a `media` swap

when The stylesheet only affects content the user cannot see yet — print styles, a below-fold widget, a modal's theme

cost If you are wrong about what it affects, the user watches the page restyle itself. Correct for genuinely non-critical CSS; a visible regression for anything else.

How to build it

Most important first.

  • Draw the graph before you optimise anything. List every resource the browser must have parsed before the first meaningful paint, and draw an edge from each one to the resource that revealed it. The longest chain in that drawing is what you are actually fighting.
  • Reduce depth first. Moving a resource from "referenced inside CSS" to "referenced in the HTML head" removes a full round trip on every cold visit, and no amount of compression buys that back (Resource Hints).
  • Get the document itself out early and let it stream, so the parser can start discovering subresources while the server is still producing the rest (Streaming Server Rendering).
  • Take scripts off the path unless they must run before content exists. defer and type="module" mean "you may keep parsing" (`defer`, `async` and `type="module"`).
  • Keep path-critical resources on the origin that is already connected. Every additional origin on the critical path is a fresh DNS lookup, connection and handshake before its first byte.
  • Inline the small amount of CSS the first screen genuinely needs and load the rest without blocking — but only once you have measured that the round trip is the problem, because this trade is expensive in other ways.

Keyboard, focus, semantics, announcement

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

  • Content that renders progressively is announced progressively, so the order in which you emit markup is the order a screen-reader user receives the page. Emit the main content before decorative shells, not after.
  • A blank document while critical resources load is unnavigable by every assistive technology — there is no landmark to jump to, no heading to list, nothing to focus. This is one of the strongest accessibility arguments for server-rendered content (Client-Side Rendering).
  • A skeleton is a purely visual signal. If the region is going to be replaced, mark it as busy and give it an accessible name, or a screen-reader user is told nothing is happening at all (Live Regions and Announcement).
  • Late-arriving CSS and fonts reflow text that has already been painted, which moves a switch or eye-tracking user's target out from under them and moves a magnifier user's reading position without warning (Visual Stability).
  • If the page navigates or swaps its content once resources land, focus must be placed deliberately. A keyboard user left on a stale element has no route into the new content (Focus Management).

What can go wrong

Failure modes
  • Inlined critical CSS that drifts. The extraction was correct for the design six months ago; now every HTML response carries dead rules and the full stylesheet still blocks the first paint anyway.
  • Loading the main stylesheet with a media swap so it stops blocking, then shipping a page that paints unstyled content and restyles it a moment later. The user sees the page twice.
  • A self-hosted font on a bucket with no edge presence: one small file, one cold origin, and every heading on the page waiting for it (Images and Fonts).
  • Optimising the path until first paint is immediate and the page is a skeleton that cannot be used for another several seconds. First paint improved; the experience did not (Interaction Responsiveness).
  • A redirect in front of the document. Every hop is a complete round trip before a single byte of HTML exists, and it is invisible in any measurement that starts at the final URL.
What can arrive out of order
  • A font and the text it styles race. Which arrives first decides whether the user sees fallback text, invisible text, or neither, and the browser's font-display policy decides which race the page is running.
  • Two render-blocking stylesheets race; the first paint waits for whichever is slower, so the path length is set by the worst one rather than the average.
  • The preload scanner and the main parser race ahead of each other, so subresource requests do not leave in document order and cannot be assumed to.
Security
  • Every origin on the critical path is an extension of the page's trust boundary. A third-party stylesheet can load fonts and images, read nothing, but stall your first paint indefinitely (Third-Party Scripts and the Supply Chain).
  • A strict Content-Security-Policy interacts directly with critical-path work: inlined CSS needs a nonce or a hash, and getting that wrong turns a performance optimisation into an unstyled page (Content Security Policy).
  • Subresource integrity lets you pin a third-party file to a hash, at the cost of a hard failure when the provider changes the file. That is usually the correct trade for something on the critical path.
  • The browser will block or refuse to upgrade insecure subresources in a secure document, so a mixed-content mistake presents as a missing stylesheet rather than as a warning anybody reads.
Misreads
  • "The critical path is whatever is above the fold." It is a dependency graph. A stylesheet whose rules only apply to the footer still blocks the first paint if it is render-blocking.
  • "We removed 300 KB, so the page is faster." Only if those bytes were on the path and only if bandwidth, not latency, was the constraint.
  • "Fewer requests is always better." That was HTTP/1.1 advice about a connection limit. On HTTP/2 and HTTP/3 the per-request cost is far lower, and one giant bundle is often worse than several (Bundlers Compared).
  • "Async everything." A stylesheet that stops blocking does not stop being needed; it just stops being waited for, which is a different and sometimes worse user experience.

Measuring it, and what changes in the field

How you would see this
  • The Network panel's initiator column is the graph. It tells you *what requested this*, which is exactly the edge you need to shorten (Debugging the Network).
  • Sort by priority rather than by time: the browser's own priority assignment is its opinion about what is on the critical path, and disagreements between its opinion and yours are the bug.
  • A request-chain view in an audit tool renders the longest dependent chain directly, which is the one number this lesson is about (Measure Before Optimising).
  • Field data, not your laptop. Discovery depth costs round trips, and round trips are the thing your development machine has almost none of (Vitals in the Field).
Slow device, slow network, large data, old tab
  • On a high-latency link, depth dominates completely. Removing one dependent hop can beat halving every file on the page.
  • On a low-bandwidth link, weight dominates and the ordering matters less — but the ordering still decides which of the bytes arrive first.
  • On a slow device, parse and compile time dominate and the same waterfall produces an entirely different profile: the network finishes early and the main thread is the queue.
  • On a repeat visit, most of the path can disappear into the HTTP cache, which is why first-visit and repeat-visit loading are two separate problems with two separate fixes (Browser HTTP Caching).
What this costs
  • Inlining critical CSS removes a round trip and makes that CSS uncacheable, growing every single HTML response including the ones from returning users who already had it.
  • Splitting CSS per route shortens each path but multiplies build complexity and creates a new failure mode: a route whose CSS was assigned to the wrong bundle (Code Splitting).
  • Consolidating onto one origin removes handshakes but gives up the parallelism and edge presence a separate asset host may provide (CDN Delivery).
  • Every optimisation here is a first-visit optimisation. If most of your traffic is returning users with a warm cache, the return is much smaller than the effort suggests.

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 dependency structure follows from the HTML and CSS specifications rather than from any implementation, so the graph is the same in every mainstream engine. What differs between engines is the heuristics they use to prioritise within that graph, and how aggressively they speculate ahead of it.
  • NETWORK-SPECIFICOn HTTP/1.1 the per-origin connection limit made request count a first-class cost, and bundling everything into one file was correct. On HTTP/2 and HTTP/3 requests are multiplexed over one connection, so splitting into many cacheable files usually wins instead — the same advice inverts on the protocol (HTTP/1.1 vs HTTP/2 vs HTTP/3 in Networking).
  • SIMPLIFIEDReal engines overlap these stages heavily: parsing continues past a blocked script for discovery only, layout can be partial, and paint is skipped for offscreen content. The linear chain here predicts blocking correctly while understating how much the browser does concurrently.

Where the depth lives

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

Domains that do not exist yet
  • Software Design — "the critical path is a dependency graph" is the same reasoning as a build graph or a module graph; the structural instinct transfers even though the units do not.