Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Resource Loading Pipeline

Concept

Vocabulary that names a phenomenon.

Chromium’s two-tier scheduling path (Blink’s ResourceLoadScheduler in the renderer plus the network service’s ResourceScheduler in the network process) that decides when and in what order a page’s subresources are requested, distinct from the Rendering Pipeline that turns the fetched bytes into pixels.

Scope

The Rendering Pipeline deliberately stops at the boundary this concept names: “Network fetches happen upstream of Parse and are governed by the network stack, the resource fetcher, and the priority hints API; they are not stages of the rendering pipeline.” The Resource Loading Pipeline is that upstream subsystem. It governs requesting bytes; rendering governs consuming them.

What It Is

A page is rarely a single document. A typical page is one HTML response plus dozens or hundreds of subresources: stylesheets, scripts, fonts, images, and the data those scripts go on to fetch. The order and timing of those requests determine how fast the page becomes usable, and that ordering is not left to the order the parser happens to encounter the tags. Two schedulers shape it.

Tier one runs inside the renderer process. Blink’s ResourceLoadScheduler (third_party/blink/renderer/platform/loader/fetch/resource_load_scheduler.cc) holds requests the renderer has discovered and releases them according to per-resource priority and throttling state. Its most consequential input arrives before the parser does any work: the renderer’s preload scanner tokenizes the incoming HTML on a background thread, races ahead of the main HTML parser, and starts fetching the subresources it finds (stylesheets, scripts, hero images) while the main thread is still busy. A <script> near the top of the document blocks the main parser, but the preload scanner has already seen the <img> and <link> tags further down and requested them. Subresource discovery is decoupled from subresource parsing on purpose.

Tier two runs in the network process. The network service’s ResourceScheduler (services/network/resource_scheduler/resource_scheduler.cc) receives requests from every renderer and orders them across the connection limits a real network imposes. It separates delayable requests (low-priority subresources that can wait) from non-delayable ones (the document, scripts, and other high-priority resources), keeps a priority-ordered queue, and caps how many delayable requests may be in flight at once so they do not crowd out critical resources or saturate a single host’s connections. The renderer proposes a priority; the network process is where that priority is enforced against the rest of the page’s traffic and the current network conditions.

Between the two tiers sits the priority model. Every request carries a priority (Chromium maps resource kinds to a small set of net::RequestPriority levels), and a developer can nudge that priority with the fetchpriority attribute (the Priority Hints API, shipped to stable in Chrome 101). fetchpriority="high" asks the browser to treat a resource as more important than its kind would default to; fetchpriority="low" asks the opposite. The word asks is load-bearing: fetchpriority is a hint, not a directive. The browser’s own heuristics (resource type, document position, viewport visibility) remain in force, and they can override the hint when they disagree with it.

Why It Matters

Performance debugging for a page that loads slowly almost never lands in Parse, Style, or Layout. It lands here. A preloaded asset still arrives late; a fetchpriority="high" hint does not move the resource to the front the way the developer expected; a third-party tag at the bottom of the page starves the connection budget the hero image needed. Each of those symptoms is a scheduling outcome, and none of them is visible from the Rendering Pipeline’s vocabulary. The stage names that diagnose “the page is slow to render” do not diagnose “the page is slow to request the right bytes first.” The two pipelines fail in different places and need different names.

The two-tier structure also answers a question that recurs whenever someone first traces a fetch: why is resource loading centralized in the browser and network processes at all, rather than each renderer fetching its own bytes directly? Chromium’s “Multi-process Resource Loading” design document gives the rationale. A single networking authority keeps the session state, the cookie store, and the cache coherent across every renderer, and it enforces the per-host connection limits that a fleet of independent renderers could not coordinate among themselves. The cost of that centralization is the cross-process hop on every request and the second scheduling tier that lives in the network process. The benefit is consistency and a single place to enforce global limits. This is the same tradeoff the Multi-Process Architecture makes everywhere: capability is concentrated in the more-privileged process, and the renderer pays a round trip to reach it.

For the RAIL Performance Model, this pipeline is where the Load budget is spent or saved. RAIL allocates a time budget to getting a page interactive; whether that budget is met depends on whether the resources on the critical path were requested early and dispatched first. Getting the priority order right is the lever that the Load budget rewards.

How to Recognize It

The pipeline is observable from surfaces a developer at a running browser already has.

The DevTools Network panel exposes the priority directly. Right-clicking the column header adds a Priority column that labels each request Highest, High, Medium, Low, or Lowest: the renderer-side priority the request was assigned. The waterfall view next to it shows when each request was actually dispatched relative to the others, which is the scheduling decision the two tiers produced. A resource that shows High priority but a late start time is the signature of connection contention or a queue cap, not a missing preload.

The preload scanner shows up as requests that begin before the main parser could plausibly have reached them: an image far down the document fetched while a blocking script near the top is still executing. A request with an Initiator of Parser that starts suspiciously early was found by the scanner, not the main parser.

The network process’s queue caps are constants in the source. kMaxNumDelayableRequestsPerHostPerClient bounds how many delayable (low-priority) requests may be outstanding to one host for one client at a time; kDelayablePriorityThreshold sets the priority level at or below which a request is treated as delayable. These are version-specific tuning constants, not eternal architecture: read them at a pinned commit rather than memorizing a number, because they are exactly the kind of value the network team revisits as protocols and connection models change.

A fetchpriority hint that did not take effect is recognizable too. If a resource marked fetchpriority="high" still loads at Medium priority in the Network panel, the browser’s heuristics overrode the hint, which confirms that the attribute is advisory. The reverse also happens: a hint the heuristics agree with is reflected immediately in the priority column.

How It Plays Out

Three scenarios show the pipeline’s diagnostic value.

A team ships a marketing page whose largest contentful element is a hero image, and the image arrives late despite a <link rel="preload"> tag in the head. A network trace shows the preload request was issued early, exactly as intended, but it sat behind a dozen delayable analytics and font requests that saturated the connection budget to the same host. The diagnosis isn’t “the preload didn’t work”; the preload worked, and the request was made early. The problem is contention at tier two: too many delayable requests in flight crowded the critical image. The fix is to mark the hero image fetchpriority="high" so it’s non-delayable and to defer the analytics tags so they stop competing. Both moves operate on the scheduler, not on the markup order.

A team adds fetchpriority="high" to a below-the-fold image expecting it to load sooner, and nothing changes. The trace shows the image still loading at its default low priority. The browser’s viewport heuristic classified the image as off-screen and declined the hint. The diagnosis is the hint-not-directive caveat in action: fetchpriority competes with the browser’s own signals and doesn’t always win. The honest fix is to stop fighting the heuristic (the image is below the fold, and loading it late is correct) rather than to escalate the hint.

A team building an enterprise dashboard reports that the first meaningful paint is gated on a script the parser reaches only halfway down a large HTML document. The expectation is that the preload scanner should have found it early. A trace shows the scanner did find it, but the script was emitted by a server-rendered template inside a <template> element the scanner does not descend into, so it was discovered by the main parser instead, late. The diagnosis is at tier one: the preload scanner’s reach has limits, and markup that hides a critical resource from it forfeits the early-fetch benefit. The fix is to surface the script with an explicit <link rel="preload"> in the head, which the scanner does see.

Consequences

Naming the two tiers and the priority model buys several operational properties.

Loading problems become attributable to a tier. A late resource is either a renderer-side discovery problem (tier one: the preload scanner never saw it, or it was discovered late by the main parser) or a network-side dispatch problem (tier two: it was queued behind higher-priority traffic or capped by a delayable limit). The two have different remedies, and naming which tier failed is the prerequisite for choosing between them.

The hint-not-directive boundary becomes explicit. fetchpriority is a way to inform the scheduler, not to command it, and treating it as a command produces the recurring surprise of a hint that did not take. Holding the distinction means reading a non-effecting hint as the browser’s heuristics disagreeing, not as a bug, and deciding whether the disagreement is correct before escalating.

The centralization cost becomes legible. Every subresource request crosses from the renderer to the network process and back, and the per-host connection limits are enforced globally rather than per-renderer. A page that opens many connections to many hosts pays differently than a page that concentrates its traffic, and the connection-limit caps are why. The cost is the cross-process hop and the second scheduling tier; the benefit is session, cookie, and cache consistency across every renderer the browser runs.

The pipeline also names what it doesn’t include. It requests bytes; it doesn’t parse them, because that’s where the Rendering Pipeline begins. It doesn’t decide cache validity or connection reuse at the protocol level; that’s the network stack proper, downstream of the scheduler. Its job is when and in what order to ask; everything about how the bytes travel once asked sits in the adjacent networking subsystems.

Notes for Agent Context

An AI coding agent producing front-end code for a Chromium target treats resource loading as a scheduled, two-tier system, not as a fetch-in-source-order guarantee. Mark genuinely critical, render-blocking resources fetchpriority="high" and de-prioritize non-critical ones (fetchpriority="low" on analytics, late-loaded widgets, below-the-fold images), but never assume the hint is honored: fetchpriority is advisory, and the browser’s own heuristics (resource type, document position, viewport visibility) can override it. Verify the resulting priority in the DevTools Network panel’s Priority column rather than trusting the attribute.

Place critical subresources where the preload scanner can find them (a <link rel="preload"> in the document head, or an early <link>/<script>/<img> in the static markup), and do not bury a critical resource inside a <template>, a JavaScript-injected tag, or a deeply nested structure the scanner does not reach, because a resource the scanner cannot see is fetched late by the main parser. Do not open many parallel low-priority requests to a single host: delayable requests are capped per host per client in the network process, so a flood of low-priority traffic delays itself and can crowd out a critical resource sharing that host.

Sources

The authoritative description of why resource loading is centralized in the browser and network processes is the Chromium project’s “Multi-process Resource Loading” design document on chromium.org, which traces a request from Blink’s ResourceLoader through the IPC boundary to the browser/network process and states the rationale: a single networking authority for session, cookie, and cache consistency and for global connection limits. The network-process scheduler’s behavior (delayable versus non-delayable requests, the priority-ordered queue, the per-host and per-client in-flight caps) is recorded in the network service source itself, services/network/resource_scheduler/resource_scheduler.cc, which is the primary record for the tuning constants and the only place they are guaranteed current. The renderer-side scheduler lives in third_party/blink/renderer/platform/loader/fetch/resource_load_scheduler.cc. For the Priority Hints API, the web.dev “Optimize resource loading with the Fetch Priority API” article documents the fetchpriority semantics and the hint-not-directive caveat, and the Chrome for Developers “New in Chrome 101” post records the version at which Priority Hints reached stable (origin trial from Chrome 96). Addy Osmani’s “Preload, Prefetch and Priorities in Chrome” is the practitioner-facing treatment of the preload scanner and Chrome’s resource-priority heuristics.

Technical Drill-Down