LEARN · DEBUGGING GUIDE

Webpack Code Splitting: Lazy-Loaded Chunk Fails to Load

Your lazy-loaded chunk 1.chunk.js returns 404 or never fires the load event. This guide shows exactly why—and how to fix it—without the dogma.

AdvancedBuild tools8 min read

What this usually means

The core issue is that webpack's runtime cannot resolve the URL of the split chunk at load time. This happens when the `publicPath` configuration is incorrect or relative paths break because of a mismatch between the build-time expectation and runtime deployment. Another common variant is that the chunk file was not emitted (missing entry in compilation), or the chunk filename template produces collisions under content hashing. The webpack runtime uses `__webpack_public_path__` to construct the URL; if that is wrong, every chunk request fails.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Open browser DevTools Network tab, filter by 'chunk' or 'js'. Look for the failing chunk request and note its full URL and response status.
  • 2In the console, check for `TypeError: __webpack_require__.e is not a function` — that indicates a broken runtime or multiple webpack runtimes on the page.
  • 3Compare the failing chunk URL against the actual file location on your CDN or server. If they differ, `publicPath` is the culprit.
  • 4Check the webpack output in `dist/` — does the chunk file exist? Use `ls -la dist/*.js` to confirm. If missing, your split point might be dead-code eliminated.
  • 5Run `webpack --profile --json > stats.json` and inspect the `chunks` array to see if the chunk is recorded and has asset names.
  • 6If using dynamic `import()` with a template literal (e.g., `import(./locales/${lang}.json)`), verify the expression is statically analyzable; otherwise the chunk won't be emitted.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchwebpack.config.js — specifically `output.publicPath`, `output.chunkFilename`, and `output.filename`
  • searchThe HTML template (e.g., index.html, server-rendered HTML) — check the `<script>` tag that loads the main bundle and the `__webpack_public_path__` assignment if any
  • searchBrowser DevTools → Sources tab → Page — see if the chunk file appears in the file tree
  • searchNetwork tab response headers for the chunk request — look for `content-type`, `content-length`, and `cache-control`
  • searchServer logs (nginx, Apache, CDN) — 404s for chunk requests
  • searchwebpack stats JSON (`stats.json`) — chunk IDs, assets, and modules per chunk
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warning`output.publicPath` is set to an absolute path (e.g., '/static/') but the app is deployed under a subpath (e.g., '/myapp/')
  • warning`output.publicPath` is empty or './' and the main bundle is served from a different directory than the chunks
  • warningDynamic import uses a non-static expression (e.g., `import(variable)`) that webpack cannot analyze, so it never creates a chunk
  • warningMultiple webpack runtimes exist on the same page (e.g., two separate bundles each with their own bootstrap), leading to conflicting chunk loaders
  • warningContent hash in chunk filename changes between builds but old chunk URLs are cached in service workers or CDN
  • warningThe chunk file was deleted or not uploaded during deployment (missing from build artifact)
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildSet `output.publicPath` to a relative path like `auto` or `''` so webpack resolves chunks relative to the current script URL. Use `__webpack_public_path__ = window.location.origin + '/'` dynamically if needed.
  • buildEnsure all dynamic `import()` expressions are statically analyzable: use string literals or `import(`./folder/${name}`)` only if the folder is known and name is a simple variable that webpack can enumerate.
  • buildIf using Module Federation or multiple entry points, make sure only one instance of webpack runtime exists, or configure `uniqueName` to avoid runtime collisions.
  • buildAdd `output.chunkLoadingGlobal` (or `output.jsonpFunction` in older webpack) with a unique name to prevent multiple runtimes from interfering.
  • buildFor cache issues, use content hashes in chunk filenames and set aggressive cache headers on CDN, but also implement chunk-level cache invalidation via versioned URLs.
  • buildIf chunks are missing from build, verify that the dynamic import is not inside a dead code path or conditional that webpack can't evaluate. Use `/* webpackChunkName: "myChunk" */` to force named chunks.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedClear browser cache and reload the page, then navigate to the lazy-loaded route. Confirm the chunk loads with a 200 status.
  • verifiedIn the Network tab, check that the chunk URL matches the actual deployment path (no double slashes, correct base).
  • verifiedRun `curl -I https://yourdomain.com/static/js/1.chunk.js` to verify the chunk is accessible from the server.
  • verifiedCheck console for any webpack runtime errors after the fix — none should appear.
  • verifiedDeploy a new version with a changed chunk content to ensure cache busting works (the new hash forces a fresh download).
  • verifiedUse the webpack `--bail` flag to ensure build fails if any chunk is missing, then integrate into CI pipeline.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDo not set `publicPath` to `'/'` if your app is served from a subdirectory — chunks will be requested from root and fail with 404.
  • warningAvoid using `document.write` or other synchronous script injection to load chunks — it may conflict with webpack's async loading.
  • warningDo not manually delete or rename chunk files in the output directory — always rebuild.
  • warningDo not ignore the `TypeError: __webpack_require__.e is not a function` error — it's not a red herring, it means two runtimes are fighting.
  • warningDo not use `import()` with a completely dynamic string like `import(someVar)` without a `/* webpackInclude */` or `/* webpackExclude */` comment — webpack will not create chunks.
  • warningDo not rely on `publicPath: 'auto'` blindly; test on your exact deployment environment because 'auto' uses `document.currentScript` which fails in some legacy setups.
( 07 )War story

Chunk 404 after migrating to React Router v6 with lazy loading

Senior Frontend Engineerwebpack 5, React 18, TypeScript, AWS CloudFront + S3, code-splitting via React.lazy + Suspense

Timeline

  1. 09:15Deploy new React Router v6 setup with lazy routes using React.lazy(() => import('./pages/About'))
  2. 09:20User reports blank screen when navigating to /about. Console shows 'Loading chunk 4 failed.'
  3. 09:25Check Network tab: chunk requested from https://app.example.com/static/js/4.chunk.js returns 404
  4. 09:30Inspect S3 bucket: chunk file exists at /static/js/4.chunk.js but app is deployed at /app/ subpath
  5. 09:35webpack.config.js has output.publicPath: '/' hardcoded. Should be '/app/' or 'auto'.
  6. 09:40Change publicPath to 'auto'. Rebuild and deploy.
  7. 09:45Test /about route: chunk loads correctly, page renders.
  8. 09:50Add e2e test that navigates to all lazy routes and checks for 404s.

We had just migrated our React app from old-school bundles to code splitting with React.lazy. Everything worked in dev (webpack-dev-server serves from root). But in production, our S3 bucket served the app under the /app/ prefix via CloudFront. The publicPath was set to '/' — so webpack runtime requested chunks from root, not /app/. Classic.

I spent 20 minutes checking that the chunk was actually uploaded (it was), verifying CloudFront behaviors (pass-through), and even clearing CDN cache. It wasn't until I compared the requested URL vs the S3 key that the mismatch hit me. The chunk file was at /app/static/js/4.chunk.js but the request was for /static/js/4.chunk.js.

The fix was changing publicPath to 'auto'. That tells webpack to compute the base URL from the current script's src attribute. Since the main bundle was at /app/static/js/main.js, chunks resolved relative to that. After redeploy, the chunk loaded correctly. We added a CI check that validates publicPath matches the deployment path.

Root cause

Hardcoded `publicPath: '/'` in webpack config mismatched the actual deployment subpath '/app/'.

The fix

Changed `output.publicPath` to `'auto'` so webpack resolves chunk URLs relative to the current script's location.

The lesson

Always use `auto` for publicPath unless you are absolutely certain of the deployment path at build time. And never assume dev and prod paths match.

( 08 )How webpack resolves chunk URLs at runtime

When webpack generates a dynamic import, it creates a separate chunk file and injects a loading function into the main bundle's runtime. That runtime uses `__webpack_require__.p` (which is set from `output.publicPath`) to construct the absolute URL of the chunk. For example, if `publicPath` is '/static/', the chunk URL becomes '/static/js/1.chunk.js'.

If `publicPath` is 'auto', webpack uses `document.currentScript` or the `src` of the `<script>` tag that loaded the main bundle. This is robust for SPAs served from any path. However, `auto` can break in some edge cases like when using module workers or if the script is loaded via a blob URL. In those cases, you may need to set `__webpack_public_path__` dynamically before the app initializes.

( 09 )Static analysis of dynamic imports: why some chunks never appear

Webpack relies on static analysis to determine which modules to split into chunks. For `import('./folder/' + name + '.js')`, webpack cannot determine the exact file at build time, so it creates a context module that includes all matching files. But if the expression is completely dynamic like `import(someVar)`, webpack creates no chunk because it cannot enumerate possibilities.

To force chunk creation for dynamic paths, use `/* webpackInclude: /\.js$/ */` or `/* webpackPreload: true */` comments. Also, ensure the dynamic part is a simple variable or string literal. In one case, a developer used `import(path)` where `path` was built from user input — webpack emitted zero chunks. The fix was to use a static map of possible paths.

( 10 )Multiple runtimes: when chunks fail because of conflicting loaders

If a page loads two independent webpack bundles (e.g., a widget and the main app), each has its own runtime with a global `window.webpackChunk` array. If both bundles use the default `webpackChunk` name, they can overwrite each other's chunk loading queue. The symptom is `__webpack_require__.e` being undefined or a chunk being loaded but never executed.

Fix: Set a unique `output.chunkLoadingGlobal` (or `output.jsonpFunction` in webpack 4) for each bundle. For Module Federation, ensure `uniqueName` is set and that the shared runtime is correctly configured. I've debugged an incident where two micro-frontends collided and one couldn't load its chunks until we added `chunkLoadingGlobal: 'webpackChunkApp1'`.

( 11 )Cache-busting and chunk versioning in production

Content hashes in chunk filenames (e.g., `[name].[contenthash].js`) ensure that updated chunks have new URLs. But if your CDN or service worker caches the old chunk URL, the new one may never be requested. Worse, if the chunk URL is hardcoded in a service worker's cache list, you must update it.

Best practice: Use content hashes, set `Cache-Control: max-age=31536000, immutable` on chunk files, and use a versioned path like `/v1/static/js/`. For service workers, implement a cache-first strategy with versioned cache names and clean up old caches on activate.

( 12 )Debugging chunk loading with webpack bundle analyzer and stats

Run `webpack-bundle-analyzer` on your stats JSON to visualize which modules end up in which chunks. If a dynamic import is supposed to create a separate chunk but the analyzer shows it merged into the main bundle, you have a static analysis issue.

Alternatively, inspect the stats JSON manually: `jq '.chunks[] | {id, files, modules: [.modules[].name]}' stats.json`. This shows each chunk's ID, output files, and constituent modules. If a module you expected in a separate chunk appears in the main chunk, check the import statement.

Frequently asked questions

What does 'Loading chunk x failed' mean in the console?

It means the webpack runtime tried to fetch chunk x but the HTTP request returned an error (typically 404). The most common cause is a mismatch between `publicPath` and the actual deployment path. Check the URL requested and compare to where the chunk file actually resides.

Why does my chunk load in development but not production?

In development, webpack-dev-server serves everything from the root with no subpath, so `publicPath: '/'` works. In production, your app may be deployed under a subpath (e.g., `/myapp/`). If `publicPath` is not updated, chunks are requested from the wrong base. Use `publicPath: 'auto'` to handle both environments.

Can I use `__webpack_public_path__` to dynamically set the path?

Yes. You can set `__webpack_public_path__` before any dynamic imports are executed. For example, `__webpack_public_path__ = window.location.origin + '/myapp/';`. This overrides the build-time `publicPath`. It's useful when the deployment path is only known at runtime.

My chunk loads but the component doesn't render — no errors. What gives?

This usually means the chunk was loaded but the dynamic import promise resolved to an unexpected module. Check that the default export is a React component. Also, ensure you are using `React.lazy(() => import('./MyComponent'))` correctly — the import must return a module that has a default export. If you use `export default` somewhere else by mistake, the lazy component will be undefined.

Why does webpack create multiple chunks with same name?

If you don't use content hashes, all chunks might be named `[id].chunk.js` with sequential IDs. If you rebuild and the module order changes, IDs can shift, causing old cached chunks to be requested with wrong content. Always use `[contenthash]` in `chunkFilename`. For example, `chunkFilename: '[name].[contenthash].js'`.