Skip to content

FRD: Lazy-Load Recharts in Chart Stories

FieldValue
IDFRD-050
OwnerDavid Holmes
StatusDraft
Last Updated2026-05-26
Target Releasev2.1.0
TypeRefactor
ComplexityS

Document Summary

Recharts (~252 kB minified) is eagerly loaded by all chart component stories and any application page that imports a chart component. This FRD defines the work to lazy-load Recharts via dynamic import so chart components emit a separate chunk loaded only when a chart is rendered.


Introduction

Overview

Bundle analysis shows that recharts (v3.8.1) contributes approximately 252 kB to the shared chunk. This chunk is loaded by every Storybook story and application page, even those that do not render charts. Lazy-loading Recharts behind a dynamic import() boundary eliminates this cost for non-chart consumers and improves initial page load performance.

Goals

  • Move recharts imports behind dynamic import() in chart components.
  • Chart components emit a separate chunk containing Recharts and its D3 dependencies.
  • Non-chart Storybook stories and application routes do not load the Recharts chunk.
  • Chart rendering remains functionally identical.

Non-Goals

  • Replacing Recharts with a lighter charting library.
  • Tree-shaking individual Recharts components (already done by the bundler for named imports).
  • Changing chart component APIs or visual design.

Scope

In Scope

ItemDescription
Chart componentsAll components that import from recharts.
Lazy wrapperReact.lazy() wrapper for each chart component (or a shared chart lazy boundary).
Loading fallbackShape-matched skeleton for each chart type.
Storybook storiesChart stories use the lazy wrapper.

Out of Scope

ItemReason
Recharts version upgradeSeparate concern.
Chart component redesignPure bundling refactor.
PodTerminal lazy loadingCovered by FRD-049.

Users and Pain Points

UserPain Point
Application user (non-chart page)Loads ~252 kB of JavaScript they never use, increasing page load time.
Storybook user (non-chart stories)Storybook initial load includes the Recharts chunk.
Mobile user252 kB of JavaScript has a significant parse-time cost on lower-powered devices.

Definitions

TermDefinition
RechartsA React charting library built on D3, used for line charts, bar charts, pie charts, and area charts in the design system.
Dynamic importA JavaScript import() expression that loads a module asynchronously at runtime.
ChunkA separate JavaScript file produced by the bundler, loaded on demand.

Current State

Chart components import Recharts at the top level:

import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';

These static imports cause the bundler to include the entire Recharts library (and its D3 dependencies) in a shared chunk that is loaded eagerly. Bundle analysis shows this chunk at approximately 252 kB (minified, pre-gzip).

All chart stories and any application page that imports a chart component pay this cost, even if the chart is conditionally rendered or behind a tab.


Proposed Solution

Identify chart components

Grep for all files importing from recharts:

Terminal window
rg "from 'recharts'" src/

Each file gets the lazy-load treatment.

Lazy wrapper pattern

For each chart component (or a shared chart wrapper):

import { lazy, Suspense } from 'react';
import { ChartSkeleton } from './chart-skeleton';
const LineChartComponent = lazy(() => import('./line-chart-impl'));
export function LineChart(props: LineChartProps) {
return (
<Suspense fallback={<ChartSkeleton width={props.width} height={props.height} />}>
<LineChartComponent {...props} />
</Suspense>
);
}

Skeleton components

Create chart-skeleton.tsx that renders a placeholder matching the chart’s expected dimensions. Use a simple gray rectangle with axis lines to suggest the chart shape.

Shared chunk

Because all chart components import from recharts, the bundler will naturally coalesce them into a single shared chunk. Verify this in the build output.


Requirements

IDPriorityRequirement
LAZY-R-01P0recharts is loaded via dynamic import only when a chart component renders.
LAZY-R-02P0Non-chart pages and stories do not load the Recharts chunk.
LAZY-R-03P0Chart rendering is functionally unchanged.
LAZY-R-04P1A shape-matched skeleton displays during chunk loading.
LAZY-R-05P1The Recharts chunk is less than 300 kB (headroom for D3 subdependencies).

Functional Requirements

  1. Each chart component renders a skeleton immediately, then swaps to the real chart once the Recharts chunk loads.
  2. Props are forwarded without loss.
  3. Charts render correctly with data, axes, tooltips, and legends after lazy load.
  4. If the chunk fails to load, an error boundary displays a retry option.
  5. Multiple chart components on the same page share the Recharts chunk (single download).
  6. Responsive behavior (chart resizing on window resize) works correctly after lazy load.

Non-Functional Requirements

CategoryRequirement
Bundle savingsMain chunk size decreases by at least 200 kB.
Load latencyRecharts chunk loads in under 300ms on broadband.
No layout shiftSkeleton matches chart dimensions.
Backward compatibilityLazy wrappers export the same props as the original chart components.

API/Interface Requirements

No public API changes. Lazy wrappers accept the same props as the original chart components. If chart components are exported from the barrel, the barrel points to the lazy wrappers.


Accessibility Requirements

  • Chart skeletons should have role="img" and aria-label="Loading chart".
  • Once loaded, charts retain their existing accessibility attributes (ARIA labels, data table alternatives).

Content and Documentation Requirements

  • Update any documentation referencing chart component imports.
  • Add a developer note about the lazy-load boundary in chart component TSDoc.

Dependencies

DependencyTypeNotes
recharts (v3.8.1)Existing (npm)Moved behind dynamic import.
D3 subdependenciesExisting (transitive)Bundled with Recharts in the lazy chunk.
Chart source componentsInternalWrapped by lazy loaders.

Risks and Tradeoffs

RiskLikelihoodImpactMitigation
Visible loading delay before charts appear.MediumLowSkeleton provides visual continuity. Charts are rarely above-the-fold critical content.
SSR incompatibility with React.lazy.LowLowCharts are client-rendered; no SSR concern.
Recharts internal state management conflicts with lazy loading.LowLowResponsiveContainer and other Recharts components are designed for client-only rendering and handle mount/unmount gracefully.

Open Questions

  1. Should all chart components share a single lazy boundary (one React.lazy for the whole chart module) or each get their own? Leaning toward one shared boundary for simplicity.
  2. Should the skeleton animate (shimmer) or be static? Leaning toward a subtle shimmer consistent with other loading states in the design system.
  3. Are there chart components used in critical-path rendering that should preload the chunk?

Acceptance Criteria

  • recharts is not in the main/shared chunk.
  • A separate chart chunk exists in the build output.
  • Non-chart Storybook stories load without fetching the Recharts chunk.
  • Charts render correctly after lazy loading.
  • Skeleton displays during chunk load.
  • Main chunk size decreases by at least 200 kB.
  • pnpm typecheck passes.
  • pnpm vitest run --project unit passes.
  • pnpm build-storybook completes without errors.

LLM Handoff Instructions

When implementing this FRD:

  1. Run rg "from 'recharts'" src/ to find all files that import from recharts.
  2. For each chart component file, rename it to *-impl.tsx (e.g., line-chart.tsx becomes line-chart-impl.tsx).
  3. Create a new file with the original name that uses React.lazy(() => import('./line-chart-impl')) and wraps it in &lt;Suspense&gt;.
  4. Create src/components/ui/chart-skeleton.tsx — a responsive placeholder with role="img" and aria-label="Loading chart". Match the design system’s existing skeleton/loading patterns.
  5. If multiple chart components exist, consider a single chart-lazy-boundary.tsx that wraps all of them rather than individual wrappers (reduces boilerplate).
  6. Update chart stories to import from the lazy wrapper files.
  7. Run pnpm build-storybook and verify the Recharts chunk is separate (check build output for chunk names and sizes).
  8. Compare main chunk sizes before and after. Record in the PR description.
  9. Run pnpm typecheck and pnpm vitest run --project unit.

Decision Log

DateDecisionRationale
2026-05-26Lazy-load at the chart component level using React.lazy.Standard React code-splitting pattern. Consistent with the PodTerminal lazy-loading approach (FRD-049).
2026-05-26Shape-matched skeleton over spinner.Prevents layout shift and matches the design system’s loading state convention.
2026-05-26Single Recharts chunk shared across all chart components.The bundler naturally coalesces imports from the same package. Separate chunks per chart type would be wasteful.

Document History

VersionDateAuthorChanges
0.12026-05-26David HolmesInitial draft.