How to Slim Down Bloated Dashboard Bundles from Recharts with TanStack Charts
If you enter a dashboard page and the hourglass spins just trying to load a chart, it's almost certainly a Recharts issue. Sighing is inevitable when you see bundle sizes inflate by hundreds of kilobytes just for adding a few charts. Recharts is tied far too closely to the React DOM reconciliation cycle. Because UI structure and data processing are tangled together, it's hard to use lightly.
TanStack Charts makes for a solid alternative in these cases. It separates visualization logic into a standalone graphics-grammar-based core. While Recharts typically consumes over 150KiB, TanStack Charts sits around 40KiB. That slims down the bundle size by close to 70%.
The catch is production code migration. Disrupting a screen that's working fine is always uneasy. Persuading team members and transitioning without breaking things requires a clear sequence.
Safely Ripping Out Recharts Code
Trying to change everything all at once makes it nearly impossible to figure out where things broke. You need to take it component by component and break it down into exactly 4 steps.
- Separate Data Preprocessing: First, extract data manipulation logic that was mixed inside Recharts component JSX into standalone pure functions.
- Mark Mapping: Move axis configurations and visual elements like lines and points into the
marks array of a defineChart declaration object.
- Replace Utilities: Swap out
ResponsiveContainer and Tooltip with TanStack's automatic scaler and the @tanstack/charts/tooltip module.
- Memoization: Wrap the chart definition object in
useMemo and pass it to the top-level Chart component.
Following just this sequence can cut the time it takes to replace your first chart component in half.
`typescript
import React, { useMemo } from 'react';
import { defineChart, lineY, dot } from '@tanstack/charts';
import { scaleBand } from '@tanstack/charts/scales/band';
import { scaleLinear } from '@tanstack/charts/scales/linear';
import { tooltip } from '@tanstack/charts/tooltip';
import { Chart } from '@tanstack/charts/react';
interface DataRow {
date: string;
value: number;
}
export function MigratedRevenueChart({ data }: { data: DataRow[] }) {
const definition = useMemo(() => {
return defineChart({
marks: [
lineY(data, {
x: 'date',
y: 'value',
stroke: 'var(--ts-chart-1, #2563eb)',
strokeWidth: 2,
}),
dot(data, {
x: 'date',
y: 'value',
fill: 'var(--ts-chart-1, #2563eb)',
r: 4,
}),
],
x: {
scale: () => scaleBand().padding(0.2),
},
y: {
scale: scaleLinear,
nice: true,
grid: true,
},
tooltip: {
use: tooltip,
className: 'custom-tooltip',
},
});
}, [data]);
return (
<div style={{ width: '100%', height: 300 }}>
);
}
`
Eliminating JavaScript Re-renders for Dark Mode Support
It looks amateurish to re-render the entire chart component every time the dark mode toggle is flipped. TanStack Charts natively supports CSS custom properties and currentColor instead of hardcoded color values.
Stop managing chart colors in JavaScript state and push them into CSS variables. You can simply make the chart default palette reference --ts-chart-1 through --ts-chart-6.
- Pre-define variables for charts in the CSS root and
[data-theme='dark'].
- Pass strings like
var(--ts-chart-1) into stroke or fill within defineChart options.
- Manage tooltip styles using CSS classes as well and handle background colors via variables.
Setting it up this way means when a designer asks you to change chart colors, you won't need to touch the TSX files at all. Tweaking the CSS is all it takes.
`css
:root {
--bg-chart-card: #ffffff;
--text-chart-main: #172033;
--border-chart-grid: #e2e8f0;
--ts-chart-1: #2563eb;
--ts-chart-2: #f97316;
}
[data-theme='dark'] {
--bg-chart-card: #0f172a;
--text-chart-main: #f8fafc;
--border-chart-grid: #334155;
--ts-chart-1: #60a5fa;
--ts-chart-2: #fb923c;
}
.custom-tooltip {
background-color: var(--bg-chart-card);
color: var(--text-chart-main);
border: 1px solid var(--border-chart-grid);
padding: 8px 12px;
border-radius: 6px;
}
`
Making Sure It Doesn't Crash Even When Fed 100,000 Data Points
Rendering 100,000 points via SVG creates 100,000 DOM nodes. The browser thread simply cannot keep up. The stuttering is plainly visible.
The solution combines two approaches. Switch to a Canvas renderer to eliminate DOM generation overhead, and compress data that is pointlessly denser than the screen pixel width using the LTTB (Largest Triangle Three Buckets) algorithm. LTTB reduces the point count while preserving the sharp characteristics (maximums, minimums) of the data.
`typescript
export function lttbDownsample(
data: T[],
xAccessor: (d: T) => number,
yAccessor: (d: T) => number,
threshold: number
): T[] {
const dataLength = data.length;
if (threshold >= dataLength || threshold === 0) return data;
const sampled: T[] = [];
let sampledIndex = 0;
const bucketSize = (dataLength - 2) / (threshold - 2);
sampled[sampledIndex++] = data[0];
for (let i = 0; i < threshold - 2; i++) {
let avgX = 0;
let avgY = 0;
const avgRangeStart = Math.floor((i + 1) * bucketSize) + 1;
const avgRangeEnd = Math.floor((i + 2) * bucketSize) + 1;
const currentAvgRangeEnd = avgRangeEnd < dataLength ? avgRangeEnd : dataLength;
for (let j = avgRangeStart; j < currentAvgRangeEnd; j++) {
avgX += xAccessor(data[j]);
avgY += yAccessor(data[j]);
}
avgX /= (currentAvgRangeEnd - avgRangeStart);
avgY /= (currentAvgRangeEnd - avgRangeStart);
const rangeOffs = Math.floor((i + 0) * bucketSize) + 1;
const rangeTo = Math.floor((i + 1) * bucketSize) + 1;
const pointAX = xAccessor(sampled[sampledIndex - 1]);
const pointAY = yAccessor(sampled[sampledIndex - 1]);
let maxArea = -1;
let maxAreaPointIndex = rangeOffs;
for (let j = rangeOffs; j < rangeTo; j++) {
const area = Math.abs(
(pointAX - avgX) * (yAccessor(data[j]) - pointAY) -
(pointAX - xAccessor(data[j])) * (avgY - pointAY)
) * 0.5;
if (area > maxArea) {
maxArea = area;
maxAreaPointIndex = j;
}
}
sampled[sampledIndex++] = data[maxAreaPointIndex];
}
sampled[sampledIndex++] = data[dataLength - 1];
return sampled;
}
`
When dealing with large-scale time-series data, use LTTB to thin it down to around 1,000 levels before passing it over. Then, enable the spatialIndex: true option to lower mouse pointer detection complexity. Finally, attach the Canvas renderer from the @tanstack/charts/react/canvas package to lock in 60fps.
How to Delegate Chart Code to Cursor or Copilot
With deeply nested JSX tag structures like Recharts, AI assistants are prone to hallucinations. They make up weird attributes or leave tags unclosed.
Libraries built with object declaration styles like TanStack Charts allow AI to pump out clean code as long as you feed it proper type schemas. Define a schema type like the one below in your project and drop it into your AI prompt or rules file (.cursorrules).
`typescript
export type ScaleType = 'linear' | 'band' | 'utc' | 'point';
export type MarkType = 'lineY' | 'barY' | 'dot' | 'areaY';
export interface AIScaleConfig {
type: ScaleType;
nice?: boolean;
grid?: boolean;
padding?: number;
}
export interface AIMarkConfig {
type: MarkType;
xKey: string;
yKey: string;
color?: string;
strokeWidth?: number;
}
export interface AIChartGenerationSchema {
data: Record<string, unknown>[];
scales: {
x: AIScaleConfig;
y: AIScaleConfig;
};
marks: AIMarkConfig[];
tooltip?: {
enabled: boolean;
className?: string;
};
}
`
Including rules in your prompt such as importing from exact subpaths (like @tanstack/charts/scales/linear) and enforcing useMemo usage ensures that the code spat out by the AI can be used immediately without modifications.