ParsingENGINE-SPECIFICNETWORK-SPECIFICBROWSER-SPECIFIC

The Preload Scanner

A second, lightweight reader runs ahead of the real parser looking for URLs to fetch — and almost every modern loading pattern accidentally hides resources from it.

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

If a script stops the parser, how does the browser keep discovering images and stylesheets further down the document — and why does that stop working in my app?

The user intent

A person is waiting for a page. The browser wants to have every byte it will need already in flight by the time it is ready to use it.

The obvious build

The browser parses top to bottom and requests each resource as it reaches it. To load resources earlier, move them higher in the document.

Why it breaks

Strictly sequential discovery would make a single blocking script catastrophic: everything below it would be undiscovered for the whole round trip. Every engine therefore runs a second, speculative scan ahead of the real parser.

How it breaks in a real browser
  • Strictly sequential discovery would make a single blocking script catastrophic: everything below it would be undiscovered for the whole round trip. Every engine therefore runs a second, speculative scan ahead of the real parser.
  • That scan only sees markup. It reads raw bytes looking for fetchable URLs in attributes; it does not build tree, run CSS matching or execute JavaScript.
  • So the moment you move a URL out of markup — data-src for a lazy-load library, a background image in CSS, a src assigned in JavaScript, a dynamic import() — the scanner cannot see it, and the resource is discovered as late as possible instead of as early as possible.
  • A client-rendered app has almost nothing in its shell for the scanner to find. The hero image, the font, the API call: all invisible until the bundle has downloaded, parsed and executed (Client-Side Rendering).
  • It is an optimisation, not a specification. Every mainstream engine has one, they do not scan identically, and none of them is a contract you can rely on for correctness.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • While the main parser is blocked — on a script, or waiting for the next chunk — a separate scanner reads ahead in the buffered byte stream that has already arrived.
  • It looks for fetchable attributes: src on script and img, href on link, srcset, poster, and <link rel="preload"> among others. What exactly is scanned differs by engine.
  • Discovered URLs are requested speculatively and land in the browser's memory cache. When the main parser reaches the same element, the response is often already there or in flight.
  • It does not execute anything. No JavaScript runs, no CSS is matched, no tree is built, so nothing it does can be observed by the page.
  • It resolves URLs against the document base, which is why a <base href> appearing after resources it affects is a real hazard: speculative resolution used a base that turned out to be wrong.
  • Firefox goes further with speculative *parsing*: it builds tree speculatively ahead of a blocked parser and discards the work if a script invalidates the speculation. The observable effect on resource discovery is similar; the internals are not.
  • CSS-referenced resources are outside its reach by construction. A font or background image inside a stylesheet is discovered after the CSS is downloaded and parsed, and a font only when style resolution matches it to an element that needs it (Images and Fonts).

What this makes the browser do

And which of it is avoidable.

  • A second pass over bytes that have already arrived, which is cheap relative to what it saves and is not on the tree-construction path.
  • Speculative fetches that may be wasted: a resource the document later removes, or a URL resolved against a base that a later <base> element changed.
  • Connection setup for origins found ahead, which is often the largest part of the saving — DNS, connect and TLS for a font or CDN origin overlapped with a script download (CDN Delivery).
  • Cache bookkeeping to match the speculative response with the later real request, so the resource is not fetched twice.
  • Avoidable waste: preloading things you do not use, and <link rel="preload"> for resources the scanner would have found anyway — that is priority spent for nothing (Resource Hints).

Two readers, one byte stream

Picture the arriving HTML as a buffer. The main parser is somewhere inside it, building tree, and it is periodically stuck — on a script that has to be fetched and executed, or simply waiting for the next chunk. The scanner reads the rest of the buffer, ignoring everything except attributes that name a URL, and starts fetching what it finds.

This is why the sequential model of loading is only half true, and why a blocking script is bad in a specific way rather than a total one: it stops tree construction, style, layout and paint, but it does not stop discovery. Understanding that split tells you which problems defer fixes and which it does not (Why a Script Tag Stops the Parser).

Speculative discovery ahead of a blocked parser
consumes in orderscans aheadstalledsrc / href / srcsetresponse waits herealready available when the parser arrivesnever reaches itBuffered HTML bytesInvisible: CSS url(), data-src, JS-set src, import()Main parser (blocked on <script>)Preload scanner (reads ahead)Network: speculative fetchesMemory cacheDOM tree — not advancing
UserLLMAgentToolDataDecisionHumanGuardrail

What it can see, and what hides from it

ENGINE-SPECIFICThe "yes" column is where engines agree in practice, not where a specification requires them to. srcset candidate selection during speculation is the clearest divergence: an engine that speculates on a different candidate than style resolution later chooses can end up fetching two images, and whether that happens depends on the engine and on whether sizes is resolvable before layout.

Every row in the table is a pattern in wide use. The left column is what the scanner reads; the right column is the round trips you pay when it cannot. The pattern is consistent: a URL in a markup attribute is early, and a URL anywhere else is late by at least one dependent fetch.

The two rows worth internalising are the CSS background image and the data-src lazy loader, because both are extremely common and both usually apply to the largest, most visible element on the page — the thing the loading measurement is actually about (Loading: Why Content Arrives Late).

PatternScanner sees it?When it is discoveredCost of being late
<img src="/hero.jpg" width height>YesImmediately, from the buffered bytesNone — and the box is sized before any bytes arrive
<link rel="stylesheet" href>YesImmediatelyNone; this is the case the scanner most reliably saves
<script src> further down the documentYesImmediately, even while the parser is blocked above itNone for the fetch; execution ordering is unchanged
<img srcset sizes>Yes, though candidate selection may differImmediatelyUsually none; an engine that picks a different candidate speculatively can fetch twice (Responsive Images)
background-image: url(...) in CSSNoAfter the stylesheet downloads and parses, and style matches the ruleAt least one extra dependent round trip, on the largest element on many pages
@font-face in CSSNoAfter CSS parse, and only when style resolution matches text needing that faceTwo dependent hops. Preload it explicitly (Images and Fonts)
<img data-src> plus a lazy-load libraryNoAfter the library downloads, parses and executesBundle round trip plus execution before the image is even requested
img.src = url in JavaScriptNoWhen that code runsEverything the script waited for, added to the image
import('./chunk.js')NoWhen the importing code executesA sequential chain; use modulepreload if the chunk is critical (Code Splitting)
A CSR shell with one script tagNo, for everything on the pageAfter bundle download, parse, execute and renderEvery resource on the page is behind one JavaScript critical path (Client-Side Rendering)
<img loading="lazy" src>Yes — the URL stays in markupSeen immediately; fetched when the browser decidesNone, and the browser chooses better thresholds than most libraries (Lazy Loading)

Writing markup a scanner can read

The fix is almost always to move a URL back into markup, which usually also improves what happens when JavaScript fails, what assistive technology sees, and whether the layout is stable. That convergence is not a coincidence: markup is the part of the page that exists before any of your code runs, and everything that reads the page early reads the same thing.

Where a URL genuinely cannot be in markup — a font inside CSS, a critical dynamic chunk — preload and modulepreload exist to declare it anyway. Use them for resources that are both critical and late-discovered, and delete the ones the console tells you were never used.

A hero image and a font, two ways
Invisible to the scanner
<head>
  <link rel="stylesheet" href="/app.css">
  <!-- font is inside app.css: two dependent round trips -->
</head>
<body>
  <!-- hero is a CSS background: discovered after CSS parse -->
  <div class="hero"></div>

  <!-- every image waits for the lazy library -->
  <img data-src="/p1.jpg" src="data:image/gif;base64,R0lGOD..." alt="">
  <script src="/lazyload.js" defer></script>
</body>
Discoverable in the first chunk
<head>
  <link rel="stylesheet" href="/app.css">
  <!-- declared, because it is critical and lives inside CSS -->
  <link rel="preload" as="font" type="font/woff2"
        href="/inter.woff2" crossorigin>
</head>
<body>
  <!-- real element, real URL, real dimensions -->
  <img src="/hero.jpg" width="1200" height="600"
       fetchpriority="high" alt="Harbour at dawn">

  <!-- native lazy loading: URL stays scannable -->
  <img src="/p1.jpg" width="400" height="300"
       loading="lazy" alt="Ferry at the quay">
</body>

Three separate wins from the same change. The hero is discovered in the first chunk instead of after a stylesheet round trip; the sized <img> reserves its box before any image bytes arrive, so nothing shifts when it lands (Visual Stability); and the real alt and real src mean the image exists for assistive technology and for a user whose JavaScript failed. The crossorigin on the font preload is not optional — font fetches are CORS-mode, and a preload without it is a second, unmatched request rather than a head start.

Preload only what is critical and late
1<!-- Good: critical, and genuinely invisible to the scanner. -->
2<link rel="preload" as="font" type="font/woff2" href="/inter.woff2" crossorigin>
3<link rel="modulepreload" href="/chunks/editor.js">
4
5<!-- Pointless: the scanner already found this one tag lines below. -->
6<link rel="preload" as="style" href="/app.css">
7<link rel="stylesheet" href="/app.css">
8
9<!-- Actively harmful: raises priority on something below the fold,
10 competing with resources the first paint depends on. -->
11<link rel="preload" as="image" href="/footer-illustration.png">
12
13<!-- Different feature entirely: a hint about the NEXT navigation,
14 at low priority. Not a faster version of preload. -->
15<link rel="prefetch" href="/settings">

The test for a preload is not "is this important" but "is this important AND would the scanner miss it". Chrome's console warning about preloaded-but-unused resources is the fastest way to find the ones to delete (Resource Hints).

How to build it

Most important first.

  • Put real URLs in real attributes. <img src> with width, height and srcset is scannable, cacheable and sizeable before a single byte of the image arrives (Responsive Images).
  • Use native loading="lazy" instead of a data-src library. The browser gets to decide when to fetch, and the URL stays visible to the scanner (Lazy Loading).
  • Reference fonts with <link rel="preload" as="font" crossorigin> in the head. Fonts are the canonical late-discovered resource, sitting two levels deep behind CSS download and style matching.
  • Keep the critical stylesheet and the critical script in the initial HTML, in the head, as markup. Everything the scanner can see in the first chunk is a round trip you do not spend later.
  • Server-render, or at least server-emit, the markup for above-the-fold content. The scanner's effectiveness is a direct argument for HTML that contains the page (Server-Side Rendering).
  • Use fetchpriority to correct a priority you disagree with, sparingly. It is a relative hint against everything else in flight, so raising everything raises nothing.
  • Never let a <base> element appear after the resources it would affect.

Keyboard, focus, semantics, announcement

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

  • Late-discovered images arrive after layout has settled, and if they were not sized they push content down under someone mid-sentence. Reserving space is the fix, and width/height on a scannable <img> gives it for free (Visual Stability).
  • Late fonts produce either invisible text or a swap that reflows. Both are worse for readers with low vision or cognitive load, who are re-finding their place each time (Contrast, Colour and Motion).
  • The data-src pattern usually implies a JavaScript-dependent image pipeline. If the script fails, there is no image and often no alt text either, because the placeholder carried neither (Images, Video and the Elements That Own Their Layout).
  • A scannable, server-rendered document is available to assistive technology at first paint. That is the same property that makes it scannable, and it is a reason to prefer markup over script-constructed DOM that goes well beyond loading speed.
  • Native loading="lazy" keeps alt and the real src in the markup, so an image that never enters the viewport is still a properly-described element in the accessibility tree (The Accessibility Tree).

What can go wrong

Failure modes
  • The lazy-load library pattern: <img data-src="..."> with a real src of a transparent placeholder. Every image in the document is invisible to the scanner, and discovery waits on the library.
  • A hero image set as a CSS background. It cannot be discovered until the CSS has downloaded and parsed, and it is frequently the largest element on the page (Loading: Why Content Arrives Late).
  • A CSR shell: the scanner sees one script tag and a <div id="root">. Every other resource on the page is behind bundle download, parse and execution (Client-Side Rendering).
  • Fonts referenced from CSS with no preload — two dependent round trips before a single glyph exists.
  • A <base href> injected by a script or emitted late, invalidating speculative URL resolution for everything above it.
  • The mitigation failing: preloading a dozen resources so the important one is "guaranteed early". Preload is a priority statement, and a page where everything is high priority has no priorities (Resource Hints).
What can arrive out of order
  • The scanner and the main parser race over the same document, and which one reaches an element first decides whether the request was speculative or parser-initiated. The page cannot observe the difference; the waterfall can.
  • A speculative fetch races a script that mutates or removes the element it came from, producing a request for a resource that is never used.
  • Speculative resolution races a late <base href>: the request may already be in flight against the old base when the new one arrives.
  • Two code paths — a preload and the real element — race for the same URL. Normally the browser matches them into one fetch, but a mismatch in as, crossorigin or type causes two downloads instead of one.
Security
  • Speculative requests are real requests. They carry cookies according to the usual rules, they appear in server logs, and they reach third-party origins before the user has interacted with anything (Cookies).
  • They are subject to CSP and mixed-content blocking like any other fetch, so the scanner cannot be used to bypass a policy. It can, however, make a policy violation appear earlier than expected in the console (Content Security Policy).
  • Speculation leaks intent. A preconnect or speculative fetch to an analytics or advertising origin discloses a visit even if the resource is never used — worth knowing before adding hints for third parties (Third-Party Scripts and the Supply Chain).
  • Because the scanner does not execute anything, it cannot be an XSS vector on its own. The markup it scans can absolutely be an XSS vector when the main parser reaches it (Cross-Site Scripting).
  • A speculative fetch for a URL built from user-controlled markup is a request an attacker can cause the browser to make. It is the same class of concern as any attacker-controlled src, arriving slightly earlier.
Misreads
  • "The preload scanner means script placement does not matter." It recovers *discovery*. Tree construction, style, layout and paint are all still blocked (Why a Script Tag Stops the Parser).
  • "<link rel=preload> makes something load faster." It makes it be discovered and prioritised earlier. If the scanner already found it, the preload adds contention and no speed.
  • "Preload and prefetch are the same." Preload is for this navigation, at high priority; prefetch is a speculative hint for a likely *next* navigation, at low priority (Resource Hints).
  • "Lazy loading always helps." It helps for content below the fold. Applied to the hero image it delays the largest element on the page (Loading: Why Content Arrives Late).
  • "The scanner is part of the HTML spec." It is an implementation optimisation present in every engine and specified by none of them. Firefox's speculative parser does noticeably more than a pure URL scan.

Measuring it, and what changes in the field

How you would see this
  • The Network panel initiator column is the direct evidence. A request initiated by the parser or listed as "preload"/"speculative" was found by the scanner; one initiated by a script was not (Reading a Network Waterfall).
  • Look for the staircase. A resource whose bar starts only after another finishes was discovered by that other resource — the definition of late discovery (Reading the Browser Waterfall in Observability).
  • Chrome warns in the console about preloaded resources that were not used within a short window, which is the cheapest way to find preloads you should delete.
  • The largest contentful element and its discovery time tell you whether your hero resource is scannable. If it is a CSS background or a script-set src, it is not (Vitals in the Field).
  • Compare a run with JavaScript disabled: whatever still loads is roughly what the scanner can see.
Slow device, slow network, large data, old tab
  • On a high-latency connection the scanner is worth the most, because it overlaps round trips that would otherwise be sequential. On a fast local connection its effect is nearly invisible.
  • On a slow device it matters differently: script execution takes longer, so the window during which the scanner runs ahead of a blocked parser is larger, and so is the saving.
  • On a repeat visit with a warm cache, speculative fetches are cache hits and the benefit mostly disappears — another reason first-visit and repeat-visit performance are separate problems (Browser HTTP Caching).
  • Behind a service worker, speculative requests still go through the worker's fetch handler, so a worker that does expensive work per request can erase the advantage (Intercepting Fetch).
  • In a client-rendered SPA after the first navigation there is no HTML parse at all, so none of this applies to subsequent routes — discovery there is entirely a routing and prefetch question (Route Loading Boundaries).
What this costs
  • Writing markup the scanner can read constrains how dynamic your HTML can be. A truly personalised hero image has to come from somewhere, and "somewhere" is usually script.
  • Native lazy loading gives up fine control over thresholds and placeholders in exchange for scannability and a browser that has more information than you do.
  • Preload converts a discovery problem into a priority problem. It is genuinely useful and genuinely easy to overuse, and an unused preload is pure waste — downloaded bytes, contention, nothing gained.
  • Server-rendering above-the-fold markup so the scanner can see it means running a server and owning its latency (Server-Side Rendering).

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.

  • ENGINE-SPECIFICThe preload scanner is an implementation optimisation, not a specified behaviour. Blink and WebKit run a lightweight URL scanner over buffered bytes; Firefox runs a speculative parser that builds tree ahead of the blocked parser and discards it on invalidation. The set of attributes each scans, and how each handles a late <base>, differ — so treat scannability as a strong optimisation and never as a correctness guarantee.
  • NETWORK-SPECIFICThe benefit is proportional to round-trip time, because what the scanner buys is overlap. On a high-latency mobile connection it can move a font or hero image several round trips earlier; on a warm local connection to a fast origin the same page shows almost no difference, which is why late-discovery regressions survive local testing.
  • BROWSER-SPECIFICDiagnosis is uneven. Chrome labels speculative and preload initiators in the Network panel and warns about unused preloads in the console; Firefox and Safari expose less, so the same investigation takes longer or has to be done by comparing waterfalls with and without JavaScript.

Where the depth lives

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

Domains that do not exist yet
  • Compilers & Programming Languages — speculative execution with rollback is the same shape as an optimising compiler emitting a fast path guarded by a check that can deoptimise. Firefox's speculative parser makes the analogy exact: it builds tree it may have to throw away.
  • Testing & Reliability Engineering — scannability is a property that regresses silently, because nothing fails. Asserting on it needs a test that inspects the initiator of a request rather than the eventual state of the page.