Suggest an editImprove this articleRefine the answer for “Resource loading strategies - preload, prefetch, modulepreload”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Preload, prefetch, and modulepreload** are `<link rel>` hints that control when and how the browser fetches resources. Preload fires at high priority immediately on discovery (LCP images, fonts, critical CSS). Prefetch queues resources at the lowest priority during idle time (next-page navigation). Modulepreload fetches ES modules and their full import graph early without executing them. ```html <link rel="preload" href="hero.jpg" as="image" fetchpriority="high"> <link rel="prefetch" href="/next-page.js"> <link rel="modulepreload" href="/app.mjs"> ``` **Key point:** always add `as` to preload, and `crossorigin` to font preloads.Shown above the full answer for quick recall.Answer (EN)Image**Resource loading strategies** are `<link rel>` hints that tell the browser when and how to fetch resources before the page actually needs them. Preload grabs critical assets at high priority the moment the parser sees the hint. Prefetch queues future-page assets at the lowest possible priority. Modulepreload fetches ES module graphs early without executing any code. ## Theory ### TL;DR - Analogy: preload = chef grabbing ingredients needed right now (high priority). prefetch = stocking the pantry for tomorrow's recipe (background, idle only). modulepreload = prepping modular ingredients without starting to cook them. - Priority gap: preload fires at the same priority as parser-inserted CSS. prefetch drops to the lowest network slot and only runs when nothing more important is queued. modulepreload is high priority, but scoped to ES modules. - Decision rule: LCP image or critical font → preload. Next-page resource the user will probably need → prefetch. ES module entry point and its import tree → modulepreload. - Gotcha: `preload` without `as` gets ignored by Chrome with the message "Did not assign resource type". Font preloads without `crossorigin` cause a cache miss on every load. ### Quick example ```html <head> <!-- High priority, fires at discovery, same queue as CSS --> <link rel="preload" href="/hero.avif" as="image" fetchpriority="high"> <!-- Font requires crossorigin or the browser fetches it twice --> <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> <!-- Lowest priority, runs when browser is idle --> <link rel="prefetch" href="/checkout.js"> <!-- Fetches app.mjs + all its imports in parallel, no execution --> <link rel="modulepreload" href="/app.mjs"> </head> <body> <img src="/hero.avif" alt="Hero"> <!-- Already in cache, no flicker --> <script type="module" src="/app.mjs"></script> <!-- Deps pre-fetched --> </body> ``` The hero image paints in under 100ms. The font loads without a flash of invisible text. App module imports resolve from cache instead of triggering a serial waterfall. ### Key difference Preload is a high-priority fetch that fires at head parse time, before the browser's normal parsing would have discovered the resource. Prefetch is speculation at idle time: the browser downloads the resource only when there is nothing more important to do, and stores it for a future navigation. Modulepreload targets ES modules specifically. It fetches the entry module, recursively resolves the `import` graph, and downloads all dependencies in parallel, but V8 does not parse or execute any of it at this stage. You get the full network benefit without touching the main thread until the `<script type="module">` tag actually runs. ### When to use - LCP hero image or above-fold video → preload with `fetchpriority="high"` - Web font referenced in critical CSS → preload with `as="font"` and `crossorigin` - JavaScript blocking first render → preload with `as="script"` - CSS needed above the fold → preload with `as="style"` - Resource on the page the user is likely navigating to next → prefetch - ES module entry points and their full import trees → modulepreload - Third-party scripts you need but can defer → prefetch - Cap preloads at 4-5 per page. Each one competes for bandwidth in the same high-priority queue. ### Comparison table | | preload | prefetch | modulepreload | |---|---|---|---| | **Priority** | High (same as CSS) | Lowest (idle only) | High (modules only) | | **When it fires** | Immediately at discovery | When browser is idle | Early, before `<script>` tag | | **Blocks render** | No | No | No | | **What it fetches** | Any type (`as` required) | Any resource type | JS modules + import graph | | **Executes code** | No | No | No | | **Main use case** | LCP image, critical font | Next-page navigation | SPA and ES module apps | | **`crossorigin` required** | For fonts, yes | No | Depends on CORS policy | | **When to use** | Critical path now | Speculative future | Modern JS module apps | ### How the browser handles this internally When the browser hits `<link rel="preload">` during `<head>` parsing, Chromium schedules the fetch at `MEDIUM` or `HIGH` in `LoadingScheduler::Priority`, the same queue as parser-inserted stylesheets. Prefetch drops to `LOWEST`, meaning it only runs once all higher-priority work clears. Modulepreload (Chrome 66+, all major browsers as of 2023) uses the browser's internal ModuleGraph. It issues a `fetch()` for the entry module, parses the `import` statements to discover dependencies, and fetches all of them in parallel. V8 skips the parse and eval stages entirely at this point. That side-steps the classic module waterfall: without modulepreload, a 3-level-deep import graph means 3 sequential round trips before any code runs. With it, everything fetches in one parallel burst. Safari 16.4 and earlier had partial support that did not recursively resolve import dependencies. Full recursive resolution arrived in Safari 17.0. ### preload in practice The `as` attribute is not optional. Without it, Chrome ignores the hint entirely and logs a console warning. The browser needs `as` to assign the correct request headers and match the pre-fetched response to the resource that later requests it. ```html <!-- Correct: browser knows the type, cache key matches --> <link rel="preload" href="/api-data.json" as="fetch" crossorigin> <!-- Browser ignores this. No cache match when the page later needs it. --> <link rel="preload" href="/api-data.json"> ``` For fonts, `crossorigin` is non-negotiable. Fonts load via CORS. A preload without `crossorigin` and the actual font request use different cache keys. The font gets fetched twice, which defeats the entire point. ### prefetch for navigation Prefetch works well when user intent is predictable. On a product detail page, prefetch the cart script. On a login page, prefetch the dashboard bundle. The browser downloads at idle time so it never competes with the current page. ```html <!-- Product page: user will likely go to cart next --> <link rel="prefetch" href="/cart.js"> <link rel="prefetch" href="/cart.css"> <!-- Blog listing: user will likely click into an article --> <link rel="prefetch" href="/article-renderer.js"> ``` One thing to keep in mind: service workers intercept prefetch requests exactly like any other fetch. If your SW has a cache-first strategy for a URL, the prefetched resource may come from the SW cache rather than the network. ### modulepreload for ES module apps Without modulepreload, an ES module app loads serially: fetch `app.mjs`, parse it, discover `import { utils } from './utils.mjs'`, fetch `utils.mjs`, parse it, discover another import, and so on. A module graph 3 levels deep means 3 sequential round trips before any code runs. ```html <!-- Without modulepreload: serial fetches, ~600ms on 3G --> <script type="module" src="/app.mjs"></script> <!-- With modulepreload: all fetched in parallel at discovery --> <link rel="modulepreload" href="/app.mjs"> <link rel="modulepreload" href="/utils.mjs"> <link rel="modulepreload" href="/api.mjs"> <script type="module" src="/app.mjs"></script> ``` Vite does this automatically. In production builds, Vite generates `<link rel="modulepreload">` for every chunk in the import graph, so the waterfall collapses to a single parallel fetch burst. ### preconnect and dns-prefetch These two operate at the connection level, not the resource level. Both are useful for third-party origins you know you will hit. `preconnect` does the full early handshake: DNS lookup, TCP connection, and TLS negotiation. That saves 100-500ms on the first request to that origin. `dns-prefetch` only resolves DNS. Cheaper, saves less (20-120ms). Use it for domains you might hit but cannot guarantee. ```html <!-- Fonts origin: you are definitely loading from here --> <link rel="preconnect" href="https://fonts.googleapis.com"> <!-- Analytics: likely, but not critical path --> <link rel="dns-prefetch" href="https://analytics.google.com"> ``` Limit preconnect to 2-3 origins. Each one keeps a TCP/TLS connection open, which has a memory cost. More than 3 and you risk burning resources on connections that time out before a request fires. ### fetchpriority attribute This is a separate lever from the hint type (Chrome 101+). It lets you bump or drop resources within their default priority bucket. ```html <!-- Boost LCP image above other high-priority fetches --> <img src="/hero.jpg" fetchpriority="high" loading="eager"> <!-- Push footer tracking pixel out of the critical path --> <img src="/tracking-pixel.jpg" fetchpriority="low"> <!-- The combination Next.js uses for its Image component --> <link rel="preload" href="/hero.jpg" as="image" fetchpriority="high"> ``` HTTP Archive data shows LCP improvement of 30-50% when pairing `preload` with `fetchpriority="high"` on above-fold hero images. Next.js applies this pattern automatically for images with the `priority` prop. ### Common mistakes **Missing `as` on preload** ```html <!-- Chrome logs "Did not assign resource type". Fetch is silently ignored. --> <link rel="preload" href="script.js"> <!-- Fix --> <link rel="preload" href="script.js" as="script"> ``` **prefetch on critical current-page CSS** ```html <!-- Gets lowest priority. CSS may arrive after paint. FOUC in production. --> <link rel="prefetch" href="critical.css" as="style"> <!-- Fix: if you need it now, use preload --> <link rel="preload" href="critical.css" as="style"> ``` **modulepreload on a classic script** ```html <!-- Does nothing for non-module scripts. No deps resolved, no cache. --> <link rel="modulepreload" href="bundle.js"> <!-- Only works with type="module" scripts --> <link rel="modulepreload" href="app.mjs"> <script type="module" src="app.mjs"></script> ``` **Font preload without crossorigin** ```html <!-- Cache miss: preload and the actual font load use different cache keys. --> <link rel="preload" href="font.woff2" as="font"> <!-- Fix --> <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin> ``` **Too many preloads at once** Preloading 8-10 resources puts them all in the same high-priority queue where they compete with each other and with render-blocking CSS. Keep it to 4-5 per page. ### Real-world usage - Next.js: auto-injects `<link rel="preload">` for images with the `priority` prop and for `_next/static/css` chunks, using `fetchpriority="high"` on LCP images. - Vite: generates `<link rel="modulepreload">` for every chunk in the module graph during production build. - React 18 streaming SSR: injects modulepreload hints into the HTML stream for hydration dependencies. - Lighthouse audit "Preload key requests" flags LCP resources that are not being preloaded. - Chrome DevTools Network tab: Priority column shows High for preload targets and Low for prefetch candidates. ### Follow-up questions **Q:** What is the fetch priority difference between preload and prefetch in Chromium's scheduler? **A:** Preload maps to MEDIUM/HIGH in `LoadingScheduler::Priority`, the same slot as parser-inserted CSS. Prefetch maps to LOWEST and only runs when no higher-priority work is queued. **Q:** Does preload block HTML parsing? **A:** No. The fetch runs in parallel with parsing. If you use an `onload` handler on the preload link to coordinate rendering, you can manually delay paint, but that is a deliberate choice, not automatic blocking. **Q:** What is the difference between modulepreload and dns-prefetch? **A:** modulepreload fetches full module files and resolves the entire import graph, populating the module cache with actual content. dns-prefetch only resolves the hostname to an IP address. No content is fetched at all. **Q:** If a preloaded resource returns a 404, does it poison the cache? **A:** No. Each subsequent request fetches fresh. You can add an `error` event listener to the preload link element to detect the failure and swap in a fallback, which is useful in SPA routers that generate dynamic asset paths. **Q:** How does preload interact with service workers? **A:** Service workers intercept preload requests the same way they intercept any fetch. If your SW has a cache-first strategy matching that URL, the pre-fetched resource may come from the SW cache rather than the network. **Q:** In production you need maximum LCP. What is the full pattern? **A:** Combine preload + `fetchpriority="high"` + `loading="eager"` on the LCP image element. Then measure with PerformanceObserver's `largest-contentful-paint` entry. On slow 3G, skipping this combo typically adds 1 second to TTI on image-heavy pages. HTTP Archive data puts the LCP gain at 30-50% for above-fold images. ## Examples ### Basic: preload, prefetch, and modulepreload on a single page ```html <!DOCTYPE html> <html> <head> <!-- Preload: high priority, fires at head parse time --> <link rel="preload" href="/hero.avif" as="image" fetchpriority="high"> <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> <!-- Prefetch: idle priority, fires in background --> <link rel="prefetch" href="/checkout.js"> <!-- Modulepreload: app.mjs + its imports fetched in parallel --> <link rel="modulepreload" href="/app.mjs"> <link rel="modulepreload" href="/utils.mjs"> </head> <body> <!-- hero.avif already in cache, paints on first frame --> <img src="/hero.avif" alt="Hero image" loading="eager"> <!-- Module deps cached, no waterfall at runtime --> <script type="module" src="/app.mjs"></script> <!-- checkout.js already cached, transition feels instant --> <a href="/checkout">Checkout</a> </body> </html> ``` The hero image is in cache before the browser reaches the `<img>` tag. Font loads without a flash of invisible text. The module graph resolves from cache in roughly 10ms instead of a 3-round-trip waterfall. ### Intermediate: Next.js production LCP optimization This reflects the pattern Next.js generates internally for a page with a hero image and a custom font. ```html <head> <!-- LCP image: preload + fetchpriority is the Next.js Image priority prop pattern --> <link rel="preload" href="/hero.avif" as="image" fetchpriority="high"> <!-- Font: crossorigin required, type helps MIME detection --> <link rel="preload" href="/_next/static/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> <!-- Critical CSS chunk from Next.js build --> <link rel="preload" href="/_next/static/css/app.css" as="style"> <!-- Prefetch likely next route --> <link rel="prefetch" href="/_next/static/chunks/dashboard.js"> </head> ``` Without the preload + fetchpriority combo, the LCP image queues behind other high-priority resources. With it, Lighthouse reports LCP at 0.8s versus 2.5s without. CLS stays at 0 because the font is ready before any text paints. ### Advanced: modulepreload with dynamic imports (collapsing the waterfall) ```html <head> <!-- Declare all modules in the graph at head parse time --> <link rel="modulepreload" href="/app.mjs"> <link rel="modulepreload" href="/utils.mjs"> <link rel="modulepreload" href="/api.mjs"> </head> <script type="module"> // Without modulepreload: // Round 1 - fetch app.mjs, parse it, find utils.mjs + api.mjs // Round 2 - fetch utils.mjs + api.mjs // Round 3 - execute: ~600ms total on 3G // With modulepreload above: // All three fetched in parallel at head parse time // Dynamic import resolves from cache in ~10ms import('./app.mjs').then(app => app.init()); </script> ``` Chrome DevTools Network timeline shows all three modules fetching in parallel from the moment the `<head>` is parsed: 200ms total. Without the modulepreload declarations, they fetch serially after `app.mjs` parses: 600ms. That 400ms gap is visible as a TTI regression on slower connections and shows up immediately in Lighthouse.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.