Skip to content

Commit 2cdf99d

Browse files
committed
Improve bundle analyzer: count sync deps, add treemap visualization
- Sum entry chunk + all chunks reachable via static imports (so the reported size reflects true up-front cost; lazy chunks excluded) - Add `--visualize` flag (and `bundle:visualize` script) that emits an interactive treemap HTML per scenario via `rollup-plugin-visualizer` - Regenerate `latest.json` baseline with the new methodology
1 parent c30f87f commit 2cdf99d

7 files changed

Lines changed: 282 additions & 50 deletions

File tree

bundle-analyzer/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.temp-bundle-analysis/
22
bundle-reports/bundle-report-*.json
3+
bundle-reports/visualize-*.html

bundle-analyzer/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ pnpm bundle:analyze -- --components
6565
# Analyze specific scenarios or components by name
6666
pnpm bundle:analyze -- geo dagre
6767

68+
# Generate interactive treemap HTML for the given scenarios
69+
pnpm bundle:visualize -- core geo
70+
# → bundle-reports/visualize-core.html, bundle-reports/visualize-geo.html
71+
# Open in a browser to drill into per-module sizes (rendered/gzipped/brotli)
72+
6873
# Compare two report files
6974
pnpm bundle:compare -- report-new.json report-old.json
7075
```

bundle-analyzer/bundle-analyzer.ts

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { build } from "vite";
22
import { svelte } from "@sveltejs/vite-plugin-svelte";
3+
import { visualizer } from "rollup-plugin-visualizer";
34
import { resolve, join } from "node:path";
45
import {
56
writeFileSync,
@@ -47,6 +48,7 @@ class BundleAnalyzer {
4748
scenarios?: string[];
4849
components?: boolean;
4950
componentFilter?: string[];
51+
visualize?: boolean;
5052
}): Promise<BundleReport> {
5153
console.log("Starting bundle analysis...\n");
5254

@@ -70,7 +72,7 @@ class BundleAnalyzer {
7072

7173
for (const scenario of scenariosToAnalyze) {
7274
console.log(` Analyzing: ${scenario.name}`);
73-
const result = await this.analyzeScenario(scenario);
75+
const result = await this.analyzeScenario(scenario, options.visualize);
7476
results.push(result);
7577
}
7678

@@ -82,6 +84,16 @@ class BundleAnalyzer {
8284
this.saveReport(report);
8385
this.printReport(report);
8486

87+
if (options.visualize) {
88+
console.log("\nTreemap visualizations:");
89+
for (const scenario of scenariosToAnalyze) {
90+
const safeName = scenario.name.replace(/[^a-zA-Z0-9_-]/g, "_");
91+
console.log(
92+
` ${join(this.outputDir, `visualize-${safeName}.html`)}`
93+
);
94+
}
95+
}
96+
8597
// Clean up temp directory
8698
if (existsSync(this.tempDir)) {
8799
rmSync(this.tempDir, { recursive: true });
@@ -102,12 +114,14 @@ class BundleAnalyzer {
102114
}
103115

104116
private async analyzeScenario(
105-
scenario: Scenario
117+
scenario: Scenario,
118+
visualize = false
106119
): Promise<BundleResult> {
107120
const entryPath = this.createEntryFile(scenario);
108121
const { size, gzipSize } = await this.buildAndMeasure(
109122
entryPath,
110-
scenario.name
123+
scenario.name,
124+
visualize
111125
);
112126

113127
return {
@@ -153,15 +167,31 @@ const refs = [
153167

154168
private async buildAndMeasure(
155169
entryPath: string,
156-
scenarioName: string
170+
scenarioName: string,
171+
visualize = false
157172
): Promise<{ size: number; gzipSize: number }> {
158173
const safeName = scenarioName.replace(/[^a-zA-Z0-9_-]/g, "_");
159174
const outputPath = join(this.tempDir, `dist-${safeName}`);
175+
const visualizePath = join(
176+
this.outputDir,
177+
`visualize-${safeName}.html`
178+
);
160179

161180
try {
162181
await build({
163182
plugins: [
164183
svelte(),
184+
...(visualize
185+
? [
186+
visualizer({
187+
filename: visualizePath,
188+
template: "treemap",
189+
gzipSize: true,
190+
brotliSize: true,
191+
title: `Bundle: ${scenarioName}`,
192+
}),
193+
]
194+
: []),
165195
{
166196
name: "strip-comments",
167197
generateBundle(_options, bundle) {
@@ -231,15 +261,43 @@ const refs = [
231261
private measureBundle(
232262
safeName: string
233263
): { size: number; gzipSize: number } {
234-
const bundlePath = join(this.tempDir, `dist-${safeName}`, "bundle.js");
264+
const distDir = join(this.tempDir, `dist-${safeName}`);
265+
const entryPath = join(distDir, "bundle.js");
235266

236-
if (!existsSync(bundlePath)) {
267+
if (!existsSync(entryPath)) {
237268
return { size: 0, gzipSize: 0 };
238269
}
239270

240-
const content = readFileSync(bundlePath, "utf-8");
241-
const size = Buffer.byteLength(content, "utf-8");
242-
const gzipSize = gzipSync(content).length;
271+
// Sum the entry chunk + all chunks reachable via STATIC imports.
272+
// Lazy-loaded chunks (via dynamic `import()`) are excluded so the
273+
// reported size reflects what every consumer of this scenario pays
274+
// up-front, not optional features they may not use.
275+
const visited = new Set<string>();
276+
const stack = ["bundle.js"];
277+
let totalContent = "";
278+
279+
while (stack.length > 0) {
280+
const file = stack.pop()!;
281+
if (visited.has(file)) continue;
282+
visited.add(file);
283+
284+
const filePath = join(distDir, file);
285+
if (!existsSync(filePath)) continue;
286+
287+
const content = readFileSync(filePath, "utf-8");
288+
totalContent += content;
289+
290+
// Match top-of-file static imports: `import ... from "./XXX.js"`
291+
// (Dynamic `import("./XXX.js")` calls have a paren — different pattern.)
292+
const staticImportRegex =
293+
/(?:^|[\n;])\s*import\s+(?:[\s\S]*?\s+from\s+)?["']\.\/([\w-]+\.js)["']/g;
294+
for (const match of content.matchAll(staticImportRegex)) {
295+
stack.push(match[1]!);
296+
}
297+
}
298+
299+
const size = Buffer.byteLength(totalContent, "utf-8");
300+
const gzipSize = gzipSync(totalContent).length;
243301

244302
return { size, gzipSize };
245303
}
@@ -365,6 +423,7 @@ async function main() {
365423
}
366424

367425
const includeComponents = args.includes("--components");
426+
const visualize = args.includes("--visualize");
368427
const scenarioFilter = args
369428
.filter((a) => !a.startsWith("--"))
370429
.filter((a) => a.length > 0);
@@ -375,6 +434,7 @@ async function main() {
375434
components: includeComponents,
376435
componentFilter:
377436
scenarioFilter.length > 0 ? scenarioFilter : undefined,
437+
visualize,
378438
});
379439
}
380440

bundle-analyzer/bundle-reports/latest.json

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
2-
"timestamp": "2026-04-26T13:08:23.594Z",
2+
"timestamp": "2026-04-26T17:17:31.860Z",
33
"results": [
44
{
55
"scenario": "core",
66
"description": "Bare minimum: Chart context + Svg layer",
7-
"size": 672241,
8-
"gzipSize": 164100,
7+
"size": 653750,
8+
"gzipSize": 158781,
99
"imports": [
1010
"Chart",
1111
"Svg"
@@ -14,8 +14,8 @@
1414
{
1515
"scenario": "line-chart",
1616
"description": "Basic line chart with axes and grid",
17-
"size": 672265,
18-
"gzipSize": 164118,
17+
"size": 653774,
18+
"gzipSize": 158804,
1919
"imports": [
2020
"Chart",
2121
"Svg",
@@ -27,8 +27,8 @@
2727
{
2828
"scenario": "line-chart-interactive",
2929
"description": "Line chart with tooltip and highlight",
30-
"size": 672642,
31-
"gzipSize": 164281,
30+
"size": 654151,
31+
"gzipSize": 158950,
3232
"imports": [
3333
"Chart",
3434
"Svg",
@@ -42,8 +42,8 @@
4242
{
4343
"scenario": "area-chart",
4444
"description": "Area chart with axes",
45-
"size": 679562,
46-
"gzipSize": 165605,
45+
"size": 661071,
46+
"gzipSize": 160338,
4747
"imports": [
4848
"Chart",
4949
"Svg",
@@ -55,8 +55,8 @@
5555
{
5656
"scenario": "bar-chart",
5757
"description": "Bar chart with axes",
58-
"size": 674531,
59-
"gzipSize": 164511,
58+
"size": 656040,
59+
"gzipSize": 159194,
6060
"imports": [
6161
"Chart",
6262
"Svg",
@@ -68,8 +68,8 @@
6868
{
6969
"scenario": "scatter-chart",
7070
"description": "Scatter plot with points",
71-
"size": 672553,
72-
"gzipSize": 164187,
71+
"size": 654062,
72+
"gzipSize": 158863,
7373
"imports": [
7474
"Chart",
7575
"Svg",
@@ -82,8 +82,8 @@
8282
{
8383
"scenario": "pie-chart",
8484
"description": "Pie/donut chart with arcs",
85-
"size": 679190,
86-
"gzipSize": 165795,
85+
"size": 660696,
86+
"gzipSize": 160487,
8787
"imports": [
8888
"Chart",
8989
"Svg",
@@ -95,8 +95,8 @@
9595
{
9696
"scenario": "high-level-charts",
9797
"description": "All high-level chart components (LineChart, BarChart, etc.)",
98-
"size": 729852,
99-
"gzipSize": 174510,
98+
"size": 711365,
99+
"gzipSize": 169194,
100100
"imports": [
101101
"LineChart",
102102
"AreaChart",
@@ -109,8 +109,8 @@
109109
{
110110
"scenario": "geo",
111111
"description": "Geographic map with paths",
112-
"size": 675634,
113-
"gzipSize": 164598,
112+
"size": 657146,
113+
"gzipSize": 159299,
114114
"imports": [
115115
"Chart",
116116
"Svg",
@@ -122,8 +122,8 @@
122122
{
123123
"scenario": "geo-tiles",
124124
"description": "Geographic map with tile layer",
125-
"size": 680081,
126-
"gzipSize": 166170,
125+
"size": 661587,
126+
"gzipSize": 160892,
127127
"imports": [
128128
"Chart",
129129
"Svg",
@@ -136,8 +136,8 @@
136136
{
137137
"scenario": "geo-full",
138138
"description": "Full geo setup with all geo components",
139-
"size": 704046,
140-
"gzipSize": 172034,
139+
"size": 685563,
140+
"gzipSize": 166781,
141141
"imports": [
142142
"Chart",
143143
"Svg",
@@ -158,8 +158,8 @@
158158
{
159159
"scenario": "force",
160160
"description": "Force-directed graph layout",
161-
"size": 675264,
162-
"gzipSize": 164971,
161+
"size": 663068,
162+
"gzipSize": 161928,
163163
"imports": [
164164
"Chart",
165165
"Svg",
@@ -172,8 +172,8 @@
172172
{
173173
"scenario": "hierarchy-tree",
174174
"description": "Tree layout with links",
175-
"size": 679115,
176-
"gzipSize": 166333,
175+
"size": 660624,
176+
"gzipSize": 161042,
177177
"imports": [
178178
"Chart",
179179
"Svg",
@@ -186,8 +186,8 @@
186186
{
187187
"scenario": "hierarchy-treemap",
188188
"description": "Treemap layout",
189-
"size": 678677,
190-
"gzipSize": 166140,
189+
"size": 660183,
190+
"gzipSize": 160848,
191191
"imports": [
192192
"Chart",
193193
"Svg",
@@ -200,8 +200,8 @@
200200
{
201201
"scenario": "hierarchy-pack",
202202
"description": "Circle packing layout",
203-
"size": 678485,
204-
"gzipSize": 166229,
203+
"size": 659994,
204+
"gzipSize": 160950,
205205
"imports": [
206206
"Chart",
207207
"Svg",
@@ -213,8 +213,8 @@
213213
{
214214
"scenario": "dagre",
215215
"description": "Dagre directed graph layout",
216-
"size": 738780,
217-
"gzipSize": 182465,
216+
"size": 720313,
217+
"gzipSize": 177133,
218218
"imports": [
219219
"Chart",
220220
"Svg",
@@ -227,8 +227,8 @@
227227
{
228228
"scenario": "sankey",
229229
"description": "Sankey flow diagram",
230-
"size": 681188,
231-
"gzipSize": 166502,
230+
"size": 662707,
231+
"gzipSize": 161204,
232232
"imports": [
233233
"Chart",
234234
"Svg",
@@ -241,8 +241,8 @@
241241
{
242242
"scenario": "chord",
243243
"description": "Chord diagram",
244-
"size": 681168,
245-
"gzipSize": 166294,
244+
"size": 662677,
245+
"gzipSize": 161036,
246246
"imports": [
247247
"Chart",
248248
"Svg",
@@ -253,8 +253,8 @@
253253
{
254254
"scenario": "canvas",
255255
"description": "Canvas-based rendering",
256-
"size": 672241,
257-
"gzipSize": 164104,
256+
"size": 653750,
257+
"gzipSize": 158785,
258258
"imports": [
259259
"Chart",
260260
"Canvas"
@@ -263,8 +263,8 @@
263263
{
264264
"scenario": "all",
265265
"description": "Everything from layerchart (worst case)",
266-
"size": 1023672,
267-
"gzipSize": 249263,
266+
"size": 1011500,
267+
"gzipSize": 246119,
268268
"imports": [
269269
"*"
270270
]

0 commit comments

Comments
 (0)