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.
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?
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 browser parses top to bottom and requests each resource as it reaches it. To load resources earlier, move them higher in the document.
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.
- 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-srcfor a lazy-load library, a background image in CSS, asrcassigned in JavaScript, a dynamicimport()— 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.
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:
srconscriptandimg,hrefonlink,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).
What it can see, and what hides from it
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).
| Pattern | Scanner sees it? | When it is discovered | Cost of being late |
|---|---|---|---|
<img src="/hero.jpg" width height> | Yes | Immediately, from the buffered bytes | None — and the box is sized before any bytes arrive |
<link rel="stylesheet" href> | Yes | Immediately | None; this is the case the scanner most reliably saves |
<script src> further down the document | Yes | Immediately, even while the parser is blocked above it | None for the fetch; execution ordering is unchanged |
<img srcset sizes> | Yes, though candidate selection may differ | Immediately | Usually none; an engine that picks a different candidate speculatively can fetch twice (Responsive Images) |
background-image: url(...) in CSS | No | After the stylesheet downloads and parses, and style matches the rule | At least one extra dependent round trip, on the largest element on many pages |
@font-face in CSS | No | After CSS parse, and only when style resolution matches text needing that face | Two dependent hops. Preload it explicitly (Images and Fonts) |
<img data-src> plus a lazy-load library | No | After the library downloads, parses and executes | Bundle round trip plus execution before the image is even requested |
img.src = url in JavaScript | No | When that code runs | Everything the script waited for, added to the image |
import('./chunk.js') | No | When the importing code executes | A sequential chain; use modulepreload if the chunk is critical (Code Splitting) |
| A CSR shell with one script tag | No, for everything on the page | After bundle download, parse, execute and render | Every resource on the page is behind one JavaScript critical path (Client-Side Rendering) |
<img loading="lazy" src> | Yes — the URL stays in markup | Seen immediately; fetched when the browser decides | None, 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.
<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>
<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.
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>withwidth,heightandsrcsetis scannable, cacheable and sizeable before a single byte of the image arrives (Responsive Images). - Use native
loading="lazy"instead of adata-srclibrary. 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
fetchpriorityto 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/heighton 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-srcpattern usually implies a JavaScript-dependent image pipeline. If the script fails, there is no image and often noalttext 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"keepsaltand the realsrcin 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
- The lazy-load library pattern:
<img data-src="...">with a realsrcof 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).
- 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,crossoriginortypecauses two downloads instead of one.
- 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.
- "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
- 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.
- 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).
- 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.
- — 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.