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.
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.
// 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.
Step-by-step verification
-
Check the emitted filenames. Every chunk in the output directory should carry a readable name. A numeric filename means a missing or misplaced comment.
-
Confirm the comment is inside the parentheses. A comment above the import statement is ignored without warning.
-
Verify grouping took effect. Modules sharing a name should appear in one file. If they did not merge, check
splitChunkscache groups before assuming the comment failed. -
Verify placeholder expansion. A dynamic path with
[request]should emit one file per resolved target, not one combined file. -
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.
-
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.
Related
- Dynamic Import Patterns for On-Demand Loading β the parent guide on the import patterns these comments annotate
- Conditionally Importing Polyfills at Runtime β a sibling pattern that relies on named chunks for diagnosis
- Choosing splitChunks Cache Groups for Shared Modules β the configuration-level grouping that can override a name
- Prefetch and Preload Strategies for Critical Routes β when the loading hints are worth adding