A Thousand Kilobytes
This case study covers two bundle-size investigations: reducing the initial download for an observability app, then reducing the import costs of a shared UI library four years later.
Reducing the application entry bundle
The application’s entry bundle included charting, editor and analytics libraries, so users downloaded these dependencies before the corresponding features were needed.
Moving dependencies into separate files reduced the entry bundle by 31.8%. The split was limited to avoid adding more network requests than the saved bytes justified.
Charting, data-grid and analytics libraries were restricted to type-only or dynamic imports by a lint rule, preventing them from being added to the entry bundle through a static import.
The build also gave output files descriptive names to make bundle reports easier to inspect.
Measuring the shared library
Four years later, an application using the shared toolkit reduced its entry chunk from 1,004 kB to 80 kB by maintaining a manual list of modules to split into chunks. That configuration indicated that consumers were compensating for the library’s package structure.
Before changing the package exports, each module was measured independently to identify which imports accounted for the size.
Each built module was bundled and minified independently, with React external, to measure its cost when imported on its own.
A charting dependency exposed only one barrel entry. Importing any symbol from it therefore added 82.6 kB minified; the theme builder, data context and chart component all had the same measured cost.
Since individual imports from that dependency had the same cost, package tiers were based on dependency boundaries rather than on individual features. Two alternative structures were also measured and rejected.
Removing a shared dependency
The token module was imported by every chart mark and also imported the theme builder from the large charting barrel. This dependency increased the size of otherwise lightweight chart imports.
Moving the theme builder into its own file reduced a themed legend from 87.9 kB to 4.4 kB without changing the public API.
Verifying the package exports
The package split was accompanied by tests to verify that existing exports remained available.
The emitted declarations were compared before and after the change. All 138 charting exports and 316 design-system exports were unchanged. Tests also checked that subpath exports were disjoint, covered the root exports, and matched the published export list.
Resolution tests imported every subpath at runtime through Node’s package exports map. A strict NodeNext TypeScript probe checked that valid subpaths compiled and invalid ones failed.
The tests identified a layout helper that had been intended as private but became public when its module was exposed as a subpath. The export was removed from that subpath.
Runtime performance
The entry-bundle changes addressed initial download size. Separate work reduced runtime overhead in the application and shared library.
In the application, data fetching moved to a query cache, unnecessary refetches were removed, chart props were stabilized to avoid repeated invalidation, and the router was replaced with one better suited to a single-page app.
The library downsamples chart series above 1,000 points using largest-triangle-three-buckets or min/max-per-pixel, virtualizes table rows, and subscribes to interaction state per key to avoid re-rendering every widget on each pointer movement.
In a separate event-sourced dashboard, replacing full-history recomputation with an incremental fold reduced per-event cost by about five orders of magnitude. Windowing a neighbouring table reduced heap usage from 2.7 GB to 68 MB.
In each case, measuring individual modules or operations identified costs that were not apparent from the package or application as a whole.