Back to Webpack

Resource hints

examples/resource-hints/template.md

5.109.07.8 KB
Original Source

Resource hints

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.

Scenarios

  1. autooutput.resourceHints: true. Auto-emit <link rel="modulepreload"> (ESM) or <link rel="preload" as="script"> (classic) for the HTML entry's initial dependency chunks.
  2. prefetchoutput.resourceHints: "prefetch". Same as above but with <link rel="prefetch"> (idle-time hint).
  3. custom-arrayoutput.resourceHints: [{...}, ...]. Supply your own <link> list (preconnect, custom font, chunk/entry references).
  4. callbackoutput.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").
  5. url-hintsmodule.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>).
  6. url-hints-scopedmodule.rules[].parser.urlHints. Because parser options are scope-aware, a rule can narrow urlHints to a subtree (e.g. critical routes upgrade prefetchpreload).
  7. ssroutput.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.
  8. font-preloadmodule.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.
  9. noneoutput.resourceHints: "none". Hard off switch: no <link> anywhere and empty stats / manifest. (false only disables the auto chunk hints; URL-asset hints keep firing.)
  10. url-hints-globaloutput.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.
  11. async-css-preloadmodule.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).
  12. auto-preconnectoutput.resourceHints.preconnect. Emit <link rel="preconnect"> for a cross-origin output.publicPath origin (the CDN serving your bundles / assets).
  13. object-form — every knob in one object: { initial, urlHints, preconnect, modulePreloadPolyfill, manifest }.
  14. csp-no-polyfillmodulePreloadPolyfill: false. The polyfill default is derived from output.environment.modulePreload; opt out under a strict CSP that forbids inline scripts.
  15. async-js-css-preloadmodule.parser.javascript.dynamicImportPreload. Couples JS and CSS of an async chunk (Vite parity) — contrast with scenario 11's CSS-only dynamicImportCssPreload.
  16. esm-default — nothing set. ESM output (output.module) enables initial by default, so initial chunks get <link rel="modulepreload"> with no resourceHints config.

webpack.config.js

javascript
_{{webpack.config.js}}_

src/routes/home.js

javascript
_{{src/routes/home.js}}_

src/routes/home-with-assets.js

javascript
_{{src/routes/home-with-assets.js}}_

src/routes/home-with-css.js

javascript
_{{src/routes/home-with-css.js}}_

src/styles/app.css

css
_{{src/styles/app.css}}_

SSR: the manifest file

With output.resourceHints.manifest: "ssr-hints.json", webpack writes the manifest for you during the build — no stats plumbing needed:

js
// 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:

js
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:

js
const escape = (s) => String(s).replace(/"/g, "&quot;");

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>`);
});

Async chunks

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.

Precedence

Highest wins:

  1. Per-URL magic comment (webpackPreload: true, webpackAs: "font", …)
  2. module.parser.<type>.urlHints rule match
  3. Nothing (no hint emitted for that URL)

For 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.

Info

webpack output

_{{stdout}}_