Guide

Charting Library bundle optimization

How the charting library keeps map topology out of the bundle that every chart page pays for, and how to measure the result.

The mechanism

Map topology (TopoJSON for US counties, CBSAs, world borders) is by far the largest thing in this library — an order of magnitude bigger than the chart renderers. None of it should load for a bar chart. Two layers of splitting keep it out:

1. Map renderers are lazy. src/lib/controller/ChartBuilder.tsx imports every map through React.lazy(), so webpack emits each as its own chunk and the main entry only carries a reference:

const AlbersUSA = lazy(() => import('../charts/maps/AlbersUSA'));
const AlbersUSACounties = lazy(() => import('../charts/maps/AlbersUSACounties'));
const AlbersUSACBSA = lazy(() => import('../charts/maps/AlbersUSACBSA'));
const BlockUSA = lazy(() => import('../charts/maps/BlockUSA'));
const HexUSA = lazy(() => import('../charts/maps/HexUSA'));
const World = lazy(() => import('../charts/maps/World'));
const WorldOrthographic = lazy(() => import('../charts/maps/WorldOrthographic'));

Each is rendered inside a Suspense boundary, so a map shows a loading state while its chunk is in flight.

2. Topology is a dynamic import behind a Suspense resource. A map renderer never statically imports its topology — that would pull the JSON straight back into the map's chunk. Instead it wraps a dynamic import() of the JSON in createTopologyLoader from @prc/charting-utilities, which returns a Suspense-compatible reader:

const loadCbsaTopology = createTopologyLoader(() => import('../../data/maps/usa-cbsa/topology.json'));
const loadUsaTopology = createTopologyLoader(() => import('../../data/maps/usa/topology.json'));

Webpack emits each topology as its own chunk, and the loader throws its pending promise so the surrounding Suspense boundary renders a fallback until the chunk lands. The resource is created at module scope, so the topology is fetched once and shared: several maps on one page that use the same topology pay for it a single time. A counties map therefore costs the entry bundle, plus its renderer chunk, plus its topology chunk.

Because both entries set output.publicPath: 'auto', chunks resolve relative to the loading module. This matters on VIP, where the build is not served from the same origin-relative path in every context — see src/publicPath.js and class-prc-charting-library.php.

Measuring current sizes

Sizes move with every dependency bump, so measure rather than trusting a number written down here:

npx turbo build --filter=@prc/charting-library
ls -la plugins/prc-charting-library/build/*.js |
	awk '{printf "%8.0f KB  %s\n", $5/1024, $9}' | sort -rn | head -20

Chunk names are content-derived numbers (855.js, 637.js), and the dual build emits a parallel view- prefixed set. To find which map a given chunk holds, grep it for a marker from that topology — a CBSA name, a county name, a country name.

Snapshot, July 2026 (uncompressed, before transfer encoding):

AssetSizeNotes
editor.js~481 KBReact editor entry
view.js~589 KBPreact frontend entry
CBSA map chunk~1.6 MBLargest topology by a wide margin
Counties map chunk~823 KB
Smaller map chunks~112 KBState-level and cartogram maps

The build prints size warnings for the map chunks. That is expected and acceptable: they are only fetched by pages that actually render that map.

Build configuration

webpack.config.js exports two configs from one source tree — a React editor build and a Preact Script Module frontend build. See the dual-build table in the package README for what each emits and how it is registered with WordPress.

The code splitting itself needs no custom configuration; wp-scripts detects dynamic import() and emits the chunks. The custom config exists for the dual build, output.library, and publicPath, not for splitting. The WordPress enqueue side needs nothing special either — the webpack runtime in the entry bundle fetches chunks on demand.

If further reduction is needed

  • Simplify the topology. More aggressive TopoJSON simplification is the highest-leverage option, since topology dominates every map chunk. The CBSA and counties files are the ones worth attacking.
  • Preload for map-heavy pages. Warm the chunk earlier for templates known to render a map, trading idle bandwidth for a shorter visible loading state.
  • Split vendor code further. Separating d3 and visx would help pages that render several different chart types.
  • Serve topology from a CDN for cross-site caching, at the cost of another origin in the critical path.

Was this helpful?