Skip to main content

Performance Optimization Large Datasets React

When datasets grow from thousands to millions of points, React charts slow down. A line chart with 100k points rendered as SVG DOM elements will freeze the browser. This article teaches you to scale charts using Canvas rendering, data aggregation (sampling), virtual scrolling, and memoization. You'll learn to profile performance, identify bottlenecks, and deploy dashboards that handle big data without compromising responsiveness.

Understanding the Performance Problem

SVG charts render each data point as a DOM element. 100k points = 100k DOM nodes. Creating, updating, and rendering this many nodes is expensive. A browser's rendering pipeline becomes the bottleneck: layout, paint, and composite operations take seconds, not milliseconds. Canvas avoids this by painting pixels directly, but brings its own challenges (no interactivity, complex updates).

The solution isn't "bigger hardware"; it's changing your approach. Most big-data visualizations don't need every point rendered. You aggregate, sample, and use Canvas for the expensive parts.

Strategy 1: Data Aggregation (Downsampling)

Most 100k-point datasets are sparse: large regions repeat the same trend. Aggregate nearby points into bins, computing min/max/average per bin. Render the aggregated data instead of raw points.

function aggregateData(data, bucketSize) {
// Group data into buckets; compute min, max, avg per bucket
const buckets = [];

for (let i = 0; i < data.length; i += bucketSize) {
const bucket = data.slice(i, i + bucketSize);
const values = bucket.map((d) => d.value);

buckets.push({
min: Math.min(...values),
max: Math.max(...values),
avg: values.reduce((a, b) => a + b, 0) / values.length,
count: bucket.length,
startIndex: i
});
}

return buckets;
}

export function AggregatedChart({ data }) {
// Aggregate 100k points into 1000 buckets
const aggregated = aggregateData(data, Math.ceil(data.length / 1000));

return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={aggregated}>
<CartesianGrid />
<XAxis dataKey="startIndex" />
<YAxis />
<Tooltip />
{/* Render aggregated values, not raw points */}
<Line
type="monotone"
dataKey="avg"
stroke="#8884d8"
isAnimationActive={false}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
);
}

100k points become 1k aggregated points, rendering instantly. Users see the overall trend without losing detail; zooming in fetches and aggregates at a finer granularity.

Strategy 2: Canvas Rendering for Extreme Scale

When even aggregation isn't enough, use Canvas. Canvas can render 1M+ points per frame:

import React, { useEffect, useRef } from "react";
import { scaleLinear } from "d3-scale";

export function CanvasLineChart({ data }) {
const canvasRef = useRef(null);

useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const margin = 50;

// Scales
const values = data.map((d) => d.value);
const xScale = scaleLinear()
.domain([0, data.length - 1])
.range([margin, width - margin]);

const yScale = scaleLinear()
.domain([Math.min(...values), Math.max(...values)])
.range([height - margin, margin]);

// Clear canvas
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);

// Draw line
ctx.strokeStyle = "steelblue";
ctx.lineWidth = 2;
ctx.beginPath();

data.forEach((d, i) => {
const x = xScale(i);
const y = yScale(d.value);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});

ctx.stroke();

// Draw axes
ctx.strokeStyle = "black";
ctx.lineWidth = 1;

// X axis
ctx.beginPath();
ctx.moveTo(margin, height - margin);
ctx.lineTo(width - margin, height - margin);
ctx.stroke();

// Y axis
ctx.beginPath();
ctx.moveTo(margin, margin);
ctx.lineTo(margin, height - margin);
ctx.stroke();
}, [data]);

return <canvas ref={canvasRef} width="800" height="400" />;
}

This Canvas chart renders 100k+ points smoothly. Trade-off: Canvas is a bitmap, so zooming requires redraw (slow) and interactivity is manual (you calculate which point was clicked).

Strategy 3: Virtual Scrolling for Lists and Tables

If you're visualizing data as a table or list (not a chart), virtual scrolling renders only visible rows:

import { FixedSizeList as List } from "react-window";

export function VirtualizedDataTable({ data }) {
const Row = ({ index, style }) => (
<div style={style} style={{...style, display: "flex", gap: "20px"}}>
<span>{data[index].id}</span>
<span>{data[index].name}</span>
<span>{data[index].value}</span>
</div>
);

return (
<List
height={400}
itemCount={data.length}
itemSize={35}
width="100%"
>
{Row}
</List>
);
}

react-window renders only visible rows (100 at a time), not all 100k. Scrolling instantly swaps rows in/out. Perfect for large data tables.

Strategy 4: Lazy Loading and Pagination

Fetch and render data in chunks:

export function PaginatedChart() {
const [page, setPage] = useState(0);
const [data, setData] = useState([]);
const pageSize = 1000;

useEffect(() => {
const fetchPage = async () => {
const response = await fetch(
`/api/data?page=${page}&limit=${pageSize}`
);
const pageData = await response.json();
setData(pageData);
};

fetchPage();
}, [page]);

return (
<div>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
{/* ... */}
</LineChart>
</ResponsiveContainer>

<div style={{ marginTop: "20px" }}>
<button onClick={() => setPage((p) => Math.max(0, p - 1))}>
Previous
</button>
<span> Page {page + 1} </span>
<button onClick={() => setPage((p) => p + 1)}>Next</button>
</div>
</div>
);
}

Show 1000 points at a time; users navigate pages. Reduces memory and rendering load.

Strategy 5: Memoization and useMemo

Prevent unnecessary re-computations:

import { useMemo } from "react";
import { scaleLinear, scaleBand } from "d3-scale";

export function OptimizedChart({ data, domain, range }) {
// Memoize scale computation
const xScale = useMemo(
() =>
scaleBand()
.domain(domain)
.range(range)
.padding(0.1),
[domain, range]
);

// Memoize aggregated data
const aggregated = useMemo(
() => aggregateData(data, 100),
[data]
);

return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={aggregated}>
{/* ... */}
</BarChart>
</ResponsiveContainer>
);
}

useMemo skips expensive computations if inputs haven't changed. Critical for large datasets.

Profiling Performance: Measuring What Matters

Use React DevTools and Chrome DevTools to identify bottlenecks:

// Measure render time
console.time("render");
const start = performance.now();

// ... your chart code

const end = performance.now();
console.timeEnd("render");
console.log(`Render took ${end - start}ms`);

// Profile memory usage
if (performance.memory) {
console.log(`Memory: ${(performance.memory.usedJSHeapSize / 1e6).toFixed(2)}MB`);
}

Typical targets for 2026 hardware:

MetricTargetBad
Initial render< 1s> 5s
Re-render on data update< 100ms> 500ms
60 FPS scroll/interaction16ms per frame< 10 FPS
Memory footprint< 100MB> 500MB

Real-World Example: Optimizing a 500k-Point Stock Chart

Start with the naive approach; measure; optimize:

// Step 1: Naive SVG (too slow)
<LineChart data={data500k}>
<Line dataKey="price" />
</LineChart>
// Render time: 10+ seconds, jank

// Step 2: Aggregate to 5k points
const aggregated = aggregateData(data500k, 100);
<LineChart data={aggregated}>
<Line dataKey="avg" />
</LineChart>
// Render time: 200ms, smooth

// Step 3: Canvas for final optimization (if needed)
<CanvasLineChart data={aggregated} />
// Render time: 50ms, 60 FPS on scroll

Most cases stop at Step 2. Canvas is only if you measure and find SVG insufficient.

Key Takeaways

  • Aggregate data first. 100k points usually aggregate to 1k without losing meaningful information.
  • Canvas for extreme scale. When aggregation isn't enough, Canvas renders millions of pixels per frame.
  • Virtual scrolling for lists. Only render visible rows; swap on scroll.
  • Memoize expensive computations. useMemo prevents redundant scale and aggregation calculations.
  • Profile before optimizing. Measure render time, memory, and FPS. Optimize the real bottleneck, not guessed ones.

Frequently Asked Questions

How do I choose between aggregation and Canvas?

Aggregate first (faster, simpler, preserves interactivity). Canvas only if aggregation still yields >10k points and you've measured performance as a problem. Start simple.

What's a good aggregation bucket size?

1000 buckets is usually good; adjust based on user screen size. On a 1000px-wide chart, each pixel represents ~1 bucket. More buckets than pixels is wasteful.

Can I combine Canvas and SVG interactivity?

Yes. Render data on Canvas; overlay SVG for interactive elements (legend, crosshair, labels). This is the hybrid approach used by many production dashboards.

How do I handle zooming with Canvas?

Recompute the domain (e.g., zoom from points 0–100k to 25k–50k), then re-aggregate at the new zoom level and re-draw. This is more complex than SVG zooming.

Does aggregation lose important detail?

Only if you aggregate too aggressively. For most trends (stock prices, CPU usage), aggregating 100 points to 1 (averaging) preserves shape and excludes noise. Min/max aggregation preserves outliers.

How do I handle real-time streaming into large datasets?

Keep only the last N points in memory (circular buffer); older data is archived. This prevents unbounded memory growth while keeping recent data interactive.

Further Reading