A Next.js app declares a `next/font/google` face in its root layout, and every route's HTML contains a preload link for that font's woff2. What decides which routes preload a next/font file, and how would you stop it?
answer
- default is on, but on for what?
- the build knows which route uses what
- where you declared it is the control
- move the call, or opt the hint out
basics
~20 sPreload hints are emitted for the routes whose module graph uses the font, and a face declared in the root layout is used by every route. Move the declaration into the modules that need it, or pass preload: false on the loader call.
solid answer
~50 snext/font preloads by default, and the scope is not global — the build emits the preload link on pages whose modules actually reference that loader's output. Declaring the face in the root layout means every route imports it, so every route preloads it, which is correct for a body font and wasteful for a display face used on one marketing page. The two levers are placement and the option: move the loader call into a module only the relevant routes import, so its preload rides along with those pages; or pass `preload: false`, which drops the hint while keeping the font self-hosted and still applied — the browser just discovers it from CSS instead of from the head. For `next/font/google`, preloading is what makes `subsets` matter, since Next has to know which character-range files are worth hinting; if you would rather not decide, turning preload off is the escape hatch. This describes next/font in the App Router, Next.js 13.2 through 16.
code
typescript · 8 lines// app/(marketing)/fonts.ts — imported only by marketing routes
import { Playfair_Display } from 'next/font/google'
export const display = Playfair_Display({
subsets: ['latin'],
weight: '700',
preload: false, // discovered from CSS; not hinted in the head
})go deeper
Know that next/font adds a preload hint for fonts by default, and that preload: false on the loader call turns that hint off without changing where the font is served from.
Explain that preload scope follows the route's module graph — a face declared in the root layout is used by every route, so every route hints it — and that placement of the loader call is therefore the real control.
Treat preloads as a budget competing with the critical path. Diagnose by comparing served HTML across routes, and separate the two fixes: relocate the declaration for a scope problem, disable the hint for a priority problem.
Set the standard for how many faces the product preloads and where font modules live, so a shared barrel imported by the root layout does not quietly promote every typeface in the codebase to app-wide preloading.
## What a preload hint buys and costs A font is discovered late by nature: the browser must fetch and parse CSS, then match a rule to an element, before it knows a font file exists. A preload hint in the document head short-circuits that, telling the browser to start the download as the HTML streams. That is genuinely valuable for the face your body copy is rendered in. It is not free. A preload competes for bandwidth with everything else on the critical path — the HTML, the CSS, the LCP image. Preloading a font that the page never renders is pure waste: bandwidth spent, a connection slot occupied, and in some browsers a console warning that the preloaded resource was not used. So the question "which pages preload this font" is a real performance question, not a detail. ## The scope rule next/font's `preload` option defaults to true, but "true" does not mean "on every page". The build knows, per route, which modules that route's tree pulls in. A loader call lives in some module; the pages that import that module (directly or transitively) are the pages that use the font, and those are the pages whose HTML gets the preload hint. That makes the placement of the loader call the actual control: - **In `app/layout.tsx` (the root layout)** — every route renders inside the root layout, so every route uses the font, so every route preloads it. That is right for the app's primary typeface. - **In a nested layout** — only routes under that segment. - **In a leaf page or a component module** — only the routes that reach it. So the symptom in the question is not a bug; it is the rule working as designed on a declaration placed at the widest possible scope. If a display face is only used by the marketing landing page, the fix is to move its loader call out of the root layout into a module that page imports. A useful corollary: exporting all fonts from one shared `app/fonts.ts` is convenient, but if the root layout imports that module for the body font, everything declared in it is now in the root's graph. Convenience and preload scope pull in opposite directions, and this is exactly the tradeoff to name in an interview. ## Turning the hint off ```ts import { Playfair_Display } from 'next/font/google' export const display = Playfair_Display({ subsets: ['latin'], preload: false, }) ``` Be precise about what this does and does not change. It removes the preload link. It does **not** stop the font being self-hosted, does **not** remove the generated `@font-face` CSS, and does **not** stop the font from being applied where you use its class. The browser simply discovers the file the ordinary way, from CSS at style-resolution time, which is later. Turning preload off is right for a face that is below the fold, used rarely, or applied only after interaction; it is wrong for the font your first paragraph is set in. ## The subsets connection For `next/font/google`, the loader needs to know which subsets you want, because a family may ship latin, latin-ext, cyrillic, greek and more as separate files, and the build has to decide which ones exist and which are worth hinting. That is why `subsets` is effectively required when preloading is on for a Google font; with `preload: false` the requirement relaxes, since nothing is being hinted. Practically, name the subsets you actually render — asking for extras inflates the payload with glyphs nobody sees. ## How to check the result View source (not the DevTools element inspector, which shows the live DOM) on two different routes and compare the preload links in the head. Or open the network panel with the cache disabled and watch which fonts start downloading at the top of the waterfall on each route. A face you see preloaded on a route that never renders it is the defect; a face preloaded on the route that renders its first paragraph is the feature. ## The judgment to demonstrate Senior candidates are expected to reason about it as a budget rather than a switch. Roughly: preload the one or two faces that render above the fold on that route, let everything else be discovered from CSS, and keep the total number of preloaded fonts small — each additional woff2 competes with the resources that determine when the page becomes useful. Two weights of one family preloaded on every route is usually defensible; five faces preloaded everywhere because they all live in the root layout's module graph is not.
- Does `preload: false` mean the font is no longer self-hosted?No. Self-hosting and preloading are independent. With `preload: false` the files are still fetched at build time, still emitted into your static output, and still referenced by generated `@font-face` CSS. The only thing removed is the head hint, so the browser discovers the file during style resolution instead of as the HTML streams.
- Why can preloading too many fonts make a page feel slower even though each font arrives sooner?Preloads compete with the critical path. Bandwidth and connection slots spent on font files are not spent on the HTML, the stylesheet, or the LCP image, so the moment the page becomes useful is pushed later even though every font individually arrives earlier. Preload the faces rendered above the fold and let the rest be discovered from CSS.
- You want a shared fonts module for convenience but not root-wide preloading. How do you get both?Split the module by scope. Keep the app-wide face in a module the root layout imports, and put narrower faces in separate modules imported only by the routes that use them. The build's per-route module graph is what drives preload scope, so one barrel imported by the root layout necessarily widens every font in it.
saying these in an interview costs you the question
- Thinks preload: true means every page preloads the font
- Believes disabling preload also disables self-hosting
- Preloads every declared weight and style by default
- Confuses the preload hint with font-display behaviour
- Checks the inspector's live DOM instead of the served HTML