Repository navigation
chore(claude): add an agent explaining bundle size changes - #1307
Conversation
The analysis of the examples could only be read in a browser: both build:analyze setups start a report server, Rsdoctor for webpack and vite-bundle-analyzer for Vite. Comparing two builds, to find what made an example grow, meant opening two reports and diffing them by eye, which a script or an agent cannot do. ANALYZE=json now writes dist/bundle-analysis.json and starts nothing. Each module carries its size and the modules that import it, which is what explaining a growth needs. Like ANALYZE=true, it disables the size budget, since an analysis is a diagnostic. - Vite: the analyzer has a json mode giving the size of each module in the minified chunk. The analysis switch moves to scripts/vite/bundle-analysis.mjs, shared by the three configurations, which each held a copy of it. - webpack: the brief JSON report of Rsdoctor 1.6 came out with an empty module graph, and the webpack stats give the importers of top level modules only, while most of @maxgraph/core is concatenated into a single module. A small plugin reads the module graph of the compilation instead. It keeps the modules of the chunks only, as the compilation also holds those that tree shaking left out, and the imports that tree shaking kept. Its sizes are before minification.
The pr-bundle-size skill says how much each example changed, never why. Finding the cause meant building both refs by hand and comparing their analyses, so a growth was often reported, or a size budget raised, without naming what grew. bundle-impact-analyst builds the example on both refs with ANALYZE=json and compares the two analyses. It reports the modules added, removed or resized and, for each new module, the chain of imports from the example that pulled it in. The reference is built in a temporary git worktree with its own install, outside the repository so that no IDE indexes it, and removed afterwards: the checkout of the user is never switched, and its dependencies never reinstalled. That costs several minutes, so the agent asks the caller to warn the user first. Sonnet is enough: the work is running builds and reading a comparison, with judgement only on what to report. The comparison is a script next to ci-bundle-sizes.sh rather than code in the prompt of the agent, so that it can be run and tested on its own. It handles both formats, and follows the reading rules of the shared README.
On its validation run, adding TooltipHandler to an example, the agent found every change but left one unexplained: util/styleUtils.js grew by 467 bytes although its source was the same on both refs. The prompt only said that a resized module of @maxgraph/core is a change of its code, which was false here. TooltipHandler imports fit and getScrollOrigin from that module, so tree shaking kept two more functions of it. The agent now checks whether the source of a resized module changed, and when it did not, looks for the added or removed module that imports from it. A few bytes with no such import are the minifier renaming variables.
WalkthroughThe PR adds JSON bundle-analysis modes for webpack and Vite, a script to compare their reports, and an agent workflow that builds and compares named examples across refs. The Vite example configurations now use a shared helper to select analysis or size-limit behavior. ChangesBundle analysis and comparison
Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant Analyst as bundle-impact-analyst
participant Head as Working-tree build
participant Reference as Reference-worktree build
participant Compare as compare-bundle-analyses.mjs
Analyst->>Head: Build examples and produce analysis JSON
Analyst->>Reference: Build examples in temporary worktree and produce analysis JSON
Head->>Compare: Provide working-tree analysis
Reference->>Compare: Provide reference analysis
Compare-->>Analyst: Return size differences, module changes, and import chains
Possibly related PRs
Merge Risk:
|
There was a problem hiding this comment.

Review info
-
Run configuration-
- Source mode: Auto
- Repository: Unavailable
- Branch: Unavailable
- File: No configuration file from this repository applied
- Commit: Unavailable
- Layers: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
13355d37-920b-497f-9ef7-d4efa8450f91
Files selected for processing: 10-
.claude/agents/bundle-impact-analyst.md.claude/rules/tooling/bundle-size-budgets.md.claude/shared/bundle-sizes/README.md.claude/shared/bundle-sizes/compare-bundle-analyses.mjs.claude/skills/pr-bundle-size/SKILL.mdpackages/ts-example-selected-features/vite.config.jspackages/ts-example-without-defaults/vite.config.jspackages/ts-example/vite.config.jsscripts/vite/bundle-analysis.mjsscripts/webpack/bundle-analysis.cjs
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.
-
The search of the import chain stopped at the first module of @maxgraph/core that nothing in the bundle imports, and reported the @maxgraph/core entry point as the origin. When the same module was also reachable from a module of the example, that more precise chain was never found. The importer-free chain is now kept only as a fallback, used when no path reaches the example. A Vite module found in several output files kept the imports of its last leaf only, which could drop the import that brought a new module in, and leave it with no importer at all. The imports of all its leaves are now merged, as the sizes already were.
The head build includes the uncommitted edits of the working tree, but the check telling a changed source from a tree shaking effect compared the reference with HEAD, which excludes them. A module resized by an uncommitted edit was then attributed to tree shaking. Comparing the reference with the working tree covers both.
The comparison names a module of @maxgraph/core by its compiled file, core/lib/esm/util/styleUtils.js, and the agent passed that path under packages/core/src unchanged. git diff on a file that does not exist prints nothing, exactly as for an unchanged source, so a real source edit could be blamed on tree shaking. The agent now maps the prefix and the extension to the TypeScript source, and checks that the file exists before reading an empty diff as no change.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

Minor · Normalize webpack asset names before comparing them. · compare-bundle-analyses.mjs:58-60
.claude/shared/bundle-sizes/compare-bundle-analyses.mjs:58-60
Minor · Functional Correctness · Quick win Normalize webpack asset names before comparing them.
The webpack reader copies
[contenthash]into each asset name. When an asset changes, exact-name matching reports separateN/Arows instead of the asset size delta. Remove the hash segment when reading webpack assets.Suggested fix
- const files = json.assets.map(({ name, size }) => ({ name, size })); + const files = json.assets.map(({ name, size }) => ({ + name: name.replace(/\.[a-f\d]+\.(?=[^.]+$)/, '.'), + size, + }));

Minor · Use the parsed-size meaning consistently in the Vite… · compare-bundle-analyses.mjs:103-110
.claude/shared/bundle-sizes/compare-bundle-analyses.mjs:103-110
Minor · Functional Correctness · Quick win Use the parsed-size meaning consistently in the Vite comparison.
vite-bundle-analyzer@1.3.9reports uncompressed bytes attributed through source maps. It does not report the bytes physically occupied by a minified chunk. Update both the comparator label and thebundle-impact-analystinstructions, or the agent can still describe the corrected value as raw minified output.Suggested fix
-// Vite, ... each with its size in the minified chunk, 'parsedSize'. +// Vite, ... each with its uncompressed parsed bytes attributed through source maps, 'parsedSize'. ... - sizeUnit: 'bytes in the minified chunk', + sizeUnit: 'uncompressed parsed bytes attributed to the chunk',Update the corresponding Vite descriptions in
.claude/agents/bundle-impact-analyst.mdto use the same parsed-byte meaning.

Minor · Check the Vite and webpack helpers independently. · bundle-impact-analyst.md:45-49
.claude/agents/bundle-impact-analyst.md:45-49
Minor · Functional Correctness · Quick win Check the Vite and webpack helpers independently.
The combined gate passes when only one helper supports
ANALYZE=json. The agent then runs the unsupported bundler and can fail while copying its missingdist/bundle-analysis.json. Check each helper separately and stop when either check fails.Suggested fix
- git grep -q "'json'" <reference> -- scripts/vite/bundle-analysis.mjs scripts/webpack/bundle-analysis.cjs + git grep -q "'json'" <reference> -- scripts/vite/bundle-analysis.mjs + git grep -q "'json'" <reference> -- scripts/webpack/bundle-analysis.cjs

Minor · Stop on build failure and require a fresh analysis report… · bundle-impact-analyst.md:56-65
.claude/agents/bundle-impact-analyst.md:56-65
Minor · Stability & Availability · Quick win Stop on build failure and require a fresh analysis report before copying.
A Vite example can fail during
tscbeforevite buildwritesbundle-analysis.json. The next unconditionalcpcan then copy an older report fromdistor fail without identifying that the build produced no report. Remove the old report before the build, chain the copy to a successful build, and check that the new report is non-empty.Suggested fix
- ANALYZE=json npm run build -w packages/<example> - cp packages/<example>/dist/bundle-analysis.json <work dir>/head-<example>.json + rm -f packages/<example>/dist/bundle-analysis.json && + ANALYZE=json npm run build -w packages/<example> && + test -s packages/<example>/dist/bundle-analysis.json && + cp packages/<example>/dist/bundle-analysis.json <work dir>/head-<example>.json

Minor · Install cleanup at work-directory creation. · bundle-impact-analyst.md:50-80
.claude/agents/bundle-impact-analyst.md:50-80
Minor · Stability & Availability · Quick win Install cleanup at work-directory creation.
The temporary directory is created before the toolchain check, and the reference worktree is created before
npm ci, builds, and comparisons. If any command fails and the agent stops before step 7, the temporary directory remains. Failures aftergit worktree addcan also leave Git worktree metadata. Install an idempotentEXITcleanup boundary so cleanup runs on both success and failure.Suggested fix
-2. **Create a work directory outside the repository**, so that no IDE indexes the second install: - `mktemp -d`. Note its path, you need it in every later call. +2. **Create a work directory outside the repository**, so that no IDE indexes the second install: + ```bash + work_dir="$(mktemp -d)" + cleanup() { + git worktree remove --force "$work_dir/reference" 2>/dev/null || true + rm -rf -- "$work_dir" + } + trap cleanup EXIT + ``` + Use `$work_dir` in every later call. ... -7. **Remove the worktree and the work directory, always**, also when a step above failed, before reporting: - `git worktree remove --force <work dir>/reference; rm -rf <work dir>; git worktree list` +7. **Before reporting, check the worktree list if needed.** The `EXIT` trap performs cleanup on both success and failure.

Review info
-
Run configuration-
- Source mode: Auto
- Repository: Unavailable
- Branch: Unavailable
- File: No configuration file from this repository applied
- Commit: Unavailable
- Layers: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
300a3bb4-500f-42fe-83c7-5b83c1225dc4
Files selected for processing: 1-
.claude/agents/bundle-impact-analyst.md
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.
-
webpack names the bundle main.<contenthash>.js, and the hash changes with the content, which is the case under analysis. The reference and head bundles never matched, so the output file table showed two N/A rows instead of the change, although for webpack it is the only figure measured after minification. The hash is now removed from the name, as it already was for Vite. Images named by their hash alone keep it, since nothing would be left of their name. The comments also say what the Vite sizes leave out: the bytes the source map attributes to no module, a few hundred per chunk, are counted in no module and in no output file.
- The check that the reference supports ANALYZE=json ran git grep on both helpers at once, which passes as soon as one of them matches: a reference where only one bundler had the mode got through, and its other builds wrote no report. Each helper is now checked on its own. - A build failing before writing its report, in the tsc step of a Vite example for instance, left the report of an earlier build in dist, and the next cp copied it as if it were new. The report is now removed before the build and copied only after a successful build that wrote a non-empty one. - The Vite sizes leave out the bytes mapped to no module, so the agent knows the output file table can be a few hundred bytes below the file on disk. An EXIT trap cleaning the work directory was suggested and not added: each Bash call of the agent runs in its own shell, so the trap would fire at the end of the call creating the directory and remove it before any build.
|
@coderabbitai Answers to the 5 comments posted outside the diff in your review of 4f906c7:
|
|
|
Tip For best results, initiate chat on the files or code changes. Analysis chain
The three fixes address the concerns:
I withdraw the other two suggestions:
This was a source inspection. I did not rerun builds or the agent. Learnings used
If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! You are interacting with an AI system. |



Overview
The
pr-bundle-sizeskill says how much each example grew, never why. Finding the cause meant runningbuild:analyzeon two refs and comparing two reports in a browser, so a growth was often reported, or a size budget raised, without naming what grew.This PR adds the
bundle-impact-analystagent, candidate 5 of analysis 003, and the machine-readable analysis it needs. Given one or more examples and a reference, it reports the modules that entered, left or changed size, and the chain of imports from the example that pulled each new module in.A JSON mode for the bundle analysis
Both analyzers only started a report server, Rsdoctor for webpack and
vite-bundle-analyzerfor Vite.ANALYZE=jsonnow writesdist/bundle-analysis.jsonand starts nothing. LikeANALYZE=true, it disables the size budget, since an analysis is a diagnostic. The regularnpm run buildis unchanged, and still enforces the budgets.scripts/vite/bundle-analysis.mjs, shared by the threevite.config.js, which each held a copy of it.@maxgraph/coreis concatenated into a single module. A small plugin inscripts/webpack/bundle-analysis.cjsreads the module graph of the compilation instead. It keeps only the modules of the chunks, since the compilation also holds those that tree shaking dropped. Its sizes are before minification, so the real change is read on the output file.The agent
.claude/agents/bundle-impact-analyst.md, Sonnet,Bash,Read,Glob,Grep.ci-bundle-sizes.sh local-toolchain. It never runsnpm installthere.npm ci, outside the repository so that no IDE indexes it, and always removed afterwards. The checkout of the user is never switched. A reference older than the JSON mode is refused rather than patched..claude/shared/bundle-sizes/compare-bundle-analyses.mjs, a script next toci-bundle-sizes.shrather than code in the prompt, so that it can be run and tested alone. It handles both formats.vite-bundle-analyzernamesimportedBythe list of modules a module imports, so the script inverts it.The install and the builds take a few minutes and load the machine, so the agent's description asks the caller to warn the user first. The
pr-bundle-sizeskill and the bundle size budgets rule point at the agent.How it was verified
ANALYZE=jsonand write their file. A regular build writes neither the file nor source maps, and gives the same webpack bundle, same content hash.*-without-defaultsand*-selected-features, finds the plugins the latter registers, with chains matching their sources, such assrc/index.js -> SelectionCellsHandler.js -> EdgeHandler.js.TooltipHandlerto the plugins ofjs-example-selected-featuresandts-example-selected-features, compared with the commit before it.TooltipHandler.jsadded, the largest changesrc/index.js -> TooltipHandler.js, andsrc/main.ts (through the @maxgraph/core entry point) -> TooltipHandler.js35.8k tokens, 6 tool calls, 198 s, the
npm ciand the six builds included.The run, made before the last commit, left one change unexplained:
util/styleUtils.jsgrew by 467 bytes in the Vite example although its source did not change.TooltipHandlerimportsfitandgetScrollOriginfrom it, so tree shaking kept two more functions. A later commit tells the agent to check for this case; it has not been run again since.The fixes made after the CodeRabbit review came after that run too. The two fixes of the comparison script, the import chain searched through every path and the imports of a Vite module found in several output files, were checked on synthetic analyses against the previous version of the script. To tell a changed source from a tree shaking effect, the agent now compares the reference with the working tree, not with
HEAD, and on the TypeScript source of the module rather than on its compiled.jsname, which matched no file and made every source look unchanged.Review comments not applied
Two comments of the CodeRabbit review on 4f906c7, posted outside the diff, were checked and not applied.
ts-examplereport, theparsedSizeof the modules of themaxgraphchunk add up to 426,170 bytes, and the minified file on disk is 426,184 bytes. What the sizes leave out is the bytes the source map attributes to no module, a few hundred per chunk (764 bytes in the smallindexchunk). The script header and the agent now say so, in 8c88de6 and 403e13b.EXITtrap to clean the work directory. Each Bash call of the agent runs in its own shell, so the trap would fire at the end of the call creating the directory and remove it before any build. The agent keeps its explicit cleanup step, run also when a step failed.Notes
mxgraph-archaeologistagent.package-lock.jsononmainis out of sync with the version ranges of somepackage.jsonfiles (^against~forstorybookandwebpack-dev-server), sonpm installrewrites it. The installed versions are not affected, and this PR does not change the lock file.Summary by CodeRabbit