Using webpackChunkName Magic Comments Effectively

Open the Network panel on a route transition and read what arrived:

Name        Status  Type    Size     Time
479.js      200     script  84.2 kB  310 ms
812.js      200     script  41.7 kB  180 ms
93.js       200     script  12.4 kB   90 ms

Three numeric ids. Nothing in that output says which is the route, which is a shared dependency, and which is a widget nobody asked for. The same opacity carries into analyzer treemaps, into field measurement keyed on filenames, and into every conversation about which chunk regressed.

Chunk names fix this, and they do more than label: reusing one name across several imports is the most direct control the dynamic import patterns for on-demand loading give you over what travels together.

Root cause: unnamed chunks get numeric ids

Every import() creates a chunk. Without a name, Webpack emits it with an id derived from the module graph’s internal numbering, which is stable only as long as the graph is β€” add a module and the ids shift. That instability is why unnamed chunks make poor keys for any measurement that spans releases, and it is the same churn problem that deterministic chunk hashing for long-term caching addresses at the hash level.

A magic comment attaches a name at the call site, before optimisation runs:

// routes.js β€” Webpack 5: every dynamic import carries a name
const Dashboard = () => import(/* webpackChunkName: "route-dashboard" */ './routes/Dashboard');
const Settings  = () => import(/* webpackChunkName: "route-settings" */  './routes/Settings');
const Billing   = () => import(/* webpackChunkName: "route-billing" */   './routes/Billing');
route-dashboard.4f2a91.js   200  script  84.2 kB  310 ms
route-settings.9c31de.js    200  script  41.7 kB  180 ms
vendor-charts.71b0aa.js     200  script  12.4 kB   90 ms

The comment must sit inside the import() parentheses, before the specifier. Placed above the statement it is an ordinary comment and is silently ignored β€” which is the single most common reason a name appears not to work.

Shared Names Merge Chunks Two import calls annotated with the same chunk name are emitted as one file; two calls with different names are emitted as two files fetched independently. Same name β€” one file import(Editor) name: "editor" import(Toolbar) name: "editor" editor.[hash].js one request, both modules Different names β€” two files import(Editor) name: "editor" import(Toolbar) name: "toolbar" editor.[hash].js toolbar.[hash].js The name is a grouping instruction, not just a label

Grouping deliberately

Because a shared name merges chunks, naming is how you express β€œthese always load together.” An editor and its toolbar, a chart and its tooltip plugin, a wizard’s four steps β€” each set is better as one request than as four.

Group by Co-Usage, Not by Folder Modules that are always used together belong in one chunk; a rarely-opened export dialog kept in the same chunk would be paid for by every editor user. name: "editor" β€” merge EditorCore Toolbar Shortcuts β€” always used together name: "export-dialog" β€” separate opened by 6% of editor sessions merging it would tax the other 94% A shared name is a merge instruction β€” use it only across a real usage boundary
// editor/index.js β€” three modules, one chunk, one request
export const loadEditor = () => Promise.all([
  import(/* webpackChunkName: "editor" */ './EditorCore'),
  import(/* webpackChunkName: "editor" */ './EditorToolbar'),
  import(/* webpackChunkName: "editor" */ './EditorShortcuts'),
]);

Group when the modules are always used together and separately from everything else. Do not group across usage boundaries: merging a rarely-opened export dialog into the editor chunk means every editor user downloads the dialog. The merging decision is the same trade-off as a splitChunks cache group, described in choosing splitChunks cache groups for shared modules, expressed at the call site instead of in configuration.

Naming imports built from a variable

An import with an interpolated path resolves to many possible files, so a single fixed name would collapse them all into one chunk. The [request] placeholder expands to the resolved path segment instead:

// locale-loader.js β€” one readable chunk per resolved file
export const loadLocale = (code) =>
  import(/* webpackChunkName: "locale-[request]" */ `./locales/${code}.js`);
// emits locale-en-GB.[hash].js, locale-fr.[hash].js, …

Without the placeholder, every locale ends up in one chunk and the entire point of loading one at a time is lost β€” the mechanics of that context expansion are covered in shrinking bundled locale and timezone data.

Loading hints: use sparingly

Two further comments change when the browser fetches a chunk rather than what it is called.

// Fetched during idle time after the parent chunk loads β€” for likely-next navigation.
const Settings = () => import(/* webpackChunkName: "route-settings", webpackPrefetch: true */ './Settings');

// Fetched in parallel with the parent chunk, at high priority β€” for code needed almost immediately.
const Critical = () => import(/* webpackChunkName: "critical-widget", webpackPreload: true */ './CriticalWidget');

Prefetch is usually safe: it uses idle time and low priority. Preload is not β€” it competes with the parent chunk for bandwidth on the critical path, and applying it to something that is not genuinely needed at first paint makes the page slower. The scheduling trade-offs are covered in prefetch and preload strategies for critical routes.

The Vite equivalent

Rollup names chunks from the entry module, so Vite ignores these comments entirely. Naming and grouping move into configuration:

// vite.config.js β€” Vite 5+: names and grouping decided centrally
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        chunkFileNames: 'assets/[name]-[hash].js',
        manualChunks(id) {
          // The grouping a shared webpackChunkName would express, as a rule.
          if (id.includes('/src/editor/')) return 'editor';
          if (id.includes('/src/locales/')) return `locale-${id.split('/').pop().replace('.js', '')}`;
        },
      },
    },
  },
});

Leaving the Webpack comments in a shared codebase is harmless β€” Vite skips them β€” but they have no effect there, and relying on them in a Vite build produces silently unnamed chunks.

Where the Naming Decision Lives Webpack reads a comment at each import call site; Vite derives names from the entry module and a central configuration function. Webpack 5 β€” per call site import(/* webpackChunkName: "editor" */ …) decided where the import is written editor.[hash].js Vite 5+ β€” central configuration manualChunks(id) + chunkFileNames decided once, for the whole build editor-[hash].js Same outcome, opposite locus of control β€” comments do nothing in Vite

Step-by-step verification

  1. Check the emitted filenames. Every chunk in the output directory should carry a readable name. A numeric filename means a missing or misplaced comment.

  2. Confirm the comment is inside the parentheses. A comment above the import statement is ignored without warning.

  3. Verify grouping took effect. Modules sharing a name should appear in one file. If they did not merge, check splitChunks cache groups before assuming the comment failed.

  4. Verify placeholder expansion. A dynamic path with [request] should emit one file per resolved target, not one combined file.

  5. Check hint behaviour. A prefetched chunk should appear in the network panel as a low-priority request after load; a preloaded one competes with the initial chunks. If a prefetch is arriving at high priority, the wrong hint is applied.

  6. Confirm names in the analyzer. Reopen the treemap described in reading webpack-bundle-analyzer treemaps and confirm every large rectangle is identifiable by name.

Edge cases and gotchas

Names colliding across features. Two unrelated features both naming a chunk utils will merge, producing a chunk that neither team expects. Prefix names by feature.

Minifiers stripping comments. A transform that removes comments before Webpack sees the module β€” an aggressive loader, or TypeScript with comment removal enabled β€” deletes the annotation. Check the loader chain if names disappear after a build-tooling change.

Names with path separators. A name containing a slash creates nested output directories, which can break asset URL assumptions on some hosts.

Renaming a chunk invalidates its cache. The name is part of the filename, so renaming a chunk makes every returning user re-download it, even though the content is unchanged.

FAQ

Do chunk names affect caching or only readability?

Both, indirectly. The name becomes part of the filename, so it is stable across builds while the content hash changes β€” which is exactly what you want for cache behaviour and for reading a network panel. More importantly, giving two imports the same name merges them into one chunk, which changes what gets downloaded together and therefore how often a cached copy remains valid.

Why did two imports with the same chunk name not merge?

Almost always because splitChunks separated them afterwards. Chunk naming happens before optimisation, and a cache group with a higher priority can pull modules out of the named chunk into a shared one. The name still appears in the output, but its contents are not what you expect. Check the cache group configuration before concluding the comment was ignored.

Does Vite support magic comments?

It ignores the Webpack-specific ones, because Rollup names chunks from the entry module’s own path. The equivalent control is the chunk file naming pattern and the manual chunk assignment function, which decide names centrally in the build configuration rather than at each call site. Leaving the comments in place is harmless for portability β€” Vite skips them β€” but they do nothing there.