Cherry-Picking D3.js ES Modules to Reduce Bundle Size
Why import the entire D3 library when you only need scale and axis? This walkthrough shows how cherry-picking ES modules cuts bundle size, enables tree-shaking, and what to watch out for during migration.
16 Sept 2026, 00:21 UTC

The Default D3 Bundle Problem
Most JavaScript projects start with import * as d3 from 'd3', pulling in nearly 80 KB gzipped even if only scale and axis are used. This default bundle behavior inflates build outputs and slows page loads, especially in component-heavy dashboards. The convenience of a single namespace comes at the cost of unused code traveling over the network.
The Modular D3 Ecosystem
D3 version 4 (released 2016) split the monolithic library into about 30 independent npm packages such as d3-scale, d3-axis, d3-selection, and others, while retaining a default d3 package that re-exports everything. This structure enables selective imports like import { scaleLinear } from 'd3-scale' instead of loading the entire namespace. Tree-shaking works reliably with modern bundlers (webpack 5+, Rollup, Vite, esbuild) when using named ES module imports from individual packages.
Individual Packages and Their Roles
d3-selection– core DOM manipulation methods such asselectandselectAll.d3-scale– scale functions includingscaleLinear,scaleOrdinal, and related helpers.d3-axis– axis generation and rendering.d3-transition– transition interpolation, but requires attention to monkey-patch behavior (see trade-offs).
Concrete Example: A Minimal Chart with Granular Imports
Below is a practical setup for a Vite + TypeScript project that imports only the modules needed for a simple bar chart.
Installation
# In your project directory, install only the required packages
npm install d3-scale d3-axis d3-selection
Usage
// src/chart.ts
import { scaleLinear } from 'd3-scale'
import { select } from 'd3-selection'
import { axisBottom } from 'd3-axis'
const x = scaleLinear()
.domain([0, 100])
.range([0, 300])
const chart = select('#chart')
.append('svg')
.attr('width', 400)
.attr('height', 300)
chart.append('g')
.attr('transform', 'translate(0,200)')
.call(axisBottom(x))
Verification
- Run your build:
npm run build - Inspect the output:
dist/assets/*.js - Check that only the imported functions appear—typically
scaleLinear,select, andaxisBottom—while unrelated D3 functions are excluded. - Use
gzip-size-clito compare:gzip-size dist/assets/chunk-js.js
In a minimal test, the gzipped bundle for this chart often lands between 15–25 KB, versus ~80 KB for the full d3 namespace import.
Trade-Offs and Migration Realities
While cherry-picking reduces bundle size, several practical considerations apply:
- UMD environments. The default
d3UMD build (dist/d3.min.js) remains required for plain script tags, Observable notebooks, or legacy tooling that doesn't support ES modules without a bundler. - Transition monkey-patch. The method
selection.transition()installs only when you side-effect-importd3-transition(import 'd3-transition') or use the default bundle. A granularimport { transition } from 'd3-transition'gives you the standalone function, not the prototype method on selections. - Plugin compatibility. Older D3 plugins (pre-v4) often expect the global
d3namespace and may break with modular imports. Verify maintenance status before committing. - Version alignment. Bundle analyzers sometimes show duplicate
d3-*submodules if different packages specify incompatible version ranges (e.g., one needsd3-selection@2, anotherd3-selection@3). Enforce consistent versions viapackage.jsonresolutions or pnpm/npm overrides. - Overlap with modern JavaScript. Packages like
d3-array,d3-collection, andd3-randomoverlap with native Array methods, Map/Set, andcrypto.getRandomValues. Evaluate whether platform primitives can further reduce dependencies.
Actionable Next Steps
- Start incremental adoption: keep
import * as d3 from 'd3'in files you're not ready to refactor, and gradually replace with granular imports per file. - Use an ESLint rule such as
no-restricted-importsto flag whole-namespace imports and enforce the granular policy across the codebase. - For library authors, publish
peerDependencieson specific packages (e.g.,d3-scale@^4) instead ofd3@^7to give consumers flexibility to deduplicate and avoid forcing the full bundle downstream. - Run the verification steps above in a clone of your project to confirm the bundle size impact before deploying to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.