skip to content

A team adds <link rel="preload" href="/fonts/inter.woff2" as="font"> to their page head, and the browser ends up downloading that font file twice. Why, and what do the as and crossorigin attributes control on a preload?

level: middleimportance: should knowfreq 52%

answer

  1. the two requests must match exactly
  2. fonts are fetched anonymously
  3. one attribute with no value
  4. destination, priority, Accept header
  5. the console warning names the symptom

basics

~20 s

Fonts are always fetched in anonymous CORS mode, so a preload without the crossorigin attribute produces a request the real font request cannot reuse, and the file downloads twice. The as attribute sets the request's destination, priority and Accept header.

solid answer

~50 s

A preload only helps if the request it makes is *identical* to the request the page will later make — same URL, same destination, and same CORS and credentials mode. Font files are always fetched anonymously in CORS mode, so `<link rel="preload" as="font">` must carry `crossorigin` even for a same-origin font. Without it the preload creates a differently-moded request, the real font request does not match it, and you pay for the file twice while shipping a console warning about an unused preload. `as` is the other half of matching: it tells the browser what kind of resource this is, which sets the request destination, the `Accept` header, the priority the request gets, and which Content Security Policy directive applies. Omitting `as` leaves the browser guessing — it fetches at a low priority and the request often fails to match too. Adding `type="font/woff2"` is optional but lets a browser skip a format it cannot use.

code

html · 5 lines
html
<!-- broken: the real font request is CORS-anonymous, this one is not -->
<link rel="preload" href="/fonts/inter.woff2" as="font">

<!-- correct: same URL, same destination, same credentials mode -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>

go deeper

for a junior

Remember that a font preload needs the crossorigin attribute and that as declares what kind of resource is being fetched. Being able to spot the same file downloaded twice is enough at this level.

for a middle

Explain the matching rule: a preload is reused only when the URL, destination and credentials mode all match the later request, and describe what as sets on the request beyond documentation.

for a senior

Demonstrate that you verify hints rather than trust them. Read the waterfall and the console warnings, and treat a duplicated download as a regression that costs bandwidth on the critical path.

for a principal

Own how these tags stay correct over time. Generate preloads from the build manifest rather than hand-writing them, and put a check in place so a renamed asset cannot leave a silently wasted fetch in production.

## Preload only works if the two requests match The mental model that makes every preload bug obvious: `<link rel="preload">` performs a real network request and parks the result in a short-lived preload cache. Later, when the page issues the *actual* request — the CSS asking for a font, an `<img>` asking for a picture, `fetch()` asking for JSON — the browser tries to satisfy it from that cache. It will only do so if the two requests are the same request. "Same" means more than the same URL: the **destination**, the **CORS mode** and the **credentials mode** must line up as well. Any mismatch and the browser does the honest thing, which is to go and fetch it again. ## Why fonts are the canonical failure Font files are special: the CSS Fonts specification requires them to be fetched in **anonymous CORS mode** — no cookies, no credentials — regardless of whether they are same-origin. So the request your `@font-face` rule eventually makes is a CORS-mode, credentials-omitted request. A bare `<link rel="preload" as="font">` is *not* that. Without a `crossorigin` attribute the link makes a request in a different mode, so the font request that follows sees no match and starts a second download. The symptom is unmistakable: the same font file appears twice in the network waterfall, and the console carries a warning that the resource "was preloaded using link preload but not used within a few seconds from the window's load event". Chrome also emits a more specific warning when the credentials mode is the thing that failed to match. The fix is one attribute: ```html <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> ``` Note that `crossorigin` with no value is equivalent to `crossorigin="anonymous"`, which is what a font needs. This is one of the few places in HTML where the *absence* of a value is meaningful and correct. ## What `as` actually controls `as` is not documentation; it is the request's type declaration, and it drives four things: - **Destination.** It sets the request destination (`font`, `script`, `style`, `image`, `fetch`, `document`, `audio`, `video`, `track`), which is part of what the later request is matched against. - **Priority.** The browser derives the fetch priority from the kind of resource. A preload declared `as="style"` is treated as urgent; `as="image"` is not. - **The `Accept` header.** An `as="image"` preload sends the image `Accept` header, which matters when the server does content negotiation and would otherwise return a different format than the real request receives. - **Content Security Policy.** The relevant directive (`script-src`, `style-src`, `font-src`, and so on) is chosen by the declared type, so a mis-declared preload can be blocked or wrongly allowed. Omit `as` entirely and the browser cannot do any of that: the resource is fetched at a low priority, with a generic `Accept`, and the later request usually does not match — the same double-download you get from the missing `crossorigin`, arrived at by a different route. ## The same matching rule beyond fonts - **`fetch()` payloads.** Use `as="fetch"`, and match the credentials mode of the call. A same-origin `fetch()` with default options matches a plain preload; a cross-origin `fetch()` in CORS mode needs `crossorigin` on the preload. - **Images.** `as="image"` matches an `<img>` or a CSS background. If the real image is picked from `srcset`, the preload must resolve to the same candidate the browser will choose, or you have preloaded a file nobody wants — `imagesrcset`/`imagesizes` on the preload exist for exactly that case. - **Cross-origin scripts.** A module script or any script fetched in CORS mode needs the preload's `crossorigin` to match, otherwise you are back to two downloads. ## How to verify, not assume Every preload should be checked once, not trusted forever. Load the page, look at the waterfall for a duplicated URL, and read the console for the unused-preload warning. A preload that shows up twice is strictly worse than no preload at all — it costs bandwidth, contends with the critical path, and delivers nothing. And because these tags are pure side effects, they rot quietly: a font renamed by a build step or a `type` narrowed to a format you no longer ship leaves the tag in place, still fetching, still useless.

  • How would you confirm from the browser alone that a preload is being wasted?
    Two signals. In the network waterfall the same URL appears twice, once from the link tag and once from the real consumer. In the console, Chrome warns that the resource was preloaded but not used within a few seconds of the load event, and issues a separate warning when the credentials mode is what failed to match. Either one means the tag is costing bytes and buying nothing.
  • Does a preload for data fetched with fetch() also need crossorigin?
    Only when the real call is cross-origin in CORS mode. The rule is to match modes: use `as="fetch"`, and give the preload the same credentials mode the `fetch()` will use. A same-origin `fetch()` with default options matches a plain preload; a cross-origin CORS call needs `crossorigin` on the link, or the two are separate cache entries.
  • What goes wrong when you preload an image that the page picks from a srcset?
    You may preload a candidate the browser never selects, so the file is downloaded and discarded while the chosen candidate is fetched separately. A preload for a responsive image should carry `imagesrcset` and `imagesizes` mirroring the element's, so the preload resolves to the same candidate the layout will end up requesting.

saying these in an interview costs you the question

  • Thinks crossorigin is only for third-party origins
  • Leaves out as because the file extension is obvious
  • Assumes preload always avoids a second download
  • Believes type replaces the as attribute
  • Adds preloads without ever checking the waterfall

context