Skip to content

    Frontend System Design

    Preloading, Prefetching and Priority Hints

    Learn what preload, prefetch, preconnect, fetchpriority and speculation rules each do, and choose the right hint for a resource without starving the page.

    By Published 14 min read

    • frontend-system-design
    • resource-hints
    • preload
    • prefetch
    • fetch-priority
    Share

    Standard lessonintermediateFrontend System Design

    Before this: The Critical Rendering Path

    What you'll learn: By the end of this lesson you will be able to say which of four questions a loading hint answers (when is the resource discovered, how urgent is it, is it for this page or the next, is the connection ready), use preload, modulepreload, prefetch, preconnect, fetchpriority and Speculation Rules for the right job, avoid each one's failure mode (a missing as, a mismatched crossorigin, prefetching a URL with side effects), and choose a hint with a short procedure.

    In a restaurant kitchen the cook starts a dish when the order reaches the pass. A good waiter helps in three ways. They call ahead: "table four will need the fish soon". They say: "table five will probably order dessert, start the base in a quiet moment". And they say: "do the soup first, that guest is waiting". A waiter who calls out everything makes the day worse, because the cook cannot do everything at once.

    A browser is that cook. It discovers resources as it reads the page and has a limited number of bytes per second to spend. Resource hints are the waiter: a line of HTML, an HTTP header or some JSON that tells the browser what it could not yet know. The running example is an online store's product page: its hero image is set from CSS, a brand font is named in a stylesheet, a reviews panel loads a JavaScript chunk when a tab is clicked, and most visitors who leave go to the checkout. Optimizing Largest Contentful Paint and Images and Fonts show a few hints in context; here they are the subject.

    The core idea

    Every hint answers one of four questions. Mixing them up causes most hint bugs.

    1. Discovery. Will the browser find the resource early enough? A resource named only inside a stylesheet, or created by script, is found late. preload and modulepreload say "this exists, start now".
    2. Priority. Of the things being fetched, which goes first? fetchpriority raises or lowers the browser's default.
    3. Scope. Is it for this page or a future navigation? preload is for the current page. prefetch and Speculation Rules are for the next one.
    4. Connection. Is the server ready to talk? preconnect and dns-prefetch do the handshake early.

    A hint never makes the network faster. It changes when work starts or in what order, and the page's bytes are shared, so a hint that helps one resource costs the others a little.

    Figure 1. Four hints, four questions

    Loading hints act on discovery, priority, connection and the next navigationHTML and CSS to Fetch queue (preload); Fetch queue to Network (fetchpriority); Network to Other origin (preconnect); Speculation rules to Prefetch cache (prefetch); Prefetch cache to Next page (click)THIS PAGENEXT NAVIGATIONHTML and CSS: tags, Link headersHTML and CSStags, LinkheadersFetch queue: discovery, priorityFetch queuediscovery,priorityNetwork: shared bandwidthNetworkshared bandwidthOther origin: DNS, TCP, TLSOther originDNS, TCP, TLSSpeculation rules: JSON in the pageSpeculationrulesJSON in the pagePrefetch cache: HTTP cache or memoryPrefetch cacheHTTP cache ormemoryNext page: activates from cacheNext pageactivates fromcachepreloadfetchprioritypreconnectprefetchclick

    How to read this: the top row is the current page. Preload moves discovery earlier, fetchpriority reorders the queue and preconnect warms a connection. The bottom row is the next navigation, served from a cache that a low-priority fetch filled earlier.

    The hint family, as the HTML Standard defines it

    The HTML Standard defines each hint as a link type. Its wording separates must (an instruction) from should (advice the browser may ignore).

    Hint What the standard says Scope Strength
    dns-prefetch Resolve the origin's DNS name early this page hint
    preconnect Connect to the origin early; "should attempt" a full handshake, may do part or skip it this page hint
    preload Fetch and cache for the current navigation, using the destination in as this page must
    modulepreload Fetch a module script into the document's module map; dependencies optionally this page must
    prefetch Fetch and cache a resource likely needed for a followup navigation; the browser may delay it to favour what the current document needs next page should

    Also from the standard: preload and preconnect links must not delay the load event, and Link header processing (the hint sent as an HTTP header) is defined for those two but does nothing for prefetch. How urgent each request is, is left to the browser; where this lesson names priorities it uses Chromium's documented values.

    Preload: say it early, say it once

    MDN describes rel="preload" as declaring fetch requests in the <head> for resources the page "will need very soon", so they start "before browsers' main rendering machinery kicks in". It does not run a script or apply a stylesheet; it schedules a download and caches the result.

    <link rel="preload" href="/fonts/brand-regular.woff2" as="font" type="font/woff2" crossorigin>
    • as names the kind of resource. MDN lists fetch, font, image, script, style and track, and says it lets the browser pick the right cache, content security policy and Accept header. In the standard's steps a link whose as is not a valid preload destination makes no request, so a missing as is a silent no-op.
    • type lets the browser skip a format it does not support. media makes a preload conditional (media="(width <= 600px)").
    • crossorigin must match how the resource will later be requested. MDN says font and fetch preloads need it "even when the fetch is not cross-origin", because they are fetched in anonymous CORS mode.

    Illustration 1: the minimal case. The product page's hero is a CSS background-image, found only after the stylesheet has been downloaded and applied. A preload moves discovery to the first bytes of HTML:

    <link rel="preload" href="/img/kettle-hero-1200.avif" as="image" type="image/avif" fetchpriority="high">

    Responsive images use imagesrcset and imagesizes; those and 103 Early Hints are in the LCP lesson.

    What preload is not. Web.dev says "Preload is a mandatory fetch, not a hint", and it still fetches at the resource's default priority unless told otherwise. An unused preload spends bandwidth for nothing. After adding one, open the Network panel and check that the resource is requested once.

    For ES modules use <link rel="modulepreload" href="/assets/reviews-4f2a.js">. MDN says it also parses and compiles the module into the module map. Fetching its imports is allowed but not required ("a browser-specific optimization"), so the portable approach is to list each dependency. Code splitting covers bundlers that emit these links. MDN marks it Baseline widely available since September 2023.

    Preconnect and dns-prefetch

    preconnect starts DNS, TCP and (for HTTPS) TLS for another origin. MDN says it "has no benefit on same-origin requests because the connection is already open", is "best used for only the most critical connections", and that preconnecting to many third-party domains "can be counterproductive". dns-prefetch does only the DNS step, a cheaper hint for origins that might be used. Reserve preconnect for one or two origins on the critical path, such as the image CDN that serves the hero.

    Priority: fetchpriority reorders the queue

    Browsers assign each request a priority from its type and position. Web.dev's page on Fetch Priority says fetchpriority "sets relative priority", raising or lowering the default. It takes high, low and auto, works on img, link and script, has a priority option on fetch(), and is "a hint, not a directive". Support, per web.dev: Chrome and Edge 102, Firefox 132, Safari 17.2.

    Chromium defaults (web.dev, at the time of writing). These are one engine's choices, not the standard's.

    Resource Default priority
    Main HTML, early CSS in the head VeryHigh
    Font VeryHigh; a preloaded font starts at High
    Early script High
    async or defer script Low
    Image in the viewport after layout Starts Low, raised to High
    First five large images (since Chrome 117) Medium
    fetch() and async XHR High
    prefetch VeryLow

    The lesson in the table is that priority is zero-sum: if five things are High, none stands out. Good uses are narrow. Mark one or two first-screen images high so they start at High instead of waiting for layout. Mark off-screen carousel images, and a preload of a non-critical script, low. Use priority: "low" for a background fetch(). Raise an async script that matters more than its default of Low.

    Web.dev says preload and priority do different jobs: preload helps discovery but not urgency, and fetchpriority helps urgency but not discovery. A preloaded hero usually needs both. It adds that gains are larger on constrained connections.

    Prefetch: spend idle time on the next page

    prefetch is for a future navigation. MDN says it is like a low-priority fetch(), generally lower than preload because "the current page is more important than the next". The result is kept in the HTTP cache, so it works for subresources the current page never uses, such as the checkout bundle. Limits: a Cache-Control: no-store or no-cache response may not be usable from a prefetch; many browsers partition the cache per top-level site, which MDN says makes prefetch useless for resources used by a different site; and the request carries Sec-Purpose: prefetch, which a server can act on. Because of this MDN advises Speculation Rules for document prefetches where supported, and rel=prefetch for subresources.

    Speculation Rules: prefetch or prerender a page

    The Speculation Rules API targets document URLs, so it suits multi-page sites more than single-page apps. A script of type speculationrules holds JSON:

    <script type="speculationrules">
    {
      "prefetch": [
        {
          "where": {
            "and": [
              { "href_matches": "/product/*" },
              { "not": { "selector_matches": ".no-prefetch" } }
            ]
          },
          "eagerness": "moderate"
        }
      ],
      "prerender": [
        { "urls": ["/checkout"], "eagerness": "conservative" }
      ]
    }
    </script>

    Prefetch versus prerender. Per MDN, a prefetch downloads the page's response body and none of its subresources, at a cost "much smaller" than a prerender. A prerender loads the page in an invisible tab and runs its JavaScript, at "about the same amount of resources as rendering an <iframe>", and the later navigation is near-instant. MDN encourages prefetching broadly and prerendering "more sparingly".

    Eagerness says how early to act (MDN): immediate as soon as possible; eager on "any slight suggestion", such as a hover; moderate after reasonable evidence, such as hovering a link in view; conservative only when the user starts to click. Unset, list rules (urls) default to immediate and document rules (where) to conservative. The browser may weigh the hint against its own heuristics.

    Figure 2. A prefetch triggered by a hover

    sequenceDiagram accTitle: Document prefetch from speculation rules accDescr: The user hovers a link, the browser prefetches the page with a Sec-Purpose header, the server answers, and a later click uses the held response. participant U as User participant B as Browser participant S as Server U->>B: hovers a product link (moderate) B->>S: GET /product/lamp with Sec-Purpose: prefetch S-->>B: 200 HTML (body held in memory) U->>B: clicks the link B->>B: navigate using the held response Note over B,S: if the user never clicks, the response is discarded

    How to read this: the request is real and reaches the server. Only the reuse is speculative, so the server pays for every wrong guess.

    Guardrails (Chrome's documentation, at the time of writing).

    Chrome limit Prefetch Prerender
    immediate 50 10
    eager or moderate, desktop 2 2
    eager or moderate, mobile 6 2
    conservative 2 2

    Chrome skips speculation when Save-Data is on, under battery saver with a low battery, under memory constraints, or in a background tab. Prerender is same-origin by default, and cross-site prefetch works only when the user has no cookies for the destination site. A response outside 200 to 299 is not used, and Chrome keeps prefetched pages for about five minutes.

    Support is partial. MDN marks the API experimental and not Baseline; Chrome's page shows Firefox as not supported for several features and Safari as behind a flag or not supported. Treat it as progressive enhancement: feature-detect with HTMLScriptElement.supports("speculationrules"), and keep the page correct without it.

    The safety rule: a speculated URL runs on your server. MDN lists URLs that must not be speculated: sign-out, language switching, add to cart, sending a one-time password, incrementing a usage allowance, recording an ad conversion. A prerendered page also runs its script, so an analytics tag can record a visit nobody made; the Document.prerendering property lets scripts wait for activation. Make state-changing endpoints POST, exclude them with a not rule, and never treat Sec-Purpose as authentication.

    Choosing: a short procedure

    Figure 3. Which hint for which resource

    flowchart TD accTitle: Choosing a loading hint accDescr: First ask whether the resource is for this page or the next. For this page, ask whether the browser can find it in the HTML; if yes leave it and adjust fetchpriority, otherwise use preload or modulepreload. If the origin is different add preconnect. For the next page use speculation rules or prefetch. Q1{"For this page
    or the next?"} -->|"this page"| Q2{"Can the browser find it
    in the HTML?"} Q1 -->|"next page"| Q5{"Whole document
    or a subresource?"} Q2 -->|"yes"| P1["Leave it. Adjust fetchpriority
    if the default is wrong"] Q2 -->|"no: CSS, font, script-made"| Q3{"A module
    chunk?"} Q3 -->|"yes"| M["modulepreload"] Q3 -->|"no"| P2["preload with as, type,
    crossorigin"] P1 --> Q4{"Different origin?"} P2 --> Q4 M --> Q4 Q4 -->|"yes"| C["preconnect (one or two)"] Q5 -->|"document"| S["Speculation rules:
    prefetch, prerender sparingly"] Q5 -->|"subresource"| F["rel=prefetch"]

    How to read this: the cheapest correct answer is often "put it in the HTML and do nothing else". A hint is for resources the browser cannot find, or finds in the wrong order.

    Hint Helps Costs Typical failure
    preload Late-discovered resources of this page Bandwidth if unused; competes with others Missing as, wrong crossorigin, too many
    modulepreload Route chunks and imports Download, parse and compile work Listing every module on every page
    preconnect First request to a critical other origin A connection that may go unused Preconnecting to many origins
    fetchpriority Order of a few key requests Others wait Marking many things high
    prefetch (link) Subresources of the likely next page Idle bandwidth, cache space Responses with no-store
    Speculation rules Near-instant next navigation Server load, memory, side effects Speculating state-changing URLs

    Illustration 2: the realistic example. On the product page the team keeps the hero as a server-rendered <img> with fetchpriority="high" (no preload needed), preloads the one brand font used above the fold, preconnects to the image CDN, uses modulepreload for the reviews chunk on the product route only, and adds a moderate document prefetch for /product/* and a conservative prerender for /checkout. Nothing is hinted that the browser already finds.

    Illustration 3: the failure case. A template adds rel="preload" for six fonts, four images and two scripts "to be safe". Each is a mandatory fetch, so the one image that is the LCP competes with eleven others for the same bandwidth. Two images belong to a tab nobody opens first, and a font preload lacks crossorigin, so it does not match the anonymous-CORS request the font rule makes, the mismatch MDN warns about. LCP is worse than before. The fix is to remove hints until the Network panel shows only what the first screen needs.

    In practice: a store that prefetched everything

    NOTE

    This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company, and every number is an illustrative assumption.

    The situation

    An online store's product pages are server-rendered. Product managers want category pages to feel instant. A developer adds one document rule: every same-origin link on a category page is prefetched, eager. Lab navigation times drop and the change ships on a Friday.

    Starting architecture

    Figure 4. What the broad rule matches

    A document rule that matches every link also prefetches an add-to-cart linkCategory page to Speculation rules (links in view); Speculation rules to Product pages (prefetch); Speculation rules to GET /cart/add?sku= (prefetch)BROWSERSTORE SERVERSCategory page: document rule: all linksCategory pagedocument rule:all linksSpeculation rules: prefetch, eagerSpeculationrulesprefetch, eagerProduct pages: safe to fetchProduct pagessafe to fetchGET /cart/add?sku=: changes the cartGET/cart/add?sku=changes the cartlinks in viewprefetchprefetch

    How to read this: the rule does not know which links are safe. The cart link is an ordinary anchor, so it matches, and prefetching it is a real request that changes server state.

    Options considered

    Option What it fixes What it costs Risk
    Keep the broad rule, add an exclusion list Keeps the speed-up A list to maintain A new unsafe link slips through
    Allow-list /product/* at moderate Fewer wasted requests, nothing unsafe A narrower speed-up Misses links outside the pattern
    Make state changes POST and keep the rule Fixes the class of bug A front-end and back-end change Needs a migration

    The decision

    Field Decision record
    Context A broad rule prefetches every link, including ones that change state.
    Decision Allow-list /product/* and /category/* at moderate, exclude .no-prefetch, and make "add to cart" a POST form button.
    Consequences, good Prefetches are safe to repeat; the server sees fewer speculative requests.
    Consequences, bad Two teams ship together; unlisted pages get no speed-up.
    Revisit when Field data shows prefetched pages are rarely activated.

    Rollout and verification

    1. Log the header. Count requests with Sec-Purpose: prefetch per route against actual navigations to them.
    2. Ship the allow-list to a small share of traffic and compare field LCP for navigations that used a prefetch against those that did not.
    3. Replay a prefetched sequence in staging and diff cart and database state, and watch analytics for page views with no click.

    The 2 a.m. failure

    Support reports carts holding items nobody added, and stock counts dropping for products no order has bought. The "add to cart" link was a GET, and the eager rule fetched it for every link in view. First three checks: (1) filter access logs for Sec-Purpose: prefetch on /cart/add; (2) compare cart changes with and without that header; (3) read the rule's matching pattern in the page source. Mitigation: exclude the path with a not rule and deploy; remove the added items by session. Follow-up: make the endpoint a POST and add a test that fails when a link matched by a speculation rule is not safe to call twice.

    Architect's notes

    • Start from the resource inventory. A list of what the first screen needs, in order, nearly writes the hint list. Hints without an inventory are guesses.
    • Prefer discoverable over hinted. Server-render the hero <img> and the stylesheet link; preload only what cannot be found.
    • Treat speculation as a capacity decision. Every wrong guess is a real request. Cap it with moderate, conservative and an allow-list. Defer prerender until the page is safe to render without a user.
    • Put hints in templates with an owner and a test. They rot when layouts change.

    Common mistakes

    • Preloading without as, so nothing is fetched, or a font without a matching crossorigin.
    • Putting fetchpriority="high" on many images.
    • Using rel="prefetch" for a document and expecting it to help a cross-site navigation.
    • Speculating a URL that changes state, or running analytics in a prerendered page.

    Key takeaways

    • A loading hint answers one of four questions: discovery, priority, scope or connection.
    • preload and modulepreload are mandatory fetches for this page; prefetch, preconnect and dns-prefetch are hints the browser may ignore.
    • Preload needs the right as, type and crossorigin; without a valid as nothing is fetched.
    • fetchpriority is relative and zero-sum: use it on one or two resources.
    • For the next page prefer Speculation Rules (prefetch broadly, prerender sparingly) and keep rel=prefetch for subresources.
    • A speculated URL is a real request: make state changes POST, exclude them, and handle analytics in prerendered pages.
    • Support differs by browser; feature-detect and keep the page correct without the hint.

    Practice exercises

    Exercise 1 (30 min): In DevTools, open the Network panel's Priority column for a page of your choice and list five resources with their priorities. Which differ from what you expected?

    Exercise 2 (1 hour): Build a page whose hero is a CSS background-image. Note when its request starts, add a preload with as="image", and compare. What changed and what did not?

    Exercise 4 (half day): Add a moderate document prefetch to a small multi-page site. Log Sec-Purpose on the server and count prefetches never followed by a navigation. Then write a check that fails when a rule matches a URL not on a safe-to-repeat list, and say what it cannot see.

    In this course:

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments