JavaScript skill 5 of 12
Dynamic Import and Code Splitting SKILL.md: Fix Failed Chunk Loads
The Dynamic Import and Code Splitting skill makes Claude, Cursor or another AI agent find and fix failed chunk loads, stale chunks after a deploy, JSON imports without attributes, cache-busting import URLs and React.lazy set up without Suspense or an error boundary. A scanner lists each pattern by file and line, and the agent edits only flagged lines.
dynamic-import-code-splitting/SKILL.md, version 1.0.0, 3,948 words, plus scripts/import_check.py and two reference files.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 7 November 2026. Updated 7 November 2026. Free to use under CC BY 4.0.
An agent works on dynamic imports in this order: decide what is worth loading late, run the scanner, fix the hard errors (JSON attributes, a then export), handle load failure at every import() site, deal with stale chunks after a deploy, fix the React lazy setup, tidy specifiers so the bundler can split them, remove top-level await, then build and test a blocked chunk. The scanner reads text. It cannot see the bundler's output, so it reports patterns and the agent confirms them against a real build.
Reusable code is in references/patterns.md. Availability notes and the rival comparison are in references/appendix.md.
When to use this skill, and when not to
Use it when a lazy-loaded route or widget shows a blank screen after a release, Failed to fetch dynamically imported module or Loading chunk failed appears in reports, a dynamic import does not shrink the main bundle, import("./data.json") throws, or a code review touches import() or React.lazy.
| Request | Use instead |
|---|---|
Catch blocks, error classes, cause, global handlers | javascript-error-handling.md |
Floating promises, Promise.all and friends | promises-async-await.md |
| Cancelling an in-flight load | abortcontroller-cancellation.md |
| Long tasks while a module evaluates | yield-to-main-thread.md |
| LCP, INP or CLS diagnosis from field data | lcp-optimisation.md, inp-optimisation.md, cls-optimisation.md |
| Bundler configuration beyond chunk naming and preload hooks (Rollup, webpack plugins, module federation) | Out of scope. Use the bundler's documentation |
| Service worker caching strategies | Out of scope |
Guardrails
- Propose, then change. Report first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
- Deploy and cache changes are not the agent's to make. Changing
Cache-Controlheaders, retention of old assets or CDN rules affects production. Write the recommendation. A person applies it. - Never run the user's code against live services to test a fix. Use a local build, the project's tests or a test environment.
- Data handled. The scanner prints file, line, rule ID and a fixed message, never source text. Chunk URLs and error payloads can hold tokens or user ids in query strings. Do not paste them into prompts or reports. Strip query strings before logging a failed import.
- Nothing is invented. Every rule carries a source and a date. Bundler and browser behaviour differs by version and carries verify before relying on this. Size budgets come from the user, not the agent.
- Refuses to: add a reload handler without a once-only guard, add
Date.now()cache-busting to animport()URL, split above-the-fold content to make a bundle number look smaller, or declare a fix verified without a build and a blocked-chunk test.
Inputs the agent must have before starting
Ask for any that are missing. Do not guess.
| Input | If missing, ask |
|---|---|
| Target files or directory | "Which folder should I scan? I will skip node_modules, dist and build." |
| Bundler and framework | "Vite, webpack, Next.js or no bundler? React, Vue or plain?" |
| Performance budget | "What is the initial JavaScript budget, or the LCP target? I will not choose one." |
| Deploy model | "After a release, do old hashed files stay on the server? For how long?" |
| Build and test commands | "How do I run a production build and the tests?" |
| Browser targets | "Which browsers must work? Failed-import retry differs by browser." |
Raw SKILL.md file
This is the exact file agents read, including the credit header and licence notice. Save it as dynamic-import-code-splitting/SKILL.md.
---
name: dynamic-import-code-splitting
description: "Audits and fixes dynamic import() and code splitting in JavaScript: failed chunk loads and stale chunks after a deploy, JSON imports without attributes, cache-busting import URLs, a 'then' export, top-level await import, React.lazy inside components or without Suspense and a boundary, and lazy loads that never split. Runs a scanner, then edits flagged lines only. Use for 'Failed to fetch dynamically imported module', 'Loading chunk failed', lazy loading, route splitting. Do NOT use for bundler config, service workers, or LCP field-data triage."
license: CC-BY-4.0
metadata:
author: Afeez Adebayo
author_url: https://afeezadebayo.com
author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
author_x: https://x.com/za_icon
source: https://afeezadebayo.com/skills-md/frontend-development/javascript/dynamic-import-code-splitting/
category: frontend-development
subcategory: javascript
version: "1.0.0"
created: "2026-11-07"
updated: "2026-11-07"
---
# Dynamic Import and Code Splitting: Load Late, Fail Safely
An agent works on dynamic imports in this order: decide what is worth loading late, run the scanner, fix the hard errors (JSON attributes, a `then` export), handle load failure at every `import()` site, deal with stale chunks after a deploy, fix the React `lazy` setup, tidy specifiers so the bundler can split them, remove top-level `await`, then build and test a blocked chunk. The scanner reads text. It cannot see the bundler's output, so it reports patterns and the agent confirms them against a real build.
Reusable code is in `references/patterns.md`. Availability notes and the rival comparison are in `references/appendix.md`.
## When to use this skill, and when not to
Use it when a lazy-loaded route or widget shows a blank screen after a release, `Failed to fetch dynamically imported module` or `Loading chunk failed` appears in reports, a dynamic import does not shrink the main bundle, `import("./data.json")` throws, or a code review touches `import()` or `React.lazy`.
| Request | Use instead |
|---|---|
| Catch blocks, error classes, `cause`, global handlers | `javascript-error-handling.md` |
| Floating promises, `Promise.all` and friends | `promises-async-await.md` |
| Cancelling an in-flight load | `abortcontroller-cancellation.md` |
| Long tasks while a module evaluates | `yield-to-main-thread.md` |
| LCP, INP or CLS diagnosis from field data | `lcp-optimisation.md`, `inp-optimisation.md`, `cls-optimisation.md` |
| Bundler configuration beyond chunk naming and preload hooks (Rollup, webpack plugins, module federation) | Out of scope. Use the bundler's documentation |
| Service worker caching strategies | Out of scope |
## Guardrails
1. **Propose, then change.** Report first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Deploy and cache changes are not the agent's to make.** Changing `Cache-Control` headers, retention of old assets or CDN rules affects production. Write the recommendation. A person applies it.
3. **Never run the user's code against live services to test a fix.** Use a local build, the project's tests or a test environment.
4. **Data handled.** The scanner prints file, line, rule ID and a fixed message, never source text. Chunk URLs and error payloads can hold tokens or user ids in query strings. Do not paste them into prompts or reports. Strip query strings before logging a failed import.
5. **Nothing is invented.** Every rule carries a source and a date. Bundler and browser behaviour differs by version and carries **verify before relying on this**. Size budgets come from the user, not the agent.
6. **Refuses to:** add a reload handler without a once-only guard, add `Date.now()` cache-busting to an `import()` URL, split above-the-fold content to make a bundle number look smaller, or declare a fix verified without a build and a blocked-chunk test.
## Inputs the agent must have before starting
Ask for any that are missing. Do not guess.
| Input | If missing, ask |
|---|---|
| Target files or directory | "Which folder should I scan? I will skip `node_modules`, `dist` and `build`." |
| Bundler and framework | "Vite, webpack, Next.js or no bundler? React, Vue or plain?" |
| Performance budget | "What is the initial JavaScript budget, or the LCP target? I will not choose one." |
| Deploy model | "After a release, do old hashed files stay on the server? For how long?" |
| Build and test commands | "How do I run a production build and the tests?" |
| Browser targets | "Which browsers must work? Failed-import retry differs by browser." |
## Workflow
### Step 1: Decide what to load late
Apply the table once per candidate. Do not split by habit.
| Candidate | Decision |
|---|---|
| A route the user reaches after the first screen | Split at the route. This is the default |
| A heavy widget behind a click (editor, chart, date picker, modal) | Split. Load on interaction, preload on hover or focus |
| Anything in the first screen or the likely LCP element | Do not split. A late load delays it |
| A small module | Do not split without measuring. A rival skill suggests that components under about 5 KB are not worth it. That figure is unsourced there, so treat it as a starting heuristic (skillselion lazy-loading-patterns, read 2026-11-07) |
| A module also imported statically anywhere in the main graph | It stays in the main bundle. Remove the static import or the split |
| Code needed only in some environments (polyfills, a locale) | Split, behind a feature check |
MDN states the trade-off: static imports suit initial dependencies because they allow static analysis and tree shaking. Dynamic `import()` suits deferred or conditional loading (MDN `import()`, read 2026-11-07).
### Step 2: Run the scanner
```bash
python3 scripts/import_check.py <paths>
```
Read only its output. Do not read the script. Each line is `path:line: ID level message`. Exit code 1 means errors, or warnings with `--strict`.
| ID | Level | Meaning | Fix in step |
|---|---|---|---|
| D1 | error | JSON `import()` with no `with` attribute | 3 |
| D7 | error | Module exports a name `then` | 3 |
| D2 | warn | Floating `import()`: not awaited, returned or caught | 4 |
| D11 | info | `await import()` outside a `try` | 4 |
| P1 | warn | `import()` used, no preload-error or chunk-error handler in scanned files | 5 |
| P2 | warn | `lazy()` used, no error boundary in scanned files | 6 |
| D5 | warn | `lazy()` inside a function | 6 |
| D6 | warn | `lazy()` in a file with no `Suspense` | 6 |
| D3 | warn | Cache-busting `import()` URL | 7 |
| D4 | info | Non-literal specifier | 7 |
| D9 | info | Relative `import()` without extension in a `.mjs` or `.cjs` file | 7 |
| D10 | warn | Same module imported statically and dynamically in one file | 1, 7 |
| D8 | info | Top-level `await import()` | 8 |
The scanner is text-based. P1 and P2 look only at the scanned paths, so scan the whole source tree or ignore them. D10 sees one file at a time: a static import in another file also defeats the split and the scanner cannot show it.
If the scanner reports more than 40 lines, fix errors first, then one rule ID at a time. Read only the reported lines plus 10 lines either side.
### Step 3: Fix the hard errors (D1, D7)
1. **D1.** JSON modules need an import attribute: `import("./data.json", { with: { type: "json" } })` (MDN). Node requires `type: "json"` for JSON modules and supports only the `type` attribute. Import attributes stopped being experimental in Node v23.1.0, v22.12.0, v20.18.3 and v18.20.5 (Node ESM docs, read 2026-11-07). **Verify before relying on this** if production runs an older Node.
2. **D7.** A module that exports `then()` is called during promise resolution when imported dynamically (MDN). Rename the export. If it is a public API, wrap it in an object and export that.
### Step 4: Handle load failure at the import site (D2, D11)
`import()` always returns a promise and never throws synchronously. It rejects when fetching fails (a `TypeError` in browsers), when parsing or linking fails (typically `SyntaxError`), or when the module or a dependency throws during evaluation (MDN).
Retry depends on the failure, because the module map caches some failures:
| Failure | Retry? | Source |
|---|---|---|
| Fetch failed (network, HTTP error) | Yes. The HTML standard requires failed fetches to be removed from the module map. The HTTP cache may still hand back a cached error response | MDN |
| Evaluation threw | No. Later imports return the same error without re-running the module | MDN |
| Parse error | No. It stays in the module map, so a corrected file is not re-fetched until reload | MDN |
| Older browsers without retriable loading | No. They cache the failure, so only a page reload helps | Vite docs, read 2026-11-07 |
Rules:
1. Every `import()` on a user-triggered path is awaited inside a `try`, or ends in `.catch`. A floating `import()` (D2) turns a failed load into an unhandled rejection, which `javascript-error-handling.md` step 7 covers.
2. On fetch failure retry once, after a short wait chosen by the user. If it fails again, show a message with a retry button. Do not loop.
3. Never retry an evaluation or parse failure. Report it with `cause` and show the fallback.
4. `return import(...)` and `() => import(...)` hand the promise to the caller. Check that the caller handles it. The scanner does not follow it.
5. Retry code is in `references/patterns.md`, section 1. Read it only when writing a loader.
### Step 5: Stale chunks after a deploy (P1)
A user keeps a tab open across a release. The old page asks for hashed chunk files that the new deploy removed, and the import fails with `Failed to fetch dynamically imported module`. Vite names version skew, poor networks and browser extensions as the usual causes (Vite troubleshooting, read 2026-11-07). The Netlify support guide describes the same failure with atomic deploys that delete previous files, and lists fixes that each trade complexity, user experience or stability (Netlify guide, 2024-02-26, read 2026-11-07). A webpack issue thread on `Loading chunk 0 failed` shows several unrelated causes and no confirmed root cause or maintainer fix, so do not assume one cause (webpack issue 5264, read 2026-11-07).
Apply in this order:
1. **Do not retry a chunk the deploy removed.** Retrying does not fix a permanently deleted file (Vite troubleshooting).
2. **Serve the HTML with `Cache-Control: no-cache`** so stale pages do not keep referencing old assets (Vite build docs, read 2026-11-07). Write this as a recommendation for the user.
3. **Keep old assets available for a period the user chooses.** The agent does not pick the number.
4. **Handle the event.** Vite emits `vite:preloadError` with `event.payload` holding the original error. `event.preventDefault()` stops the error being thrown. The docs show a reload as one response (Vite build docs). Use the once-only guard in `references/patterns.md`, section 2, so a missing chunk cannot cause a reload loop.
5. **Other bundlers.** webpack raises `ChunkLoadError`. Catch it at the import site (step 4). **Verify the error name for the bundler version in use.**
6. **Extensions blocking chunks.** Ad-blockers can block chunks by filename. Vite suggests changing chunk names with an output option (Vite troubleshooting). **Verify the option name for the Vite version in use**: the page read names it `build.rolldownOptions.output.chunkFileNames`.
If P1 fires on a project with no `import()` in user paths, ignore it.
### Step 6: React `lazy`, Suspense and boundaries (D5, D6, P2)
1. Define `lazy()` at module top level, never inside a component. Inside one, every render makes a new component type and the subtree remounts (rival skill rule, read 2026-11-07. **Verify against the React docs for `lazy`**).
2. Each lazy region needs a `Suspense` boundary with a fallback.
3. Suspense does not handle errors. Wrap lazy regions in an error boundary as well, or a failed chunk blanks the tree (skills.cat `implementing-code-splitting` lists this as missing in a rival, read 2026-11-07. Boundary behaviour: React reference, read 2026-11-07).
4. Named exports: `lazy(() => import("./x.js").then((m) => ({ default: m.X })))`.
5. Give the fallback the dimensions of the content it replaces. Check CLS in step 9.
6. Preload on hover or focus for routes the user is likely to take next. Do not preload everything: that restores the single large download.
D6 fires when the `Suspense` is in a parent file. Confirm that and move on.
### Step 7: Specifier hygiene (D3, D4, D9, D10)
1. **D3.** `import("/x.js?t=" + Date.now())` forces a fresh module, but the engine cannot garbage-collect the old namespaces, which leaks memory in a long-lived app (MDN). Remove the query. If code must reload, reload the page.
2. **D4.** A non-literal specifier is legal (MDN), but a bundler may be unable to analyse it. Replace it with an explicit map: `const loaders = { a: () => import("./a.js"), b: () => import("./b.js") }`. If the specifier must stay dynamic, add the bundler's ignore comment (`/* @vite-ignore */` or `/* webpackIgnore: true */`) after confirming the files are served as-is. **Verify per bundler.**
3. **D9.** Node ESM needs a file extension on relative specifiers, and directory imports need `./dir/index.js` (Node ESM docs).
4. **D10.** If a module is imported statically anywhere in the main graph, `import()` of it does not create a separate chunk (build tools report this: **verify in your bundler's output**). Pick one.
5. Importing the same module through `import()` and `import * as m` gives `===` namespace objects (MDN). Do not rely on a second import re-running side effects.
### Step 8: Top-level `await` (D8)
Top-level `await import()` blocks evaluation of the module and everything that imports it, so it defeats the lazy load. In Node, an unsettled top-level `await` ends the process with exit code 13, and CommonJS `require()` can load only ES modules without top-level `await` (Node ESM docs). Move the `await` into a function called on demand. From CommonJS, use `await import()` inside an async function (Node ESM docs).
### Step 9: Build and test a blocked chunk
1. Run the production build. Compare the initial bundle with the budget the user gave. The agent reports numbers from the build output and does not estimate them.
2. In the browser, open the network panel, load the page, and confirm the split chunk is requested only when its trigger happens.
3. Block the chunk (DevTools "block request URL", or a test-runner route abort: `references/patterns.md`, section 4), trigger it, and confirm the user sees the fallback and a retry, not a blank screen.
4. Simulate a deploy: change the chunk name, keep the old page open, trigger the import, confirm the handler reloads once.
5. Re-run the scanner. Errors must be zero.
Report with the template below. Do not echo unchanged code.
## Worked example
Input: `app.js`, 31 lines in the original, 40 in the fixed version. Scanner run on the original:
```
app.js:3: D1 error JSON import() without { with: { type: "json" } }
app.js:3: D11 info await import() outside try: confirm a caller handles the rejection
app.js:3: D8 info top-level await import(): blocks module evaluation, defeats lazy loading
app.js:6: D11 info await import() outside try: confirm a caller handles the rejection
app.js:11: D4 info non-literal specifier: bundler may not split it; check build output
app.js:15: D3 warn cache-busting import() URL: every call makes a new module that is never freed
app.js:15: D4 info non-literal specifier: bundler may not split it; check build output
app.js:19: D2 warn floating import(): not awaited, returned or caught, so a failed load is an unhandled rejection
app.js:23: D10 warn same module imported statically and dynamically in this file: no split
app.js:26: D7 error module exports a name 'then': import() will call it as a thenable
app.js:29: D5 warn lazy() created inside a function: a new component type on every render
app.js:29: D6 warn lazy() in a file with no Suspense: confirm a parent provides one
(project): P1 warn import() used but no preloadError or ChunkLoadError handler in scanned files
(project): P2 warn lazy() used but no error boundary in scanned files
scanned 1 files: 7 import() calls, 2 errors, 7 warnings
```
Decisions: line 3 is a JSON file loaded at start, so a static `import ... with { type: "json" }` replaces the top-level await (D1, D8, D11). Line 6 is a click path, so it gets `try` with `cause` (D11). Line 11 keeps a dynamic specifier on purpose: the plugin files are served as-is, so it gets `@vite-ignore` (D4). Line 15 loses its `Date.now()` query (D3). Line 19 gets `.catch` (D2). Line 23 duplicates a static import, so the dynamic import is removed (D10). Line 26 renames the export (D7). Line 29 moves `lazy()` to module level and gains `Suspense` and a boundary (D5, D6, P2). A `vite:preloadError` handler with a once-only guard is added (P1).
Second run on the fixed version: `scanned 1 files: 4 import() calls, 0 errors, 0 warnings`, exit 0. The first run exits 1.
Result report (illustrative: the call sites and test counts are invented for the example):
```
Fixed 14 of 14 flagged items in app.js. Scanner: 2 errors, 7 warnings -> 0, 0.
Build: not run, no command given. Needs a person: add Cache-Control: no-cache to the HTML response.
Behaviour change: the plugin loader still resolves at runtime, so those files are not split.
```
## Templates
### Report
```
Scope: <paths>, <bundler>, <framework>
Scanner: <errors> errors, <warnings> warnings -> <errors>, <warnings>
| file:line | ID | decision | fix |
Build: initial bundle <before> -> <after> (from build output, or "not run")
Blocked-chunk test: <fallback shown | not run>
Needs a person: <headers, retention, CDN rules>
```
### Starting prompt
```
Read dynamic-import-code-splitting.md. Scan <paths> for dynamic import and
lazy-loading problems. Bundler: <name>. Budget: <initial JS size or LCP target>.
Run the scanner and report findings as file, line, ID and proposed fix.
Do not edit until I approve a batch. Do not change cache headers or deploy settings.
```
Code templates (retry loader, preload-error handler, lazy route, blocked-chunk test) are in `references/patterns.md`. Read it only when a step names a template.
## Failure modes and anti-patterns
| # | Failure | How to spot it | Do instead |
|---|---|---|---|
| 1 | A lazy load that never splits | D10, D4. Build output shows the module in the main chunk | Remove the static import, or use an explicit loader map. Confirm in the build |
| 2 | Blank screen after a release | P1, D2. Reports show `Failed to fetch dynamically imported module` from tabs opened before a deploy | `Cache-Control: no-cache` on HTML, keep old assets, `vite:preloadError` or a catch with a once-only reload |
| 3 | Retry loops | A handler that reloads or retries with no guard, or retries an evaluation error | Retry fetch failures once. Never retry parse or evaluation failures. Guard reloads with a flag |
| 4 | Splitting the first screen or tiny modules | LCP gets worse after the change. Many small requests in a waterfall | Step 1 table. Measure before and after |
| 5 | Surprises from `then`, `Date.now()` and top-level `await` | D7, D3, D8 | Step 3, step 7 and step 8 |
## Quality bar
Run against the output before handing over. Each item is a check, not an opinion.
1. Scanner exit code is 0, or each remaining warning is justified in the report.
2. Every `import()` on a user path is in a `try` with `await`, ends in `.catch`, or is returned to a caller named in the report.
3. No `import()` URL contains `Date.now()`, `Math.random()` or a `t=` query.
4. No module exports a name `then`.
5. Every `lazy()` is at module level and sits under a `Suspense` and an error boundary.
6. Every reload handler has a once-only guard (`grep -n "preloadError"` and read the matching lines).
7. The report quotes initial bundle numbers from the build output, or says "not run".
8. The blocked-chunk test was run or listed as not run, with the reason.
9. Cache-header and retention changes appear under "Needs a person", not in the diff.
10. The diff touches only flagged lines and the files needed for the handler.
## Cost
Figures are estimates from this skill's own count on the final files (words × 1.35 for prose, characters ÷ 3.5 for code and tables). No tokeniser could be installed in the build environment, so none is measured. Rival figures are estimates from the visible text of pages I read.
| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 111 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 5,542 | Estimate | When the skill fires |
| C. `references/appendix.md` | 772 | Estimate | Only for availability notes or the rival comparison |
| C. `references/patterns.md` | 1,017 | Estimate | Only when a step names a template or code block |
| Script output (typical run) | 278 | Estimate | Scanner output on the worked example (1385 characters) |
| D. Typical run (A + B + script output) | 5,931 | Estimate | Common path, no references |
| D. Worst case (A + B + both references + 40 lines of output) | 8,202 | Estimate | Large input, every branch |
| E. Cheaper path | 5,931 | Estimate | Run the scanner, fix only flagged lines, skip both references |
Rival bodies I could size run from about 340 to about 1,890 tokens. This skill costs more to load. In return the scanner finds the patterns so the agent does not read every file to look for them, and the failure handling is in the skill, not left to the agent.
## Handoffs
`javascript-error-handling.md` covers what to do with the error once an import rejects: causes, logging and global handlers. `promises-async-await.md` covers floating promises. `yield-to-main-thread.md` covers long tasks while a loaded module runs. The marketing skills `lcp-optimisation.md` and `javascript-seo.md` cover field-data diagnosis and rendering. This skill does not repeat them.
## Appendix: tool-specific notes
Kept short. Version details, support notes and the rival comparison are in `references/appendix.md`. Read it only for availability notes or the rival comparison.
| Tool | Note |
|---|---|
| Vite | `vite:preloadError` event, `event.payload`, `preventDefault()`. Retriable module loading depends on the browser |
| webpack | `ChunkLoadError`. Magic comments such as `webpackIgnore`. **Verify against the version in use** |
| Node | `with { type: "json" }`. Extensions mandatory. `require()` of ESM only for modules without top-level `await` |
| React | `lazy`, `Suspense`, class-component error boundaries |
| Service workers and worklets | `import()` rejects there (MDN). Use static imports |
## Sources
Last verified 7 November 2026 for every source.
- MDN, import(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
- Node.js docs, ECMAScript modules: https://nodejs.org/api/esm.html
- Vite docs, Building for production: https://vite.dev/guide/build.html
- Vite docs, Troubleshooting: https://vite.dev/guide/troubleshooting.html
- Netlify support guide, code-splitting issues (26 February 2024): https://answers.netlify.com/t/support-guide-handling-code-splitting-issues-on-netlify/113158
- webpack issue 5264, Loading chunk failed: https://github.com/webpack/webpack/issues/5264
- React reference, Component (error boundaries): https://react.dev/reference/react/Component
- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- Rival, skillselion lazy-loading-patterns: https://skillselion.com/skills/yonatangross/orchestkit/lazy-loading-patterns
- Rival, skills.cat implementing-code-splitting: https://skills.cat/skills/djankies/claude-configs/implementing-code-splitting
- Rival, openskillindex js-dynamic-import (metadata only): https://openskillindex.com/skills/intense-visions-harness-engineering-js-dynamic-import-bd5432
- Adjacent, cloudfront esm-module-patterns: https://d8uzp82hd82jh.cloudfront.net/agent-skills/esm-module-patterns-2
---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/javascript/dynamic-import-code-splitting/
Download: https://afeezadebayo.com/wp-content/uploads/skills-md/frontend-development/javascript/dynamic-import-code-splitting/SKILL.md
Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.
Workflow
Step 1: Decide what to load late
Apply the table once per candidate. Do not split by habit.
| Candidate | Decision |
|---|---|
| A route the user reaches after the first screen | Split at the route. This is the default |
| A heavy widget behind a click (editor, chart, date picker, modal) | Split. Load on interaction, preload on hover or focus |
| Anything in the first screen or the likely LCP element | Do not split. A late load delays it |
| A small module | Do not split without measuring. A rival skill suggests that components under about 5 KB are not worth it. That figure is unsourced there, so treat it as a starting heuristic (skillselion lazy-loading-patterns, read 2026-11-07) |
| A module also imported statically anywhere in the main graph | It stays in the main bundle. Remove the static import or the split |
| Code needed only in some environments (polyfills, a locale) | Split, behind a feature check |
MDN states the trade-off: static imports suit initial dependencies because they allow static analysis and tree shaking. Dynamic import() suits deferred or conditional loading (MDN import(), read 2026-11-07).
Step 2: Run the scanner
python3 scripts/import_check.py <paths>Read only its output. Do not read the script. Each line is path:line: ID level message. Exit code 1 means errors, or warnings with --strict.
| ID | Level | Meaning | Fix in step |
|---|---|---|---|
| D1 | error | JSON import() with no with attribute | 3 |
| D7 | error | Module exports a name then | 3 |
| D2 | warn | Floating import(): not awaited, returned or caught | 4 |
| D11 | info | await import() outside a try | 4 |
| P1 | warn | import() used, no preload-error or chunk-error handler in scanned files | 5 |
| P2 | warn | lazy() used, no error boundary in scanned files | 6 |
| D5 | warn | lazy() inside a function | 6 |
| D6 | warn | lazy() in a file with no Suspense | 6 |
| D3 | warn | Cache-busting import() URL | 7 |
| D4 | info | Non-literal specifier | 7 |
| D9 | info | Relative import() without extension in a .mjs or .cjs file | 7 |
| D10 | warn | Same module imported statically and dynamically in one file | 1, 7 |
| D8 | info | Top-level await import() | 8 |
The scanner is text-based. P1 and P2 look only at the scanned paths, so scan the whole source tree or ignore them. D10 sees one file at a time: a static import in another file also defeats the split and the scanner cannot show it.
If the scanner reports more than 40 lines, fix errors first, then one rule ID at a time. Read only the reported lines plus 10 lines either side.
Step 3: Fix the hard errors (D1, D7)
- D1. JSON modules need an import attribute:
import("./data.json", { with: { type: "json" } })(MDN). Node requirestype: "json"for JSON modules and supports only thetypeattribute. Import attributes stopped being experimental in Node v23.1.0, v22.12.0, v20.18.3 and v18.20.5 (Node ESM docs, read 2026-11-07). Verify before relying on this if production runs an older Node. - D7. A module that exports
then()is called during promise resolution when imported dynamically (MDN). Rename the export. If it is a public API, wrap it in an object and export that.
Step 4: Handle load failure at the import site (D2, D11)
import() always returns a promise and never throws synchronously. It rejects when fetching fails (a TypeError in browsers), when parsing or linking fails (typically SyntaxError), or when the module or a dependency throws during evaluation (MDN).
Retry depends on the failure, because the module map caches some failures:
| Failure | Retry? | Source |
|---|---|---|
| Fetch failed (network, HTTP error) | Yes. The HTML standard requires failed fetches to be removed from the module map. The HTTP cache may still hand back a cached error response | MDN |
| Evaluation threw | No. Later imports return the same error without re-running the module | MDN |
| Parse error | No. It stays in the module map, so a corrected file is not re-fetched until reload | MDN |
| Older browsers without retriable loading | No. They cache the failure, so only a page reload helps | Vite docs, read 2026-11-07 |
Rules:
- Every
import()on a user-triggered path is awaited inside atry, or ends in.catch. A floatingimport()(D2) turns a failed load into an unhandled rejection, whichjavascript-error-handling.mdstep 7 covers. - On fetch failure retry once, after a short wait chosen by the user. If it fails again, show a message with a retry button. Do not loop.
- Never retry an evaluation or parse failure. Report it with
causeand show the fallback. return import(...)and() => import(...)hand the promise to the caller. Check that the caller handles it. The scanner does not follow it.- Retry code is in
references/patterns.md, section 1. Read it only when writing a loader.
Step 5: Stale chunks after a deploy (P1)
A user keeps a tab open across a release. The old page asks for hashed chunk files that the new deploy removed, and the import fails with Failed to fetch dynamically imported module. Vite names version skew, poor networks and browser extensions as the usual causes (Vite troubleshooting, read 2026-11-07). The Netlify support guide describes the same failure with atomic deploys that delete previous files, and lists fixes that each trade complexity, user experience or stability (Netlify guide, 2024-02-26, read 2026-11-07). A webpack issue thread on Loading chunk 0 failed shows several unrelated causes and no confirmed root cause or maintainer fix, so do not assume one cause (webpack issue 5264, read 2026-11-07).
Apply in this order:
- Do not retry a chunk the deploy removed. Retrying does not fix a permanently deleted file (Vite troubleshooting).
- Serve the HTML with
Cache-Control: no-cacheso stale pages do not keep referencing old assets (Vite build docs, read 2026-11-07). Write this as a recommendation for the user. - Keep old assets available for a period the user chooses. The agent does not pick the number.
- Handle the event. Vite emits
vite:preloadErrorwithevent.payloadholding the original error.event.preventDefault()stops the error being thrown. The docs show a reload as one response (Vite build docs). Use the once-only guard inreferences/patterns.md, section 2, so a missing chunk cannot cause a reload loop. - Other bundlers. webpack raises
ChunkLoadError. Catch it at the import site (step 4). Verify the error name for the bundler version in use. - Extensions blocking chunks. Ad-blockers can block chunks by filename. Vite suggests changing chunk names with an output option (Vite troubleshooting). Verify the option name for the Vite version in use: the page read names it
build.rolldownOptions.output.chunkFileNames.
If P1 fires on a project with no import() in user paths, ignore it.
Step 6: React lazy, Suspense and boundaries (D5, D6, P2)
- Define
lazy()at module top level, never inside a component. Inside one, every render makes a new component type and the subtree remounts (rival skill rule, read 2026-11-07. Verify against the React docs forlazy). - Each lazy region needs a
Suspenseboundary with a fallback. - Suspense does not handle errors. Wrap lazy regions in an error boundary as well, or a failed chunk blanks the tree (skills.cat
implementing-code-splittinglists this as missing in a rival, read 2026-11-07. Boundary behaviour: React reference, read 2026-11-07). - Named exports:
lazy(() => import("./x.js").then((m) => ({ default: m.X }))). - Give the fallback the dimensions of the content it replaces. Check CLS in step 9.
- Preload on hover or focus for routes the user is likely to take next. Do not preload everything: that restores the single large download.
D6 fires when the Suspense is in a parent file. Confirm that and move on.
Step 7: Specifier hygiene (D3, D4, D9, D10)
- D3.
import("/x.js?t=" + Date.now())forces a fresh module, but the engine cannot garbage-collect the old namespaces, which leaks memory in a long-lived app (MDN). Remove the query. If code must reload, reload the page. - D4. A non-literal specifier is legal (MDN), but a bundler may be unable to analyse it. Replace it with an explicit map:
const loaders = { a: () => import("./a.js"), b: () => import("./b.js") }. If the specifier must stay dynamic, add the bundler's ignore comment (/* @vite-ignore */or/* webpackIgnore: true */) after confirming the files are served as-is. Verify per bundler. - D9. Node ESM needs a file extension on relative specifiers, and directory imports need
./dir/index.js(Node ESM docs). - D10. If a module is imported statically anywhere in the main graph,
import()of it does not create a separate chunk (build tools report this: verify in your bundler's output). Pick one. - Importing the same module through
import()andimport * as mgives===namespace objects (MDN). Do not rely on a second import re-running side effects.
Step 8: Top-level await (D8)
Top-level await import() blocks evaluation of the module and everything that imports it, so it defeats the lazy load. In Node, an unsettled top-level await ends the process with exit code 13, and CommonJS require() can load only ES modules without top-level await (Node ESM docs). Move the await into a function called on demand. From CommonJS, use await import() inside an async function (Node ESM docs).
Step 9: Build and test a blocked chunk
- Run the production build. Compare the initial bundle with the budget the user gave. The agent reports numbers from the build output and does not estimate them.
- In the browser, open the network panel, load the page, and confirm the split chunk is requested only when its trigger happens.
- Block the chunk (DevTools "block request URL", or a test-runner route abort:
references/patterns.md, section 4), trigger it, and confirm the user sees the fallback and a retry, not a blank screen. - Simulate a deploy: change the chunk name, keep the old page open, trigger the import, confirm the handler reloads once.
- Re-run the scanner. Errors must be zero.
Report with the template below. Do not echo unchanged code.
Worked example
Input: app.js, 31 lines in the original, 40 in the fixed version. Scanner run on the original:
app.js:3: D1 error JSON import() without { with: { type: "json" } }
app.js:3: D11 info await import() outside try: confirm a caller handles the rejection
app.js:3: D8 info top-level await import(): blocks module evaluation, defeats lazy loading
app.js:6: D11 info await import() outside try: confirm a caller handles the rejection
app.js:11: D4 info non-literal specifier: bundler may not split it; check build output
app.js:15: D3 warn cache-busting import() URL: every call makes a new module that is never freed
app.js:15: D4 info non-literal specifier: bundler may not split it; check build output
app.js:19: D2 warn floating import(): not awaited, returned or caught, so a failed load is an unhandled rejection
app.js:23: D10 warn same module imported statically and dynamically in this file: no split
app.js:26: D7 error module exports a name 'then': import() will call it as a thenable
app.js:29: D5 warn lazy() created inside a function: a new component type on every render
app.js:29: D6 warn lazy() in a file with no Suspense: confirm a parent provides one
(project): P1 warn import() used but no preloadError or ChunkLoadError handler in scanned files
(project): P2 warn lazy() used but no error boundary in scanned files
scanned 1 files: 7 import() calls, 2 errors, 7 warningsDecisions: line 3 is a JSON file loaded at start, so a static import ... with { type: "json" } replaces the top-level await (D1, D8, D11). Line 6 is a click path, so it gets try with cause (D11). Line 11 keeps a dynamic specifier on purpose: the plugin files are served as-is, so it gets @vite-ignore (D4). Line 15 loses its Date.now() query (D3). Line 19 gets .catch (D2). Line 23 duplicates a static import, so the dynamic import is removed (D10). Line 26 renames the export (D7). Line 29 moves lazy() to module level and gains Suspense and a boundary (D5, D6, P2). A vite:preloadError handler with a once-only guard is added (P1).
Second run on the fixed version: scanned 1 files: 4 import() calls, 0 errors, 0 warnings, exit 0. The first run exits 1.
Result report (illustrative: the call sites and test counts are invented for the example):
Fixed 14 of 14 flagged items in app.js. Scanner: 2 errors, 7 warnings -> 0, 0.
Build: not run, no command given. Needs a person: add Cache-Control: no-cache to the HTML response.
Behaviour change: the plugin loader still resolves at runtime, so those files are not split.Templates
Report
Scope: <paths>, <bundler>, <framework>
Scanner: <errors> errors, <warnings> warnings -> <errors>, <warnings>
| file:line | ID | decision | fix |
Build: initial bundle <before> -> <after> (from build output, or "not run")
Blocked-chunk test: <fallback shown | not run>
Needs a person: <headers, retention, CDN rules>Starting prompt
Read dynamic-import-code-splitting.md. Scan <paths> for dynamic import and
lazy-loading problems. Bundler: <name>. Budget: <initial JS size or LCP target>.
Run the scanner and report findings as file, line, ID and proposed fix.
Do not edit until I approve a batch. Do not change cache headers or deploy settings.Code templates (retry loader, preload-error handler, lazy route, blocked-chunk test) are in references/patterns.md. Read it only when a step names a template.
Failure modes and anti-patterns
| # | Failure | How to spot it | Do instead |
|---|---|---|---|
| 1 | A lazy load that never splits | D10, D4. Build output shows the module in the main chunk | Remove the static import, or use an explicit loader map. Confirm in the build |
| 2 | Blank screen after a release | P1, D2. Reports show Failed to fetch dynamically imported module from tabs opened before a deploy | Cache-Control: no-cache on HTML, keep old assets, vite:preloadError or a catch with a once-only reload |
| 3 | Retry loops | A handler that reloads or retries with no guard, or retries an evaluation error | Retry fetch failures once. Never retry parse or evaluation failures. Guard reloads with a flag |
| 4 | Splitting the first screen or tiny modules | LCP gets worse after the change. Many small requests in a waterfall | Step 1 table. Measure before and after |
| 5 | Surprises from then, Date.now() and top-level await | D7, D3, D8 | Step 3, step 7 and step 8 |
Quality bar
Run against the output before handing over. Each item is a check, not an opinion.
- Scanner exit code is 0, or each remaining warning is justified in the report.
- Every
import()on a user path is in atrywithawait, ends in.catch, or is returned to a caller named in the report. - No
import()URL containsDate.now(),Math.random()or at=query. - No module exports a name
then. - Every
lazy()is at module level and sits under aSuspenseand an error boundary. - Every reload handler has a once-only guard (
grep -n "preloadError"and read the matching lines). - The report quotes initial bundle numbers from the build output, or says "not run".
- The blocked-chunk test was run or listed as not run, with the reason.
- Cache-header and retention changes appear under "Needs a person", not in the diff.
- The diff touches only flagged lines and the files needed for the handler.
Cost
Figures are estimates from this skill's own count on the final files (words × 1.35 for prose, characters ÷ 3.5 for code and tables). No tokeniser could be installed in the build environment, so none is measured. Rival figures are estimates from the visible text of pages I read.
| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 111 | Estimate | Every session with the skill installed |
B. SKILL.md body | 5,542 | Estimate | When the skill fires |
C. references/appendix.md | 772 | Estimate | Only for availability notes or the rival comparison |
C. references/patterns.md | 1,017 | Estimate | Only when a step names a template or code block |
| Script output (typical run) | 278 | Estimate | Scanner output on the worked example (1385 characters) |
| D. Typical run (A + B + script output) | 5,931 | Estimate | Common path, no references |
| D. Worst case (A + B + both references + 40 lines of output) | 8,202 | Estimate | Large input, every branch |
| E. Cheaper path | 5,931 | Estimate | Run the scanner, fix only flagged lines, skip both references |
Rival bodies I could size run from about 340 to about 1,890 tokens. This skill costs more to load. In return the scanner finds the patterns so the agent does not read every file to look for them, and the failure handling is in the skill, not left to the agent.
Handoffs
javascript-error-handling.md covers what to do with the error once an import rejects: causes, logging and global handlers. promises-async-await.md covers floating promises. yield-to-main-thread.md covers long tasks while a loaded module runs. The marketing skills lcp-optimisation.md and javascript-seo.md cover field-data diagnosis and rendering. This skill does not repeat them.
Appendix: tool-specific notes
Kept short. Version details, support notes and the rival comparison are in references/appendix.md. Read it only for availability notes or the rival comparison.
| Tool | Note |
|---|---|
| Vite | vite:preloadError event, event.payload, preventDefault(). Retriable module loading depends on the browser |
| webpack | ChunkLoadError. Magic comments such as webpackIgnore. Verify against the version in use |
| Node | with { type: "json" }. Extensions mandatory. require() of ESM only for modules without top-level await |
| React | lazy, Suspense, class-component error boundaries |
| Service workers and worklets | import() rejects there (MDN). Use static imports |
Sources
Last verified 7 November 2026 for every source.
- MDN, import(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
- Node.js docs, ECMAScript modules: https://nodejs.org/api/esm.html
- Vite docs, Building for production: https://vite.dev/guide/build.html
- Vite docs, Troubleshooting: https://vite.dev/guide/troubleshooting.html
- Netlify support guide, code-splitting issues (26 February 2024): https://answers.netlify.com/t/support-guide-handling-code-splitting-issues-on-netlify/113158
- webpack issue 5264, Loading chunk failed: https://github.com/webpack/webpack/issues/5264
- React reference, Component (error boundaries): https://react.dev/reference/react/Component
- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- Rival, skillselion lazy-loading-patterns: https://skillselion.com/skills/yonatangross/orchestkit/lazy-loading-patterns
- Rival, skills.cat implementing-code-splitting: https://skills.cat/skills/djankies/claude-configs/implementing-code-splitting
- Rival, openskillindex js-dynamic-import (metadata only): https://openskillindex.com/skills/intense-visions-harness-engineering-js-dynamic-import-bd5432
- Adjacent, cloudfront esm-module-patterns: https://d8uzp82hd82jh.cloudfront.net/agent-skills/esm-module-patterns-2
What this skill does
A dynamic import fails in more ways than a static one. import() always returns a promise, so a failed load becomes an unhandled rejection unless something catches it. MDN says a failed fetch can be retried, but an evaluation error is cached and a parse error stays in the module map, so a blanket retry is wrong. After a release, a tab that is still open asks for chunk files the new deploy deleted.
The skill first decides what is worth splitting: routes and heavy widgets yes, the first screen no. It then runs scripts/import_check.py. The agent fixes JSON attributes and a then export, handles each failure by type, adds a once-only reload guard for stale chunks, moves lazy() under Suspense and an error boundary, and lists cache headers for a person to apply.
Who it is for
Use it if you are a frontend developer on Vite, webpack or a framework built on them, a reviewer of AI-generated lazy loading, or the owner of an app that shows a blank screen after each release. It fits when reports contain Failed to fetch dynamically imported module or Loading chunk failed, when a dynamic import does not shrink the main bundle, or when import("./data.json") throws. It does not cover bundler configuration, service workers or field-data diagnosis of LCP.
What is inside
The file is 3,948 words (version 1.0.0) with one script and two reference files. It holds:
- Six guardrails, including never changing cache headers or deploy settings and never adding a reload handler without a once-only guard.
- Six inputs the agent must have first, including the performance budget and the deploy model.
- A nine-step workflow, from deciding what to load late through failure handling to a blocked-chunk test.
- One tool,
scripts/import_check.py, a standard-library scanner with thirteen codes that prints file, line and a fixed message, never source text. - A worked example with real output: 2 errors, 7 warnings and 5 info lines before, none after.
- A report template, a starting prompt, five failure modes and a ten-item quality bar.
- Two reference files, read only when a step points to them: code templates, and availability notes with the rival comparison.
| Item | Tokens | Status | Read when |
|---|---|---|---|
| Description | 111 | Estimate | Every session with the skill installed |
SKILL.md body | 5,542 | Estimate | When the skill fires |
references/patterns.md | 1,017 | Estimate | Only when a step names a template or code block |
references/appendix.md | 772 | Estimate | Only for availability notes or the rival comparison |
| Typical run | 5,931 | Estimate | Description, body and scanner output |
| Worst case | 8,202 | Estimate | Both references and 40 lines of output |
| Cheaper path | 5,931 | Estimate | Run the scanner, fix flagged lines, skip the references |
The figures come from the skill’s own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. The rival bodies I could size have about 340 to 1,890 tokens by the same method. Of the two I could read in full, neither ships a scanner or handles stale chunks after a deploy.
How to install and use it in Claude, Cursor and other agents
Claude. Keep the folder as dynamic-import-code-splitting/ with SKILL.md, scripts/ and references/ inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ or .claude/skills/ in a project.
Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the script beside your code. The scanner needs Python 3 and nothing else. Then send the starting prompt from the Templates section.
Before you install. Read the script first. It reads files and writes nothing. The skill tells the agent to edit only on a branch in batches you approve, never to run your code against live services, never to paste chunk URLs or error payloads into prompts, and never to change cache headers or deploy settings. Install skills only from sources you trust.
A worked example
An invented 31-line file, app.js, loads JSON with a top-level await import(), awaits an editor import outside any try, appends Date.now() to a module URL, fires an import with .then and no catch, imports one module both statically and dynamically, exports a function named then and creates React.lazy inside a component. The scanner prints 2 errors, 7 warnings and 5 info lines.
The agent turns the JSON load into a static import with an attribute, wraps the editor import in try with cause, removes the timestamp, adds .catch, drops the duplicate import, renames the export, moves lazy() under Suspense and a boundary, and adds a vite:preloadError handler with a once-only guard. The re-run prints 0 errors and 0 warnings.
Common mistakes
The skill is built around five ways this goes wrong:
- A lazy load that never splits. The module is also imported statically, so it stays in the main bundle. The skill removes one of the two and checks the build.
- A blank screen after a release. Old tabs ask for deleted chunks. The skill adds a handler with a once-only reload and recommends a cache header.
- Retry loops. Retrying an evaluation error returns the same cached error. The skill retries fetch failures once and nothing else.
- Splitting the first screen. A late load delays what the visitor sees first. The skill splits routes and heavy widgets only.
- Surprises from
then,Date.now()and top-level await. Each one has a rule and a scanner code.
Related skills
JavaScript Error Handling covers what to do with the error once an import rejects: causes, logging and global handlers. Promises and Async/Await covers floating promises, which is where an uncaught failed import starts. Yield to the Main Thread covers long tasks while a loaded module runs. For field data, see JavaScript SEO.
Frequently asked questions
How do I fix Failed to fetch dynamically imported module?
The usual cause is version skew: a tab opened before a deploy asks for hashed chunks the new build removed. Retrying does not fix a deleted file. Serve the HTML with Cache-Control: no-cache, keep old assets for a while, and handle vite:preloadError with a once-only reload. The skill checks each part.
Can I retry a failed dynamic import in JavaScript?
Sometimes. MDN says failed fetches are removed from the module map, so a network failure can be retried. An evaluation error is cached and returns the same error, and a parse error stays in the module map. Older browsers cache any failure and need a reload.
Why does import() of a JSON file throw?
JSON modules need an import attribute: import(“./data.json”, { with: { type: “json” } }). Import attributes stopped being experimental in Node 23.1.0, 22.12.0, 20.18.3 and 18.20.5, according to the Node ESM docs, so check your production version. The scanner flags JSON imports without the attribute.
Why does my lazy-loaded module still land in the main bundle?
Usually the same module is imported statically somewhere in the main graph, so the bundler keeps it in the main chunk. A non-literal specifier can also stop splitting. Remove the static import or use an explicit loader map, then read the build output to confirm.
Is it safe to add a timestamp to a dynamic import URL?
No. MDN says a query such as ?t=Date.now() forces a fresh module, but the engine cannot garbage-collect the old namespaces, so long-running apps can leak memory. Remove the query. If code must reload, reload the page. The scanner flags it.
Does React.lazy need an error boundary as well as Suspense?
Yes. Suspense shows a fallback while the chunk loads but does not handle a failed load, so a rejected import can blank the tree. Wrap each lazy region in an error boundary and define lazy() at module level. Check the React docs for lazy for your version.
How many tokens does the Dynamic Import and Code Splitting skill use?
The description is about 111 tokens and loads in every session. The SKILL.md body is about 5,542 tokens when the skill fires. A typical run is about 5,931 tokens with scanner output, the cheaper path is the same, and the worst case with both references is about 8,202. All figures are estimates, not from a model tokeniser.
Changelog
- 1.0.0, 7 November 2026: first release.
Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.
