Skip to content

perf(appkit-ui): enable tree-shaking with sideEffects flag and modular echarts imports#442

Merged
MarioCadenas merged 4 commits into
mainfrom
perf/appkit-ui-tree-shaking
Jul 7, 2026
Merged

perf(appkit-ui): enable tree-shaking with sideEffects flag and modular echarts imports#442
MarioCadenas merged 4 commits into
mainfrom
perf/appkit-ui-tree-shaking

Conversation

@MarioCadenas

Copy link
Copy Markdown
Collaborator

Problem (P-1, High)

@databricks/appkit-ui had no sideEffects declaration, so webpack/esbuild/Rollup consumers had to assume every module re-exported by the /react barrel (src/react/index.ts) is side-effectful and keep all of them. Importing just Button shipped the entire chart stack — including the full echarts bundle (~350KB gz), @tanstack/react-table, and marked + dompurify — because charts/base.tsx imported echarts-for-react's default entry, which imports the monolithic echarts package.

Changes

"sideEffects": ["**/*.css"] in packages/appkit-ui/package.json

This tells bundlers that only CSS files have import side effects, so any JS module whose exports are unused can be dropped from the graph. Reasoning for the array (vs false):

  • No JS module in the package relies on import-time side effects from another module (no polyfills, no cross-module registration) — verified by survey. The new ECharts registration lives in the same module as BaseChart, so it always executes when any chart component is used.
  • The package ships dist/styles.css (exported as ./styles.css), which consumers import purely for its side effect. Declaring false would let bundlers prune that import; ["**/*.css"] keeps it safe.
  • The dist publish tool (tools/dist-appkit.ts) spreads the whole package.json, so the field survives into the published package.

Modular ECharts imports in charts/base.tsx

Switched from echarts-for-react (full echarts entry) to echarts-for-react/lib/core + echarts/core, registering exactly what the option builders in options.ts use:

  • Series: LineChart (line + area), BarChart (vertical + horizontal), PieChart (pie + donut), ScatterChart, HeatmapChart, RadarChart
  • Components: TitleComponent (title), TooltipComponent, LegendComponent, GridComponent (grid/xAxis/yAxis), VisualMapComponent (heatmap color scale)
  • Features: LegacyGridContainLabel — the cartesian option builder sets grid.containLabel: true, which in ECharts 6 is a separately registered feature (the full bundle auto-includes it; the mount tests caught its absence as a runtime warning)
  • Renderer: CanvasRenderer (BaseChart always renders with renderer: "canvas")

Nothing in options.ts/normalize.ts uses dataZoom, toolbox, markLine, dataset, or other components, so they are intentionally not registered. The ECharts instance type stays a type-only import type { ECharts } from "echarts" (erased at compile time). Consumers passing custom options that need extra features can register them in their own app via echarts/core's use() — documented in a comment at the registration site.

New mount tests (charts/__tests__/base.test.tsx)

Existing chart tests only covered pure option/normalize functions and never exercised ECharts rendering. ECharts does not throw on a missing registration — it logs "Series X is used but not imported" and renders a blank chart. The new suite mounts BaseChart for every chart family (line, area, bar, horizontal bar, scatter, pie, donut, radar, heatmap with visualMap) under jsdom with a canvas stub and fails on any missing-registration error/warning.

Verification

  • pnpm build — dist preserves module structure (unbundle mode); built dist/react/charts/base.js imports only echarts/core subpaths, no bare echarts value import anywhere in dist
  • pnpm vitest run --project appkit-ui — 17 files, 320 tests passed (including 9 new mount tests)
  • pnpm -r typecheck, pnpm check:fix, pnpm docs:build — clean

This pull request and its description were written by Isaac.

@MarioCadenas MarioCadenas requested a review from a team as a code owner June 11, 2026 16:32
@MarioCadenas MarioCadenas requested a review from pkosiec June 11, 2026 16:32
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 658 KB (-4.2 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 686 KB (-2.9 KB) 240 KB (-1.3 KB)
Type declarations 268 KB (-323 B) 91 KB (-113 B)
Source maps 1.3 MB (-9.1 KB) 445 KB (-3.3 KB)
Other 11 KB 3.7 KB
Total 2.3 MB (-12 KB) 779 KB (-4.7 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 74 KB (-185 B) 2.5 KB 76 KB (-185 B) external 243 KB (-576 B)
./beta 39 KB 231 B 39 KB external 117 KB
./type-generator 18 KB (-161 B) 0 B 18 KB (-161 B) external 53 KB (-449 B)

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 70 KB
. utils.js initial 4.0 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 30 KB
./beta databricks.js initial 5.7 KB
./beta service-context.js initial 3.0 KB
./beta client-options.js initial 219 B
./beta databricks.js lazy 128 B
./beta index.js lazy 103 B
./type-generator index.js initial 18 KB

@databricks/appkit-ui

npm tarball (packed): 298 KB (+946 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 354 KB (-8.8 KB) 117 KB (-2.9 KB)
Type declarations 204 KB (+517 B) 73 KB (+266 B)
Source maps 676 KB (+3.5 KB) 220 KB (+1.3 KB)
CSS 16 KB 3.3 KB
Total 1.2 MB (-4.8 KB) 413 KB (-1.3 KB)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 4.2 KB 49 KB 54 KB 208 KB 11 KB
./js/beta 20 B 0 B 20 B 0 B 0 B
./react 429 KB (-162 KB) 49 KB 477 KB (-162 KB) 1.3 MB (-495 KB) 166 KB (-305 B)
./react/beta 20 B 0 B 20 B 0 B 0 B

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 4.1 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 20 B
./react index.js initial 427 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 20 B

…r echarts imports

Importing any single component from the @databricks/appkit-ui/react barrel
(e.g. Button) previously forced bundlers to retain every re-exported module,
including the full echarts bundle (~350KB gz), because the package declared
no sideEffects field and charts/base.tsx imported the monolithic echarts
entry via echarts-for-react's default export.

- Declare "sideEffects": ["**/*.css"] so bundlers can drop unused modules
  while keeping the dist/styles.css side-effect import alive. No JS module
  in the package relies on import-time side effects from another module;
  the ECharts registration lives in the same module as BaseChart, so it is
  always retained when charts are used.
- Switch base.tsx to echarts-for-react/lib/core + echarts/core, registering
  only what the option builders use: LineChart, BarChart, PieChart,
  ScatterChart, HeatmapChart, RadarChart; Title/Tooltip/Legend/Grid/
  VisualMap components; LegacyGridContainLabel (grid.containLabel) and
  CanvasRenderer. Keep ECharts instance type as a type-only import.
- Add mount tests for every chart family that fail on ECharts
  missing-registration errors (which are logged, not thrown).

Co-authored-by: Isaac
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…tions

- Document on the `options` prop that only the built-in feature set is
  registered; extra ECharts features (dataZoom, toolbox, markLine, ...)
  must be registered by the consumer via `use` from "echarts/core",
  which requires resolving the same echarts module instance/version.
- Note in the registration block that tooltip-driven axisPointer ships
  with TooltipComponent, and that `use()` must stay co-located with
  BaseChart because package.json#sideEffects declares JS modules pure.
- Fix the type-only import comment: the ECharts type is used for the
  stored instance ref.
- Add tests: custom `options` with a registered component logs no
  registration errors; empty data renders the "No data" fallback.

Co-authored-by: Isaac
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas force-pushed the perf/appkit-ui-tree-shaking branch from 883bc3d to 39bd6f2 Compare July 7, 2026 13:47
… export

echarts-for-react ships no exports map, so the /lib/core subpath resolves to the
CommonJS build whose default export is module.exports. Under ESM default-import
interop a consumer's bundler hands back the whole { default, __esModule } object
rather than the component, so BaseChart rendered an object and React threw
"Element type is invalid". Import echarts-for-react/esm/core (real ESM export
default) instead, which keeps the modular tree-shakeable core and fixes interop.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…aking

The "." barrel re-exports several modules; without a sideEffects field a consumer
importing a single symbol (e.g. sql from appkit-ui) forces bundlers to retain every
sibling module and its runtime deps. Declare sideEffects: ["**/cli/**"] so bundlers
can drop unused barrel modules, while preserving the CLI's import "dotenv/config"
side effect (the . barrel never reaches ./cli).

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas merged commit b0fdd19 into main Jul 7, 2026
10 checks passed
@MarioCadenas MarioCadenas deleted the perf/appkit-ui-tree-shaking branch July 7, 2026 15:24
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.

2 participants