examples/resource-hints/template.md
This example gathers every way to configure <link rel="preload"> /
<link rel="prefetch"> / <link rel="modulepreload"> emission in one place.
Each scenario is a separate compiler in the webpack.config.js array; pick
the one that fits your app and copy from it.
Two surfaces are involved:
output.resourceHints — the emission config. It accepts the initial chunk
graph shorthand directly (true / "prefetch" / "none" /
HtmlResourceHint[] / a function — equivalent to { initial: … }) or the
full object { initial, urlHints, preconnect, modulePreloadPolyfill, manifest }.
Hints land in an HTML entry's <head> (or stats.entrypoints[name].resourceHints
/ the manifest file for SSR). initial defaults on for ESM output
(output.module) — like Vite, since native import() would otherwise
waterfall; classic output stays opt-in.module.parser.<type>.urlHints — controls per-URL-referenced-asset
defaults (fonts, images, workers) for new URL(...), CSS url(...),
HTML ``, etc. Per-URL webpackPreload / webpackPrefetch magic
comments still win. output.resourceHints.urlHints is a project-wide shorthand
that seeds the same list under every parser at once.output.resourceHints: true. Auto-emit
<link rel="modulepreload"> (ESM) or <link rel="preload" as="script">
(classic) for the HTML entry's initial dependency chunks.output.resourceHints: "prefetch". Same as above but with
<link rel="prefetch"> (idle-time hint).output.resourceHints: [{...}, ...]. Supply your own
<link> list (preconnect, custom font, chunk/entry references).output.resourceHints: fn. One hook that receives the auto
defaultHints plus { entryName, entrypoint, hostType, compilation } and
returns the final list. Each hint carries hostChunks (the referencing chunk
names — Vite's hostId) so you can rewrite per origin chunk. Replaces both
the old chunks: fn and resolveDependencies hooks; runs for HTML pages
(hostType === "html") and JS-only entries (hostType === "js").module.parser.javascript.urlHints. Rule-based
preload/prefetch defaults for JS new URL(...) references. Same
shape works under parser.css.urlHints (CSS url(...)) and
parser.html.urlHints (HTML `` / <link href>).module.rules[].parser.urlHints. Because parser
options are scope-aware, a rule can narrow urlHints to a subtree
(e.g. critical routes upgrade prefetch → preload).output.resourceHints.manifest + output.resourceHints. JS-only
build; hints are written to a JSON file ({ [entry]: descriptors }) and are
also on stats.entrypoints[name].resourceHints. The server renders them into
the initial HTML shell.module.parser.css.fontPreload: true. Auto-emit
<link rel="preload" as="font"> for the primary @font-face src in the
initial CSS. Only the first url per @font-face is preloaded.output.resourceHints: "none". Hard off switch: no <link>
anywhere and empty stats / manifest. (false only disables the auto chunk
hints; URL-asset hints keep firing.)output.resourceHints.urlHints. Project-wide shorthand for the
same urlHints list under every parser (JS / CSS / HTML). Parser-scoped
rules and magic comments still override it.module.parser.javascript.dynamicImportCssPreload.
Auto <link rel="preload" as="style"> for a dynamically imported chunk's
CSS (parallel with the chunk; the JS itself is not preloaded).output.resourceHints.preconnect. Emit
<link rel="preconnect"> for a cross-origin output.publicPath origin
(the CDN serving your bundles / assets).{ initial, urlHints, preconnect, modulePreloadPolyfill, manifest }.modulePreloadPolyfill: false. The polyfill default
is derived from output.environment.modulePreload; opt out under a strict
CSP that forbids inline scripts.module.parser.javascript.dynamicImportPreload.
Couples JS and CSS of an async chunk (Vite parity) — contrast with
scenario 11's CSS-only dynamicImportCssPreload.output.module) enables
initial by default, so initial chunks get <link rel="modulepreload">
with no resourceHints config._{{webpack.config.js}}_
_{{src/routes/home.js}}_
_{{src/routes/home-with-assets.js}}_
_{{src/routes/home-with-css.js}}_
_{{src/styles/app.css}}_
With output.resourceHints.manifest: "ssr-hints.json", webpack writes the
manifest for you during the build — no stats plumbing needed:
// dist/ssr/ssr-hints.json
const manifest = require("./dist/ssr/ssr-hints.json");
// { "home": [ { rel, href, as?, type?, media?, fetchPriority? }, ... ], "product": [ ... ] }
Prefer computing it in-process (e.g. custom filtering)? The same data is on
stats.entrypoints[name].resourceHints:
const webpack = require("webpack");
const fs = require("fs");
webpack(require("./webpack.config").find((c) => c.name === "ssr"), (err, stats) => {
if (err || stats.hasErrors()) return console.error(err || stats.toJson().errors);
const json = stats.toJson({
all: false,
entrypoints: true,
chunkGroupResourceHints: true
});
const manifest = {};
for (const [name, ep] of Object.entries(json.entrypoints)) {
manifest[name] = ep.resourceHints || [];
}
fs.writeFileSync("dist/ssr/hints.json", JSON.stringify(manifest, null, 2));
});
Server:
const escape = (s) => String(s).replace(/"/g, """);
const renderHint = (h) => {
const attrs = [];
if (h.as) attrs.push(`as="${escape(h.as)}"`);
if (h.type) attrs.push(`type="${escape(h.type)}"`);
if (h.media) attrs.push(`media="${escape(h.media)}"`);
if (h.fetchPriority) attrs.push(`fetchpriority="${escape(h.fetchPriority)}"`);
return `<link rel="${h.rel}" href="${escape(h.href)}"${attrs.length ? " " + attrs.join(" ") : ""}>`;
};
app.get("/product/:id", (req, res) => {
res.type("html").send(`<!doctype html>
<html>
<head>
${(manifest.product || []).map(renderHint).join("\n ")}
<title>Product</title>
</head>
<body>
<div id="root">${renderToString(<App id={req.params.id} />)}</div>
<script type="module" src="/static/product.js"></script>
</body>
</html>`);
});
output.resourceHints and module.parser.<type>.urlHints cover the
initial graph only. Hints for chunks loaded via import() are their
own subsystem — use module.parser.javascript.dynamicImportPrefetch /
dynamicImportPreload / dynamicImportFetchPriority, or per-call
/* webpackPrefetch: true */ magic comments. Those route through
webpack's existing on-demand chunk-load runtime. dynamicImportCssPreload
is a CSS-only variant: it preloads a dynamically imported chunk's stylesheet
(as="style") in parallel with the chunk, without preloading its JS.
Highest wins:
webpackPreload: true, webpackAs: "font", …)module.parser.<type>.urlHints rule matchFor chunk hints, output.resourceHints is a single source of truth — the
callback form receives auto defaults and can filter / add / rewrite them
however you need.
_{{stdout}}_