bundle-budget-code-splitting-review
Reviews JavaScript/CSS bundle composition against explicit numeric budgets, evaluates route- and component-level code-splitting boundaries, and requires a CI-enforced budget before endorsing any size fix as resolved.
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/bundle-budget-code-splitting-review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Bundle Budget & Code-Splitting Review
Purpose
Teams routinely respond to "the bundle feels big" with an ad hoc React.lazy() here and a dynamic import() there, ship it, and call the finding closed once the build succeeds. That proves nothing: it does not show whether the total byte weight moved, whether the change reduced or inflated the request count, or whether the win survives the next dependency bump. This skill turns "bundle feels big" into a numeric, device/network-scoped budget tied to INP/TTI, ranks analyzer output by both byte weight and main-thread execution cost, classifies each contributor into a specific remediation path, and refuses to mark a size finding resolved without a CI-enforced budget check — a one-time manual fix without an enforced budget is a regression waiting to happen.
When to use
Use this skill when the user asks to:
- reduce JavaScript or CSS bundle size,
- review a bundle-analyzer report (webpack-bundle-analyzer, rollup-plugin-visualizer, or equivalent stats output),
- set or enforce a performance budget for a route or shared entry,
- decide where to add route-level or component-level code splitting,
- assess the size impact of adding a new dependency before it merges.
When NOT to use
- Pure image/media asset optimization — different budget class (bytes-per-image, format/compression), different tooling; not this skill's concern.
- Server-side bundle or cold-start size for a serverless/edge function — different runtime, not the client-bundle budget this skill enforces.
- Deciding whether a chunk's contents are dead code once it is correctly split — hand off to
tree-shaking-dead-code-reviewfor that specific question; a chunk can be correctly split and still be full of unused exports, which is a distinct defect from a splitting-boundary defect. - Diagnosing which Core Web Vitals sub-phase is regressing before a cause is known — hand off to
core-web-vitals-triagefirst if the user only has a vague "it feels slow" complaint with no analyzer report yet; return here once JS weight is confirmed as the dominant contributor.
Context7 Documentation Protocol
Bundler chunking APIs are version-sensitive and have changed shape recently — do not prescribe a config from memorized training data.
- Call
ToolSearchwith query"context7"(or"select:mcp__Context7__resolve-library-id,mcp__Context7__query-docs") to load the Context7 tools if not already loaded this session. - Call
mcp__Context7__resolve-library-idfor the bundler in scope (Vite, webpack, Rollup, esbuild, Rolldown) before prescribing any chunking configuration. - Call
mcp__Context7__query-docsfor the specific mechanism — e.g. "manualChunks vs codeSplitting", "splitChunks cacheGroups", "dynamic import chunk naming" — before ruling on it. Do this per review; do not reuse a prior session's memory of bundler internals. - Known version-sensitive trap verified via Context7 as of this skill's
updateddate: Vite 8 (Rolldown-powered) removes the object form ofbuild.rollupOptions.output.manualChunksentirely and deprecates the function form; the documented replacement isbuild.rolldownOptions.output.codeSplittingwith agroupsarray ({ name, test }entries). Do not hand a Vite 8+ project amanualChunks: { vendor: [...] }object-form snippet — it will not apply. For Vite <8, function-formmanualChunks(id) { ... }is still valid but is itself deprecated and should be flagged as a forward-migration item, not endorsed as the long-term pattern. - webpack's chunking surface (
optimization.splitChunks.cacheGroups, dynamicimport()withwebpackChunkNamemagic comments) is comparatively stable across recent majors per Context7-grounded docs, but still confirm the installed webpack major before prescribing a specificcacheGroupsshape, since defaults (chunks: 'async'vs'all') affect which imports are eligible for splitting. - If Context7 is unavailable or returns no relevant match for the installed bundler, fall back to
official_docsand mark the claimdocumentation-based (Context7 unavailable)rather than presenting it as freshly verified. - Never invent a bundler config key, CLI flag, or plugin option that no queried source confirms.
Lean operating rules
- Establish the numeric budget before ranking anything. A budget with no device/network class and no percentile is not a budget — it is a guess. Refuse to close a finding against an undefined budget.
- Rank analyzer contributors by both parsed/gzipped byte size and main-thread execution cost; these produce different orderings, and execution cost correlates more directly with INP than raw byte size does. Report both, do not collapse them into one number.
- Classify every large module into exactly one path: needed on the critical path (keep, minimize), needed but deferrable (route/component-split candidate), duplicated across chunks (fix chunk-grouping config, not application code), or a heavier-than-necessary dependency (replace, or hand off to
tree-shaking-dead-code-reviewif the issue is unused exports rather than the dependency itself). - Treat over-splitting as a real regression, not just under-splitting. Every new chunk is a new request; if the aggregate request-overhead cost (connection reuse aside, still parse/eval/scheduling cost) offsets the byte savings, the split is net-negative. Require the byte-vs-request-count comparison before endorsing a split.
- Confirm the installed bundler major version via Context7 before prescribing
manualChunks,rolldownOptions.codeSplitting, orsplitChunkssyntax — see Context7 Documentation Protocol. Do not assume a memorized API is still current. - Never accept "it built without errors" as evidence a split is beneficial. Require a before/after byte comparison from the analyzer output, gzipped or brotli as configured for production.
- Flag any dynamic
import()whose module specifier is built from unsanitized user input as a code-injection risk, not merely a performance nit — this is a security-relevant finding, not a style note. - Do not recommend inlining third-party scripts to "save a request" without naming the CSP/subresource-integrity trade-off of inlined vs. externally loaded, SRI-checkable code.
- Require a CI-enforced budget (bundlesize, Lighthouse CI budget, or bundler-plugin size-limit check) as a condition of marking any size finding resolved. A manual one-time fix with no enforcement is not a fix, it is a snapshot.
References
Load these only when needed:
- Budget methodology — use when the user has no existing budget, or an existing budget lacks a device/network class or percentile, and one must be established or corrected.
- Bundler chunking APIs — use when prescribing or reviewing Vite, webpack, or Rollup chunking configuration; contains the version-sensitive API grounding.
- Code-splitting boundaries — use when deciding where to add route- or component-level splitting, or when diagnosing duplicated-dependency-across-chunks and over-splitting.
- CI enforcement and verification — use for every review to specify the CI-enforced budget mechanism, the verification command, and the adversarial checklist before closing the finding.
Response minimum
Return, at minimum:
- the numeric budget in scope (bytes, compression form, device/network class, percentile), stated explicitly or flagged as missing,
- the ranked contributor list with both byte size and main-thread execution-cost figures,
- per-item classification (keep / split / replace / dead-code-handoff / chunk-grouping-fix) with the concrete config diff, version-confirmed against the installed bundler major,
- the CI-enforced budget definition to add, not just a one-time manual fix,
- the verification command (
npm run build -- --reportor the project's equivalent analyzer invocation) and the request-count check confirming the split did not net-negative the change.
Files (vanguard-frontier-agentic)
-
references
-
budget-methodology.md 3.8 KB
# Budget Methodology Use this reference when the user has no existing performance budget, or an existing budget is missing a device/network class, a compression form, or a percentile — establish or correct it before ranking anything against it. ## What people get wrong The common bad assumption is: > "Keep the bundle small" is a budget. It is not. A budget with no number is a slogan. A number with no device/network class and no percentile is a coin flip about whose experience it describes. Per web.dev's performance-budget guidance, a usable budget names: 1. **the metric being bounded** — total JS/CSS transfer bytes, per-route transfer bytes, or a timing metric (TTI/INP) the byte budget is a proxy for, 2. **the compression form** — gzip or brotli, matching what production actually serves; raw/uncompressed bytes overstate the number users pay for, and mixing forms across comparisons invalidates the comparison, 3. **the device/network class** — a mid-tier mobile CPU on a throttled "good 4G" profile is a meaningfully different budget than an unthrottled desktop; web.dev's budget guidance frames budgets in terms of a target device class precisely because bundle-parse/execution cost scales with CPU, not just network time, 4. **the percentile** — p75 is the conventional anchor (it is also the percentile Core Web Vitals field assessment uses), not the best-case or median session. ## Establishing a budget when none exists 1. Ask (or infer from existing CI config, `lighthouserc`, `bundlesize.config.json`, or a `size-limit` entry) whether a budget already exists in any form. Do not assume none exists just because this review wasn't asked to check. 2. Anchor the budget to a route classification, not a single global number: - **critical path / shared entry** — loaded on every route; tightest budget, since it blocks first render everywhere. - **secondary route** — route-specific bundle loaded via code splitting; budget can be looser since it does not block other routes. - **vendor chunk** — third-party dependency weight; budget separately from application code so a dependency bump is caught distinctly from an application-code regression. 3. State the number in gzipped (or brotli, matching production) bytes, tied to p75 on the project's target device/network class. If the project has no stated target device/network class, default to a mid-tier mobile device on a throttled "good 4G" profile per web.dev's stated default budget context, and say explicitly that this default was assumed, not confirmed with the user. 4. Tie the byte budget back to a timing outcome (TTI or INP) where possible — a byte number with no timing justification is arbitrary. State the reasoning explicitly (e.g., "X KB of parse/execute cost on the target device class corresponds to roughly Y ms of main-thread blocking, which risks the INP budget"). ## Correcting an existing but underspecified budget If a budget exists but is missing a device/network class or a percentile, do not silently adopt it as sufficient. State the gap explicitly as a finding, propose the missing dimension using the defaults above, and note that the corrected budget needs sign-off before being treated as CI-enforceable — see [CI enforcement and verification](ci-enforcement-and-verification.md). ## Non-negotiables - Do not rank an analyzer report against a budget that has no stated compression form — a byte number with unstated compression is not comparable to anything. - Do not accept "under 250 KB" with no percentile or device class as a complete budget; treat it as a starting number that needs the missing dimensions filled in before enforcement. - Do not invent a budget number without grounding it in either the project's existing target (stated by the user, present in CI config) or web.dev's documented default budget context — state which one was used. -
bundler-chunking-apis.md 4.7 KB
# Bundler Chunking APIs Use this reference when prescribing or reviewing chunking configuration for Vite, webpack, or Rollup. Every claim below was confirmed against Context7-indexed official documentation as of this skill's `updated` date; re-verify against the installed bundler major before use — see the Context7 Documentation Protocol in `SKILL.md`. > Version note: bundler chunking surfaces are actively evolving (Vite's Rolldown migration in particular). Verify the installed major version before applying any snippet in this file to a real project. ## What people get wrong The naive story is: > "manualChunks is the Vite way to split vendor code, and that's stable." Wrong, as of Vite 8. Confirmed via Context7 against `/vitejs/vite`'s migration guide: the **object form** of `build.rollupOptions.output.manualChunks` is removed entirely in Vite 8 (Rolldown-powered), and the **function form** is deprecated. Handing a Vite 8+ project the classic object-form snippet produces a config that silently does not apply the intended grouping. ## Vite: manualChunks → codeSplitting **Deprecated / removed (pre-Vite-8 object form — removed in Vite 8):** ```javascript // Vite <8, object form — REMOVED in Vite 8 build: { rollupOptions: { output: { manualChunks: { vendor: ['react', 'vue'], }, }, }, }, ``` **Deprecated but still functional (function form, pre-Vite-8 and transitional):** ```javascript // Still works pre-Vite-8; deprecated, flag as a forward-migration item manualChunks(id) { if (id.includes('some-heavy-lib')) { return 'heavy-lib-chunk' } }, ``` **Current documented replacement (Vite 8+, Rolldown):** ```javascript build: { rolldownOptions: { output: { codeSplitting: { groups: [ { name: 'vendor', test: /[\\/]node_modules[\\/]/ }, ], }, }, }, } ``` Decision rule: confirm the installed Vite major first. - Vite 8+: use `rolldownOptions.output.codeSplitting.groups`. Do not offer the object-form `manualChunks` snippet — it does not apply. - Vite <8: function-form `manualChunks(id) {...}` is valid today but is a deprecated pattern; note the future migration to `codeSplitting` as a forward-looking item rather than presenting it as the long-term answer. ## webpack: splitChunks / cacheGroups Confirmed via Context7 against `/websites/webpack_js` guides (caching, printable, code-splitting). This surface is comparatively stable across recent majors, but `chunks` defaults and `cacheGroups` shape still depend on the installed major — confirm before prescribing. **Vendor chunk extraction:** ```javascript optimization: { runtimeChunk: 'single', splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', }, }, }, }, ``` Notes: - `chunks: 'all'` makes both synchronously and dynamically imported modules eligible for the cache group; the webpack default for `optimization.splitChunks.chunks` (when not explicitly set) is `'async'`, which only splits dynamically imported modules. Confirm which behavior the review target actually wants — a review that recommends `cacheGroups` without checking the effective `chunks` mode can silently miss the synchronous-import case. - Merging all of `node_modules` into a single `vendors` chunk (as shown) is explicitly called out in webpack's own guidance as not generally recommended for typical apps — it produces a single large chunk that invalidates entirely on any dependency bump. Prefer scoping `cacheGroups` to specific heavy dependencies over a single monolithic vendor bucket unless the project's caching strategy specifically wants long-term vendor-chunk stability at the cost of granularity. **Dynamic import as a split point:** ```javascript function onClick() { import("./module") .then((module) => module.default) .catch((err) => { console.log("Chunk loading failed") }) } ``` **Named dynamic-import chunk (magic comment):** ```javascript import( /* webpackChunkName: "app" */ "./app.jsx" ).then((App) => { // ... }) ``` Use `webpackChunkName` when the review needs a stable, human-readable chunk name for budget tracking across builds — an unnamed dynamic-import chunk gets a content-hashed name that is harder to track budget deltas against over time. ## Cross-bundler decision rule - Do not present a Rollup, Vite, or webpack chunking snippet as bundler-agnostic; the option names and defaults are not interchangeable, and copying a webpack `cacheGroups` shape into a Vite config (or vice versa) will not work. - Every chunking recommendation in a review must state which bundler and which major version it targets, resolved via Context7 for that specific project, not assumed from the most common current pattern in training data. -
ci-enforcement-and-verification.md 5 KB
# CI Enforcement and Verification Use this reference for every review to specify the CI-enforced budget mechanism, the verification command, and the adversarial checklist before a size finding can be closed. ## Non-negotiable: no finding closes without a CI-enforced budget A one-time manual fix — splitting a chunk, removing a dependency, adjusting `cacheGroups` — proves the bundle is smaller *today*. It proves nothing about tomorrow's dependency bump, an unreviewed import added six weeks from now, or a well-intentioned refactor that quietly reintroduces the same weight. Require one of the following, tied to the numeric budget established in [Budget methodology](budget-methodology.md), before marking any size finding resolved: - A `bundlesize`-style check (or equivalent size-limit tool) wired into CI that fails the build/PR when a named entry or chunk exceeds its budget. - A Lighthouse CI budget (`budgets.json` / `lighthouserc` budget assertions) that fails on `resource-summary` script/stylesheet byte-size thresholds. - A bundler-plugin size check (e.g., a Rollup/Vite/webpack plugin that asserts output size at build time) that fails the build rather than just reporting. If none of these exist in the project, state that as an explicit gap in the finding — do not let a review close as "fixed" when the only evidence is a single local `npm run build` byte count with no enforcement wired into CI. ## Verification command State the exact command used to produce the before/after comparison, matching the project's actual tooling rather than assuming a generic one: - `npm run build -- --report` (or the project's equivalent analyzer-invoking build script) to regenerate the analyzer report. - For Vite projects, confirm whether `rollup-plugin-visualizer` (or an equivalent) is wired into the build config; if not present, state that as a prerequisite gap before a byte-level review can be evidence-backed rather than estimated. - For webpack projects, confirm `webpack-bundle-analyzer` (or the `--json` stats output piped into an equivalent tool) is available. - Always compare gzipped (or brotli, matching production `Content-Encoding`) sizes, not raw/parsed sizes, unless the budget was explicitly defined in a different compression form — see [Budget methodology](budget-methodology.md). ## Adversarial checklist Before closing a bundle-size finding, answer these: - Is the budget tied to a percentile and a device/network class, or is it vague ("keep it small")? If vague, it is not a budget yet — fix that first. - Is the prescribed bundler chunking API confirmed against the installed major version via Context7, or is it a memorized snippet that might target a removed/deprecated API shape? - Does the proposed split reduce byte weight without increasing request count enough to net-negative the change? Was that comparison actually made, or only the byte number? - Is a CI-enforced budget check part of the deliverable, or only a one-time manual diff? - Was the ranking done by byte size alone, or also by main-thread execution cost? A large but rarely-executed module and a small but hot-path module do not carry equal INP risk. - If a dependency was flagged as "too heavy," was a lighter alternative actually identified with a size delta, or was "split it" offered as a substitute for that harder analysis? - If a dynamic `import()` specifier is built from any user-controlled input (route param, query string, feature flag value sourced externally), was that flagged as a code-injection risk, not filed only as a performance note? If any of these cannot be answered affirmatively, the finding is not ready to close — say so explicitly rather than presenting a partial analysis as complete. ## When to push back Push back if the user asks for: - a one-time size fix with no request to add or verify a CI budget check — explain why that guarantees regression. - inlining a third-party script "to save a request" without acknowledging the CSP/SRI trade-off. - splitting every component in a route "to be safe," without a request-count comparison — that is not caution, it is trading one unverified assumption for another. - a chunking config copied from a different bundler or an unconfirmed version, because "it's probably still the same API" — that is exactly the failure mode this skill's Context7 protocol exists to prevent. Those are not shortcuts. They convert a measurable problem into an unmeasured one. ## Handoff boundaries - If the root cause is unused exports inside an otherwise-necessary, correctly-split dependency: hand off to `tree-shaking-dead-code-review`. - If the user's starting complaint is a vague "it feels slow" with no analyzer report yet and JS weight has not been confirmed as the dominant contributor: hand off to `core-web-vitals-triage` first, then return here once bundle weight is confirmed as the cause. - If the fix under discussion is caching strategy for repeat visits rather than first-load byte weight: hand off to `service-worker-cache-strategy-review` (or the closest equivalent asset in scope) rather than stretching this skill's budget framing to cover caching. -
code-splitting-boundaries.md 5.9 KB
# Code-Splitting Boundaries Use this reference when deciding where to add route- or component-level splitting, or when diagnosing a duplicated-dependency-across-chunks finding or an over-splitting regression. ## What people get wrong The naive story is: > "More `import()` calls always make the bundle better." Wrong. Splitting trades parse/execute weight on one chunk for request-count and coordination overhead across chunks. Per web.dev's code-splitting guidance, the goal is to defer weight that is not needed for the current route/interaction — not to fragment the bundle for its own sake. Every additional chunk is an additional request the browser must schedule, and on a constrained connection or a cold cache, that overhead is real and can offset the byte savings the split was meant to deliver. ## Classification per contributor For each large module identified in the analyzer report, classify it into exactly one path: 1. **Needed on the critical path (keep, minimize).** The module is required to render the current route's above-the-fold content or its primary interaction. Do not split it away; instead look for a lighter alternative or confirm it is already tree-shaken (hand off to `tree-shaking-dead-code-review` if unused exports are suspected within an otherwise-necessary dependency). 2. **Needed but deferrable (route/component-split candidate).** The module is required somewhere in the app but not on the current route's critical render/interaction path — e.g., a charting library only used on a `/reports` route, a modal's rich-text editor only needed after a user action. Split via dynamic `import()` at the route or component boundary where it is actually invoked. 3. **Duplicated across chunks.** The same dependency (or a near-duplicate version) appears in the byte weight of more than one chunk. This is a chunk-grouping configuration defect, not an application-code defect — fix it via the bundler's vendor-chunk / `cacheGroups` / `codeSplitting.groups` configuration (see [Bundler chunking APIs](bundler-chunking-apis.md)), not by rewriting application imports. 4. **Heavier than necessary for its purpose.** The dependency itself is oversized relative to what the app uses from it (e.g., importing a full date-utility library for one format call). Recommend a lighter alternative and state the size delta; do not default to "just split it" when the real fix is not depending on the heavy library at all. ## Route-level vs. component-level splitting - **Route-level splitting** is the default first move: split at the router boundary so an entire route's code (including its route-specific dependencies) loads only when that route is navigated to. This is the highest-leverage split because it aligns with actual user navigation and typically yields one request per route transition, not per component. - **Component-level splitting** is for a specific heavy component *within* an already-loaded route that is not needed immediately — e.g., a modal, a below-the-fold widget, a rarely-used settings panel. Reach for this only after route-level splitting is exhausted; splitting every individual component inside an already-necessary route multiplies request count without changing what must load before the route is usable. - Do not recommend component-level splitting for something the user will interact with immediately on route entry — that just moves the same byte weight into a second network round trip with no benefit, and adds a loading-state flash that can itself become a CLS or perceived-jank complaint (hand off wording/attribution disagreements to `core-web-vitals-triage`). ## Over-splitting: a real regression, not just a missed optimization Before endorsing any new split point, require a request-count comparison alongside the byte comparison: - If a split reduces the shared/critical-path bundle by N KB but adds M new chunks that are each fetched on the same route transition (e.g., several components all lazy-loaded on the same screen, each producing its own round trip), the net effect can be worse: more connection/scheduling overhead, more parse/compile invocations, and a longer time to fully interactive — even though the "critical path" number looks smaller in isolation. - HTTP/2+ multiplexing reduces but does not eliminate this cost: each chunk still has its own parse, compile, and module-registration cost on the main thread, which is the more INP-relevant number per `core-web-vitals-triage`'s execution-cost framing. - The correct test is: does total time-to-interactive (or the relevant INP-adjacent metric) improve, not just does the critical-path byte number shrink. State this comparison explicitly in the finding; do not accept a smaller entry-chunk number alone as proof of improvement. ## Duplicated dependency across chunks — diagnosis path 1. Confirm via the analyzer report (most bundle-analyzer tools flag this directly, e.g. webpack-bundle-analyzer's treemap showing the same module path under multiple chunk nodes) that the same module resolves into more than one output chunk. 2. Check for version skew first — two different semver ranges of the same dependency resolving to two separate copies is an application-level (`package.json`/lockfile) defect, not a chunking-config defect; recommend a dependency dedupe/resolution fix, not a chunking change. 3. If it is genuinely one version pulled into multiple chunks because multiple entry points or route chunks each import it, this is the textbook case for a shared vendor chunk (webpack `cacheGroups`) or an equivalent shared-group rule (Vite `codeSplitting.groups`) — see [Bundler chunking APIs](bundler-chunking-apis.md). ## Non-negotiables - Do not recommend a component-level split for anything visible immediately on route entry. - Do not endorse a split without the request-count comparison alongside the byte comparison. - Do not treat a duplicated-dependency finding as an application-code problem before checking for version skew.
-
-
metadata.json 1.7 KB
{ "id": "bundle-budget-code-splitting-review", "name": "Bundle Budget & Code-Splitting Review", "type": "skill", "provider": "frontend", "harnesses": [ "codex", "claude-code", "cursor", "gemini", "kiro", "other" ], "summary": "Reviews JavaScript/CSS bundle composition against explicit numeric budgets, ranks analyzer contributors by byte weight and main-thread execution cost, evaluates route- and component-level code-splitting boundaries against version-confirmed bundler chunking APIs, and requires a CI-enforced budget before endorsing any size fix as resolved, loaded progressively.", "source_type": "original", "official_docs": [ "https://web.dev/articles/reduce-javascript-payloads-with-code-splitting", "https://web.dev/articles/your-first-performance-budget", "https://vite.dev/guide/build.html", "https://webpack.js.org/guides/code-splitting/", "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules/Dynamic_module_loading", "https://webpack.js.org/plugins/split-chunks-plugin/" ], "security_notes": "Do not recommend inlining third-party scripts to save a request without disclosing the CSP/subresource-integrity trade-off of inlined vs. externally loaded, SRI-checkable code. Flag any dynamic import() of a module specifier built from unsanitized user input as a code-injection risk, not merely a performance concern. Do not accept or echo any credential-shaped string found in a pasted build config or analyzer report as if it were safe to keep in the transcript.", "last_verified": "2026-07-02", "path": "skills/frontend/bundle-budget-code-splitting-review", "author": "github: VincentChuWaiChow", "version": "0.1.0" } -
SKILL.md 8.4 KB
--- name: bundle-budget-code-splitting-review description: Reviews JavaScript/CSS bundle composition against explicit numeric budgets, evaluates route- and component-level code-splitting boundaries, and requires a CI-enforced budget before endorsing any size fix as resolved. allowed-tools: Read Grep Glob Bash(npm run build:*) Bash(du:*) metadata: author: "github: VincentChuWaiChow" version: "0.1.0" updated: "2026-07-02" category: operational --- # Bundle Budget & Code-Splitting Review ## Purpose Teams routinely respond to "the bundle feels big" with an ad hoc `React.lazy()` here and a dynamic `import()` there, ship it, and call the finding closed once the build succeeds. That proves nothing: it does not show whether the total byte weight moved, whether the change reduced or inflated the request count, or whether the win survives the next dependency bump. This skill turns "bundle feels big" into a numeric, device/network-scoped budget tied to INP/TTI, ranks analyzer output by both byte weight and main-thread execution cost, classifies each contributor into a specific remediation path, and refuses to mark a size finding resolved without a CI-enforced budget check — a one-time manual fix without an enforced budget is a regression waiting to happen. ## When to use Use this skill when the user asks to: - reduce JavaScript or CSS bundle size, - review a bundle-analyzer report (webpack-bundle-analyzer, rollup-plugin-visualizer, or equivalent stats output), - set or enforce a performance budget for a route or shared entry, - decide where to add route-level or component-level code splitting, - assess the size impact of adding a new dependency before it merges. ## When NOT to use - Pure image/media asset optimization — different budget class (bytes-per-image, format/compression), different tooling; not this skill's concern. - Server-side bundle or cold-start size for a serverless/edge function — different runtime, not the client-bundle budget this skill enforces. - Deciding whether a chunk's *contents* are dead code once it is correctly split — hand off to `tree-shaking-dead-code-review` for that specific question; a chunk can be correctly split and still be full of unused exports, which is a distinct defect from a splitting-boundary defect. - Diagnosing which Core Web Vitals sub-phase is regressing before a cause is known — hand off to `core-web-vitals-triage` first if the user only has a vague "it feels slow" complaint with no analyzer report yet; return here once JS weight is confirmed as the dominant contributor. ## Context7 Documentation Protocol Bundler chunking APIs are version-sensitive and have changed shape recently — do not prescribe a config from memorized training data. 1. Call `ToolSearch` with query `"context7"` (or `"select:mcp__Context7__resolve-library-id,mcp__Context7__query-docs"`) to load the Context7 tools if not already loaded this session. 2. Call `mcp__Context7__resolve-library-id` for the bundler in scope (Vite, webpack, Rollup, esbuild, Rolldown) before prescribing any chunking configuration. 3. Call `mcp__Context7__query-docs` for the specific mechanism — e.g. "manualChunks vs codeSplitting", "splitChunks cacheGroups", "dynamic import chunk naming" — before ruling on it. Do this per review; do not reuse a prior session's memory of bundler internals. 4. Known version-sensitive trap verified via Context7 as of this skill's `updated` date: Vite 8 (Rolldown-powered) removes the object form of `build.rollupOptions.output.manualChunks` entirely and deprecates the function form; the documented replacement is `build.rolldownOptions.output.codeSplitting` with a `groups` array (`{ name, test }` entries). Do not hand a Vite 8+ project a `manualChunks: { vendor: [...] }` object-form snippet — it will not apply. For Vite <8, function-form `manualChunks(id) { ... }` is still valid but is itself deprecated and should be flagged as a forward-migration item, not endorsed as the long-term pattern. 5. webpack's chunking surface (`optimization.splitChunks.cacheGroups`, dynamic `import()` with `webpackChunkName` magic comments) is comparatively stable across recent majors per Context7-grounded docs, but still confirm the installed webpack major before prescribing a specific `cacheGroups` shape, since defaults (`chunks: 'async'` vs `'all'`) affect which imports are eligible for splitting. 6. If Context7 is unavailable or returns no relevant match for the installed bundler, fall back to `official_docs` and mark the claim `documentation-based (Context7 unavailable)` rather than presenting it as freshly verified. 7. Never invent a bundler config key, CLI flag, or plugin option that no queried source confirms. ## Lean operating rules - Establish the numeric budget before ranking anything. A budget with no device/network class and no percentile is not a budget — it is a guess. Refuse to close a finding against an undefined budget. - Rank analyzer contributors by both parsed/gzipped byte size and main-thread execution cost; these produce different orderings, and execution cost correlates more directly with INP than raw byte size does. Report both, do not collapse them into one number. - Classify every large module into exactly one path: needed on the critical path (keep, minimize), needed but deferrable (route/component-split candidate), duplicated across chunks (fix chunk-grouping config, not application code), or a heavier-than-necessary dependency (replace, or hand off to `tree-shaking-dead-code-review` if the issue is unused exports rather than the dependency itself). - Treat over-splitting as a real regression, not just under-splitting. Every new chunk is a new request; if the aggregate request-overhead cost (connection reuse aside, still parse/eval/scheduling cost) offsets the byte savings, the split is net-negative. Require the byte-vs-request-count comparison before endorsing a split. - Confirm the installed bundler major version via Context7 before prescribing `manualChunks`, `rolldownOptions.codeSplitting`, or `splitChunks` syntax — see Context7 Documentation Protocol. Do not assume a memorized API is still current. - Never accept "it built without errors" as evidence a split is beneficial. Require a before/after byte comparison from the analyzer output, gzipped or brotli as configured for production. - Flag any dynamic `import()` whose module specifier is built from unsanitized user input as a code-injection risk, not merely a performance nit — this is a security-relevant finding, not a style note. - Do not recommend inlining third-party scripts to "save a request" without naming the CSP/subresource-integrity trade-off of inlined vs. externally loaded, SRI-checkable code. - Require a CI-enforced budget (bundlesize, Lighthouse CI budget, or bundler-plugin size-limit check) as a condition of marking any size finding resolved. A manual one-time fix with no enforcement is not a fix, it is a snapshot. ## References Load these only when needed: - [Budget methodology](references/budget-methodology.md) — use when the user has no existing budget, or an existing budget lacks a device/network class or percentile, and one must be established or corrected. - [Bundler chunking APIs](references/bundler-chunking-apis.md) — use when prescribing or reviewing Vite, webpack, or Rollup chunking configuration; contains the version-sensitive API grounding. - [Code-splitting boundaries](references/code-splitting-boundaries.md) — use when deciding where to add route- or component-level splitting, or when diagnosing duplicated-dependency-across-chunks and over-splitting. - [CI enforcement and verification](references/ci-enforcement-and-verification.md) — use for every review to specify the CI-enforced budget mechanism, the verification command, and the adversarial checklist before closing the finding. ## Response minimum Return, at minimum: - the numeric budget in scope (bytes, compression form, device/network class, percentile), stated explicitly or flagged as missing, - the ranked contributor list with both byte size and main-thread execution-cost figures, - per-item classification (keep / split / replace / dead-code-handoff / chunk-grouping-fix) with the concrete config diff, version-confirmed against the installed bundler major, - the CI-enforced budget definition to add, not just a one-time manual fix, - the verification command (`npm run build -- --report` or the project's equivalent analyzer invocation) and the request-count check confirming the split did not net-negative the change.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.