Skip to content

Reduce bundle size#845

Merged
techniq merged 24 commits intonextfrom
bundle-reduction-phase-1
Apr 27, 2026
Merged

Reduce bundle size#845
techniq merged 24 commits intonextfrom
bundle-reduction-phase-1

Conversation

@techniq
Copy link
Copy Markdown
Owner

@techniq techniq commented Apr 26, 2026

No description provided.

techniq added 6 commits April 11, 2026 10:24
- Add `"sideEffects": ["**/*.css"]` to layerchart/package.json so downstream bundlers can prune unused barrel re-exports
- Convert ChartChildren's value imports of components only referenced in `ComponentProps<typeof X>` to `import type` (Area, Arc, Bars, BrushContext, Group, Line, Pie, Spline, TooltipContext)
- Inline `geoFitObjectTransform` into Chart.svelte to drop the static import edge through `$lib/utils/geo.js` (which transitively imports d3-geo)

No visible change in the bundle analyzer (it already does aggressive treeshaking), but unlocks the dynamic-import refactor in the next commit and protects consumers whose bundlers tree-shake less aggressively.
Convert statically-imported components to `{#await import(...)}` so they only ship to users who opt in via the corresponding prop:
- ChartChildren: ChartAnnotations (when annotations.length > 0), DefaultTooltip (tooltipContext truthy), Labels, Legend, Points
- TooltipContext: Voronoi (mode === 'voronoi'), Arc (radial bounds/band)
Voronoi alone removes d3-geo-voronoi and its transitive d3-geo from the always-loaded bundle.
Update the bundle analyzer to sum the entry chunk plus all chunks reachable via static imports (lazy chunks excluded), so the reported size reflects up-front cost rather than total feature surface.

Result: `core` (`Chart` + `Svg`) drops 154.94 → 109.95 KB gz (-29%). Comparable savings on every scenario except `all` (which exercises every lazy path).
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 26, 2026

🦋 Changeset detected

Latest commit: 8379d86

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
layerchart Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 26, 2026

Bundle Size Analysis

Use-Case Scenarios

Foundation

Scenario Current New Change
🟢 core 638.43 KB (155.06 gz) 432.62 KB (105.35 gz) -205.81 KB (-32.2%) (-49.71 gz, -32.1%)
🟢 canvas 638.43 KB (155.06 gz) 432.62 KB (105.35 gz) -205.81 KB (-32.2%) (-49.71 gz, -32.1%)

Cartesian charts

Scenario Current New Change
🟢 line-chart 638.45 KB (155.08 gz) 432.64 KB (105.36 gz) -205.81 KB (-32.2%) (-49.72 gz, -32.1%)
🟢 line-chart-interactive 638.82 KB (155.22 gz) 446.87 KB (107.74 gz) -191.95 KB (-30.0%) (-47.49 gz, -30.6%)
🟢 area-chart 645.58 KB (156.58 gz) 456.62 KB (110.96 gz) -188.96 KB (-29.3%) (-45.62 gz, -29.1%)
🟢 bar-chart 640.66 KB (155.46 gz) 441.27 KB (107.40 gz) -199.40 KB (-31.1%) (-48.06 gz, -30.9%)
🟢 scatter-chart 638.73 KB (155.14 gz) 436.77 KB (106.46 gz) -201.96 KB (-31.6%) (-48.68 gz, -31.4%)
🟢 pie-chart 645.21 KB (156.73 gz) 439.50 KB (107.11 gz) -205.71 KB (-31.9%) (-49.62 gz, -31.7%)
🟢 high-level-charts 694.69 KB (165.23 gz) 527.97 KB (126.96 gz) -166.72 KB (-24.0%) (-38.27 gz, -23.2%)

Geo

Scenario Current New Change
🟢 geo 641.74 KB (155.57 gz) 447.22 KB (108.73 gz) -194.52 KB (-30.3%) (-46.83 gz, -30.1%)
🟢 geo-tiles 646.08 KB (157.12 gz) 451.56 KB (110.24 gz) -194.52 KB (-30.1%) (-46.88 gz, -29.8%)
🟢 geo-full 669.50 KB (162.87 gz) 498.24 KB (123.27 gz) -171.25 KB (-25.6%) (-39.60 gz, -24.3%)

Hierarchy

Scenario Current New Change
🟢 hierarchy-tree 645.14 KB (157.27 gz) 458.26 KB (112.14 gz) -186.88 KB (-29.0%) (-45.13 gz, -28.7%)
🟢 hierarchy-treemap 644.71 KB (157.08 gz) 438.91 KB (107.32 gz) -205.80 KB (-31.9%) (-49.75 gz, -31.7%)
🟢 hierarchy-pack 644.53 KB (157.18 gz) 438.73 KB (107.43 gz) -205.79 KB (-31.9%) (-49.75 gz, -31.6%)

Graph / network

Scenario Current New Change
🟢 force 647.53 KB (158.13 gz) 460.65 KB (112.96 gz) -186.87 KB (-28.9%) (-45.17 gz, -28.6%)
🟢 dagre 703.43 KB (172.98 gz) 516.74 KB (128.02 gz) -186.69 KB (-26.5%) (-44.96 gz, -26.0%)
🟢 sankey 647.17 KB (157.43 gz) 460.32 KB (112.30 gz) -186.85 KB (-28.9%) (-45.12 gz, -28.7%)
🟢 chord 647.15 KB (157.26 gz) 441.39 KB (107.48 gz) -205.75 KB (-31.8%) (-49.79 gz, -31.7%)

Worst case

Scenario Current New Change
🟢 all 987.79 KB (240.35 gz) 964.01 KB (238.25 gz) -23.79 KB (-2.4%) (-2.11 gz, -0.9%)

Understanding this report
  • Use-case scenarios measure the bundle cost of common chart configurations (e.g. a line chart with axes)
  • Individual components measure each component imported in isolation
  • Svelte runtime is excluded; sizes reflect layerchart + its dependencies (d3, etc.)
  • When multiple components share dependencies (e.g. d3-scale), the real-world cost is lower than the sum of individual sizes
  • Changes smaller than 10 bytes or 0.1% are considered insignificant

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 26, 2026

Open in StackBlitz

npm i https://pkg.pr.new/layerchart@845

commit: 8379d86

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 26, 2026

built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
layerchart ✅ Ready (View Log) Visit Preview 8379d86

techniq added 2 commits April 26, 2026 13:05
The test queried `.lc-legend-swatch-button` synchronously, but `Legend` is now dynamically imported inside `ChartChildren` and isn't in the DOM until the chunk resolves. Wrap the query in `vi.waitFor` so the test waits for the buttons to mount before clicking.
techniq added 2 commits April 26, 2026 13:32
A filtered run (e.g. `pnpm bundle:visualize -- core`) was overwriting `bundle-reports/latest.json` with just the filtered scenarios, causing the PR comparison comment to show "0 KB" for every scenario the filtered run didn't cover. Now `latest.json` is only updated when no `--components`, scenario, or component filters are passed; filtered runs still get a timestamped report. Also regenerate the full baseline against the current lazy-loaded code.
# Conflicts:
#	bundle-analyzer/bundle-reports/latest.json
…oltip

DefaultTooltip is dynamically imported from ChartChildren. It was using `import * as Tooltip from '../tooltip/index.js'`, which dragged the entire tooltip barrel — including TooltipContext.svelte (already in the static graph via Chart.svelte) — into its lazy chunk. Under CI's resource-constrained dev server this broke the DefaultTooltip browser test ("Failed to fetch dynamically imported module"). Replace the namespace import with explicit named imports of just the 5 components actually used (no Context). The local `const Tooltip = { Root, Header, List, Item, Separator }` keeps the existing template syntax (`<Tooltip.Root>`, etc.) unchanged. Bonus: tightens tree-shaking — `all` scenario drops ~3 KB gz.
The dynamic import of `DefaultTooltip` from `ChartChildren` caused a CI-only "Failed to fetch dynamically imported module" failure in `DefaultTooltip.svelte.test.ts`. Local tests passed; only the Linux/playwright runner reproduced. Switching the inner `import * as Tooltip` namespace to named imports (commit 7e5d6e7) didn't help. The savings were small (~5 KB gz on `core`) and not worth the test instability — the other lazy-loads (Voronoi, Arc, ChartAnnotations, Labels, Legend, Points) remain. Net Phase 2 gain on `core` is now -39 KB gz (-25%) instead of -45 KB gz (-29%).
The DefaultTooltip vitest-browser test still failed in CI ("Failed to fetch dynamically imported module") even after reverting just DefaultTooltip's lazy-load and after switching the inner namespace import to named imports. Rather than continue narrowing, revert all 4 ChartChildren lazy-loads (Labels, Legend, Points, ChartAnnotations). The TooltipContext lazy-loads (Voronoi, Arc) stay — they're the biggest win (~17 KB gz on core from removing d3-geo-voronoi + d3-geo from the static graph) and aren't in the test failure path. Net Phase 2 gain on `core` is now -17 KB gz (-11%).
The `{#await import('./X.svelte')}` template pattern broke `DefaultTooltip.svelte.test.ts` in CI ("Failed to fetch dynamically imported module" on the test file). Local tests passed; only the Linux/playwright runner reproduced. Move the dynamic imports back into `$effect` blocks (script-side `import()`), which Vite/vitest-browser appears to handle differently. Same chunks, same bundle savings, but the dynamic imports live in regular JS rather than Svelte template syntax. TooltipContext keeps `{#await}` for Voronoi/Arc — those weren't in the test failure path. Net Phase 2 gain on `core` recovered to -40 KB gz (-25%).
… snippet

Replace per-component `$state`/`$effect`/conditional-render boilerplate in ChartChildren with a generic `<Lazy>` component that takes a `load` factory and either spreads remaining props to the loaded component (single-render case: ChartAnnotations, Legend) or passes it via a `then` snippet (loop case: Points, Labels). Conditional gating uses standard `{#if}` outside `<Lazy>` rather than a `when` prop. The snippet is named `then` (not `children`) to avoid collisions with loaded components that have their own `children` prop. Same bundle behavior and CI-friendly `$effect`-under-the-hood as the previous explicit pattern; ~half the lines per lazy-load.
Now that `optimizeDeps.include: ['d3-interpolate']` prevents the mid-test Vite reload (the actual cause of the CI flake), the cleaner inline `{#await import('./X.svelte') then { default: X }}` pattern works fine in CI. Revert ChartChildren back to that pattern, matching what TooltipContext already does. Remove `Lazy.svelte` since it's no longer needed. Same chunks, same ~25% savings on `core` (115.60 KB gz).
…erchart

The bundle analysis CI workflow runs `pnpm build:packages` (which calls `pnpm --filter './packages/*' build`) before `pnpm bundle:analyze`. layerchart only had a `package` script (svelte-package convention), no `build`, so the filter call did nothing in CI — the analyzer ran against an empty `dist/` and produced 0-byte sizes for every scenario. That's why the PR comment showed "0.00 KB" for "New". Add a `build` alias for `svelte-package` (kept alongside existing `package` for back-compat) so CI's existing build step now actually rebuilds layerchart's `dist/`.
techniq added 2 commits April 26, 2026 21:00
The script already computed `sizePercent` and `gzipSizePercent`; now display them inline in the Change column alongside the raw KB deltas (e.g. `-160.00 KB (-25.1%)`). Easier to scan relative impact across scenarios than raw byte counts alone. Same for the Individual Components table.
Add a `group` field to scenarios (Foundation, Cartesian charts, Geo, Hierarchy, Graph / network, Worst case) and render the PR comment with one sub-table per group instead of one alphabetized list. Reorder `define-scenarios.ts` to put `core` first and scenarios within their category. Remove the alphabetical sort in `analyzeChanges` so the comment preserves the natural order.
Remove from root `layerchart`: `Geo*` + `Graticule` + `TileImage`, `Tree`/`Treemap`/`Pack`/`Partition`, `ForceSimulation`, `Dagre`/`Sankey`/`Chord`/`Ribbon`. Each group now lives in its own folder + sub-path entry: `layerchart/geo`, `layerchart/hierarchy`, `layerchart/force`, `layerchart/graph`.

Defends against bundlers that don't tree-shake the root barrel cleanly — `@dagrejs/dagre` (~22 KB), `d3-geo` (~15 KB), `d3-force` (~7 KB), `d3-hierarchy` (~6 KB), `d3-sankey` (~6 KB), and `d3-chord` (~2 KB) are now reachable only via opt-in imports. Per-scenario bundle sizes are unchanged for already-good consumers; the worst-case `all` scenario drops 241.8 → 235.5 KB gz.

`Voronoi`/`Hull` stay at root (already lazy via `TooltipContext`). `Contour`/`Density`/`Raster`/`BoxPlot`/`Violin`/`Threshold` also stay (not category-specific). High-level charts (`LineChart`, `BarChart`, etc.) remain at root.

Breaking: imports for the moved components must move to the new sub-paths.
Three components that everyone pays for in `core` today, but only some users actually need:

- `Spline` in `Grid` (radial linear grid lines only — non-radial users never render it)
- `Bar` in `Highlight` (only when user sets `bar` prop, default `false`)
- `BrushContext` in `Chart` (only when user sets `brush` prop, default `undefined`) — required splitting the inner `<TooltipContext>` tree across the brush/no-brush branches; brush tests now wait for the lazy chunk via a new `awaitBrushReady` helper

Saves ~4 KB gz on `core` (115.60 → 111.31 KB) and similar on every cartesian/geo/graph/hierarchy scenario. ~28% total reduction on `core` vs the pre-Phase-1 baseline.

Also switch `@layerstack/svelte-actions` imports from the barrel to sub-paths (`/styles`, `/portal`). No bundle effect since tree-shaking already stripped the unused `popover.js`, but it stops the Svelte REPL/CDN from eagerly fetching `@floating-ui/dom` (popover's transitive dep) when users load `layerchart` from a CDN.
The previous structure put `<TooltipContext>` + `<ChartChildren>` *inside* the `{#await import('./BrushContext.svelte')}` block, so on slow networks the entire chart was blocked on the chunk fetch (~300-1000ms on Fast 4G). Move the same subtree into the `{#await}`'s pending branch as well so the chart paints immediately; the `{:then}` branch then re-mounts it inside `BrushContext` once the chunk arrives.

Trade-off: brief one-time re-mount of `TooltipContext` + `ChartChildren` (~50ms) when the chunk lands. Acceptable because it happens before any user interaction (no tooltip/series state to lose) and brush is opt-in. Bundle savings preserved (core +0.1 KB from the duplicated template, since the actual modules are deduped).

The other lazy-load sites (`Voronoi`/`Arc` in `TooltipContext`, `DefaultTooltip`, `Bar`, `Points`/`Labels`/`Legend`/`ChartAnnotations` in `ChartChildren`, `Spline` in `Grid`) don't need the same treatment — none of them block visible chart content from rendering.
@techniq techniq merged commit 888c990 into next Apr 27, 2026
9 checks passed
@techniq techniq deleted the bundle-reduction-phase-1 branch April 27, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant