PageSourceSearch

https://hoist-learn.netlify.app/chunk-D2Y_CDdC2.js

js hoist-learn.netlify.app collected 2026-10-03 10:54:43 UTC 16,283 bytes, 58 lines download raw bytes

1import{t as e}from"./chunk-BdJQ6Tnf.js";var r={id:`loading`,moduleId:`performance`,trackId:`platform-2026`,title:`Streaming & priority`,blurb:`Streaming SSR, fetchpriority, speculation rules.`,minutes:10,level:`deep`,tldr:{model:"Nearly every loading problem is discovery or priority: the browser learned about a resource too late, or ranked it wrongly. `preconnect` makes no request, `preload` creates one, `fetchpriority` only reorders one.",gotcha:"Images start at low priority until layout proves they are visible, so blanket lazy `loading` on the hero delays the LCP fetch — eager and high above the fold, lazy below it."},blocks:[{kind:`prose`,md:`The previous lesson was about the metrics. This one is about the machinery underneath them \u2014 how a browser decides what to fetch, in what order, and how early it is allowed to start.
2
3Nearly every loading problem is one of two things: the browser **found out too late** that it needed something, or it found out in time and **ranked it wrongly**. Discovery and priority. Every technique below is one of those two levers, and knowing which lever a technique pulls is what separates a diagnosis from a checklist.
4
5This is documented platform behaviour with real API names \u2014 none of it can be executed in jsdom, which has no loader, no priority queue and no rendering. The numbers about this application at the end are real, taken from its own build and configuration.`},{kind:`heading`,text:`Discovery: the preload scanner`},{kind:`prose`,md:"Browsers run a second, lightweight parser over the raw HTML bytes, ahead of the main parser: the **preload scanner**. It understands almost nothing about the document — it just looks for `<img src>`, `<link href>` and `<script src>` and starts fetching them.\n\nIt exists because the main parser stops dead at a synchronous `<script>`, and without the scanner the browser would sit idle on a fast connection while a script downloads. With it, everything else in the document is already in flight.\n\nThe consequence is a rule with no exceptions worth caring about: **a resource referenced in the initial HTML is discovered immediately; a resource created by JavaScript is not discovered until the JavaScript has been downloaded, parsed, executed and rendered.** On a mobile connection that gap is routinely hundreds of milliseconds, and it is the single most common explanation for a late LCP.\n\nThe corollaries follow directly. CSS-referenced resources — a background image in a stylesheet — are invisible to the scanner too, because the scanner does not parse CSS. A framework component that renders the hero image after hydration has, by construction, forfeited early discovery. And `<img src>` beats every clever alternative for anything that must appear fast."},{kind:`heading`,text:`Priority: what the browser guesses, and how to correct it`},{kind:`prose`,md:`Once resources are discovered they are queued, roughly in this order:
6
71. The HTML document.
82. CSS and fonts found in the head \u2014 render-blocking, so highest.
93. Synchronous scripts.
104. \`async\` and \`defer\` scripts.
115. **Images \u2014 low**, promoted to high only once layout confirms they are in the viewport.
126. Prefetches, at the lowest priority of all.
13
14Step 5 is the important one. The browser cannot know your hero image is the LCP element until it has done layout, and layout needs CSS, so by default the most important pixel on the page is fetched at low priority behind everything else. That is what \`fetchpriority\` exists to correct.
15
16The three tools are distinct, and using the wrong one is common:
17
18- **\`fetchpriority="high|low|auto"\`** \u2014 a **hint** that reorders a request the browser was going to make anyway. Cheap, safe, no new request.
19- **\`<link rel="preload">\`** \u2014 creates a fetch for something that would otherwise be discovered late, typically a font referenced from CSS or a resource behind a JavaScript boundary. It is a **priority override**, so over-preloading simply flattens the queue and prioritises nothing.
20- **\`<link rel="preconnect">\`** \u2014 makes no request. It performs DNS, TCP and TLS to an origin ahead of time, so the eventual request starts warm. Worth two or three critical origins; each one costs a connection, so ten is a regression.`},{kind:`code`,lang:`html`,caption:`The head, with each directive doing a different job`,code:`<!-- No request. Pays DNS + TCP + TLS to a critical third origin early. -->
21<link rel="preconnect" href="https://images.example.com" crossorigin>
22
23<!-- Creates a request: the font is referenced from CSS, so the
24     preload scanner would never have found it. -->
25<link rel="preload" as="font" type="font/woff2" crossorigin
26      href="/fonts/inter-subset.woff2">
27
28<!-- Reprioritises a request that was already going to happen.
29     Discoverable, eager, high priority, and it reserves its box. -->
30<img src="/hero.avif" width="1200" height="630"
31     fetchpriority="high" decoding="async" alt="Product overview">
32
33<!-- Everything below the fold: the opposite settings. -->
34<img src="/detail.avif" width="800" height="600"
35     loading="lazy" decoding="async" alt="Detail">`},{kind:`callout`,tone:`gotcha`,title:`Lazy-loading the hero`,md:'`loading="lazy"` applied to every image looks like diligence and is an own goal. A lazy image is not fetched until layout has confirmed it is near the viewport — so the LCP element now waits for CSS, layout and the scroll position check before its request even starts, undoing exactly the head start the preload scanner would have given it.\n\nThe rule is the opposite of blanket: **eager and high priority above the fold, lazy below it.** `fetchpriority="low"` is the subtler version of the same idea for images that are visible but unimportant, such as a carousel\'s second slide.'},{kind:`heading`,text:`Streaming: stop waiting for the slowest thing`},{kind:`prose`,md:"Server rendering without streaming has a structural flaw: the server must resolve **every** data dependency before it can send **any** bytes. One slow query sets TTFB for the entire document, and the browser — which could have been fetching CSS and JavaScript that whole time — is idle.\n\nStreaming SSR flushes the response in pieces. The **shell** goes out immediately: `<head>`, the stylesheet links, the script tags, the layout, and placeholders where the slow parts will go. The browser starts fetching the critical resources at once. Then, as each slow region's data resolves, the server sends that region on the **same response**, and the client patches it into place.\n\nThe framework spellings differ, the mechanism does not: React `<Suspense>` boundaries with `renderToPipeableStream`, Angular's `@defer` blocks and incremental hydration, Astro's server islands, SvelteKit's streamed promises. In each case a `<Suspense>`-shaped boundary is simultaneously **a loading-state declaration and a flush point**.\n\nThe framing to keep: streaming does not make the slow query faster. It removes the slow query from the critical path of everything else, so **a slow region delays only itself**."},{kind:`prose`,md:"**103 Early Hints** attacks the same idle time from the other end. Before the real `200`, the origin sends an informational `103` carrying `Link` headers:\n\n```\nHTTP/2 103 Early Hints\nLink: </app.css>; rel=preload; as=style\nLink: <https://images.example.com>; rel=preconnect\n```\n\nThe browser acts on those while the server is still generating the HTML. It is the only mechanism that turns server think-time into client progress. It pays when server-side work is substantial and the critical resources are known in advance; it is pointless in front of a fast static response, where the HTML arrives before the hint could have helped. Support is not universal, so treat it as an enhancement."},{kind:`heading`,text:`Speculation rules: loading the page before it is asked for`},{kind:`prose`,md:"The most aggressive lever is to start the *next* navigation before the user commits to it. **Speculation Rules** is a declarative JSON script for that, and it does something `<link rel=\"prefetch\">` never could.\n\n`prefetch` warms the cache with bytes. **`prerender` renders the page** — in a hidden background tab, running its JavaScript, building its DOM — so activating it is close to instantaneous rather than merely cached.\n\nThe `eagerness` values are the useful part of the API: `immediate` and `eager` speculate straight away, `moderate` waits for a hover of roughly 200ms, and `conservative` waits for pointer-down. For prerendering, `moderate` or `conservative` is almost always the right choice, because prerendering a page costs a full render's worth of CPU, memory and bandwidth on the user's device."},{kind:`code`,lang:`html`,caption:`Prefetch broadly, prerender narrowly`,code:`<script type="speculationrules">
36{
37  "prefetch": [
38    { "where": { "href_matches": "/lessons/*" },
39      "eagerness": "moderate" }
40  ],
41  "prerender": [
42    { "where": { "selector_matches": ".primary-cta" },
43      "eagerness": "conservative" }
44  ]
45}
46<\/script>`},{kind:`callout`,tone:`warn`,title:`A prerendered page runs`,md:`This is the part people miss. Prerendering **executes your JavaScript**, so anal
46ytics fire for a visit that never happens, view counters increment, A/B tests get assigned, and any side-effecting request on load runs speculatively \u2014 possibly many times per session for pages the user never opens.
47
48The platform gives you the guard: check \`document.prerendering\`, and listen for \`prerenderingchange\` to run the deferred work at activation.
49
50\`\`\`
51if (document.prerendering) {
52  document.addEventListener('prerenderingchange', sendPageView, { once: true });
53} else {
54  sendPageView();
55}
56\`\`\`
57
58Also budget honestly: each prerender is a full page render on the user's device, on their battery and their data. And support is not cross-browser, so this must be a pure enhancement \u2014 never load-bearing.`},{kind:`compare`,aLabel:`Technique`,bLabel:`Which lever it pulls`,rows:[{label:`preconnect`,a:`No request; warms an origin`,b:`Removes handshake latency`},{label:`preload`,a:`Creates an early request`,b:`Discovery`},{label:`fetchpriority`,a:`Reorders an existing request`,b:`Priority`},{label:`103 Early Hints`,a:`Acts during server think-time`,b:`Discovery, earlier than the HTML`},{label:`Streaming SSR`,a:`Flushes the shell before data resolves`,b:`Discovery, and TTFB`}
58,{label:`prefetch`,a:`Caches the next page bytes`,b:`Anticipation`},{label:`prerender`,a:`Renders the next page in the background`,b:`Anticipation, at real device cost`},{label:`Lazy chunks`,a:`Defers bytes not needed yet`,b:`Removes work from the critical path`}]},{kind:`heading`,text:`This app, with real numbers`},{kind:`prose`,md:'Hoist is client-rendered rather than streamed — it must work offline, which rules out an architecture that begins by asking a server for HTML. But the same two levers apply, pulled with the tools an SPA has, and the results are measurable.\n\nThe app maps **108 lessons** and ships roughly **92 kB of initial transfer** (about 334 kB raw). Two split boundaries produce that:\n\n- **Every route is lazy** via `loadComponent`, so the home screen contains neither the review session nor the profile page.\n- **Every lesson body is behind its own dynamic import** in the `LESSON_LOADERS` map. Opening one lesson fetches exactly one chunk; the other 107 are never transferred, parsed or executed.\n\nThe service worker is the prefetching half. Its `ngsw-config.json` declares the app shell — `index.html`, `/*.css`, `/*.js`, the manifest and the favicon — with `installMode: "prefetch"`, so the entire shell is pulled down at install time and every subsequent load is a **local read** rather than a network round trip. Images and fonts use `installMode: "lazy"` with `updateMode: "prefetch"`: not fetched until first needed, but refreshed eagerly when a new version ships.\n\nAnd the deliberate exception, which is the most instructive part: **the review cards are eager**, all in one `cards.ts`. Splitting them would be mechanically correct by every rule above and would make the product worse, because spaced review has to work on a cold start, offline, without opening any lesson first. The right split boundary is a product decision about what the user needs before their first interaction — not a rule that everything should be lazy.'},{kind:`callout`,tone:`interview`,title:`How this gets asked`,md:'Usually *"how would you make this load faster?"*, and the answer that stands out organises the techniques instead of listing them.\n\nOpen with the frame: nearly every loading problem is **discovery** or **priority** — the browser learned about a resource too late, or ranked it wrongly. Then explain the **preload scanner**, because it is the mechanism behind the most common finding: a resource in the initial HTML is fetched immediately, while one created by JavaScript waits for the bundle to download, parse, execute and render.\n\nSeparate the three directives cleanly, since they get conflated: **`preconnect`** makes no request and pays the handshake early, **`preload`** creates a request for something discovered late, **`fetchpriority`** only reorders a request that was already going to happen. Add the detail that shows you know the defaults: **images start at low priority** until layout proves they are in the viewport, which is precisely why the LCP image needs `fetchpriority="high"` and must never be `loading="lazy"`.\n\nFor streaming, the sentence is that it does not make the slow query faster — it takes the slow query off the critical path, so **a slow region delays only itself**, and a Suspense boundary is both a loading state and a flush point. Mention **103 Early Hints** as the only way to use server think-time productively.\n\nFinish on speculation rules, and finish with the caveat rather than the feature: `prerender` actually **executes the page**, so analytics fire for visits that never happen unless you gate on `document.prerendering` and `prerenderingchange`. Then ground it in something you have built — *"we ship 92 kB initial for 108 lessons because every route and every content body is its own chunk, but the review deck is deliberately eager so review works offline before any lesson is opened."*'},{kind:`takeaways`,items:[`Almost every loading problem is discovery or priority: the browser learned too late, or ranked it wrongly.`,`The preload scanner reads raw HTML ahead of the main parser and starts fetches even while the parser is blocked.`,`A resource created by JavaScript is invisible to the scanner and waits for the bundle to download, parse, execute and render.`,`CSS-referenced resources are also invisible to the scanner, which is why fonts are the classic preload case.`,`Default priority: document, then head CSS and fonts, then sync scripts, then async and defer, then images, then prefetch.`,`Images start low and are promoted only after layout — hence fetchpriority="high" on the LCP image.`,`preconnect makes no request, preload creates one, fetchpriority reorders one. They are three different levers.`,`Never loading="lazy" above the fold; it delays the fetch until layout confirms visibility.`,`Streaming SSR flushes the shell first so the browser can fetch CSS and JS while slow data resolves.`,`Streaming does not speed up the slow query; it stops that query delaying everything else.`,`103 Early Hints carries preload and preconnect Link headers before the real response, using server think-time.`,`Speculation rules can prerender, not merely prefetch — the page is rendered in the background and activation is near-instant.`,`Eagerness levels immediate, eager, moderate and conservative map to hover and pointer-down heuristics.`,`A prerendered page executes, so gate analytics and side effects on document.prerendering and prerenderingchange.`,`This app: 108 lessons, ~92 kB initial transfer, every route and every lesson body its own lazy chunk.`,`Its service worker prefetches the app shell at install and caches images and fonts lazily, so repeat loads are local reads.`,`The review deck is deliberately eager — the correct split boundary is a product decision, not a mechanical rule.`]}],cards:e};export{r as LOADING};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.