MUI Docs Infra

Warning

This is an internal project, and is not intended for public use. No support or stability guarantees are provided.

Benchmarking Streamed Charts (client)

These demos showcase useStream client-side streaming of large datasets as coordinated chunks. The data is computed in the browser; the entire simplified chart paints first, then each chunk swaps serially to its full detail.

For the server-side equivalents — computing the chunks in RSC via abstractCreateStream — see the Abstract Create Stream benchmarks.

Line chart

100,000 points streamed in chunks of 1,000. Each loading chunk simplifies its slice to 10 points; the coarse 1,000-point chart is up front, then each chunk swaps in its full 1,000-point detail serially.

page.tsx
'use client';
import * as React from 'react';
import { useStream } from '@mui/internal-docs-infra/useStream';
import type { StreamSource } from '@mui/internal-docs-infra/useStream';
import { CoordinatedLazy } from '@mui/internal-docs-infra/CoordinatedLazy';

const TOTAL = 100_000;
const CHUNK_SIZE = 1000;
const CHUNK_COUNT = TOTAL / CHUNK_SIZE; // 100
const SIMPLE_PER_CHUNK = 10;
const WIDTH = 900;
const HEIGHT = 220;

// A flowing multi-harmonic signal: a couple of low harmonics for the overall
// shape, plus finer detail whose amplitude swells and fades across the chart (the
// envelope) — so the 10-point loading slice reads as a clean sketch while the
// 1000-point detail adds the texture.
const TAU = Math.PI * 2;
const curve = (t: number) => {
  const envelope = 0.4 + 0.6 * Math.abs(Math.sin(t * TAU * 1.3));
  const base = 0.6 * Math.sin(t * TAU * 2.4) + 0.26 * Math.sin(t * TAU * 5.7 + 0.8);
  const detail = envelope * (0.2 * Math.sin(t * TAU * 23) + 0.09 * Math.sin(t * TAU * 411));
  return HEIGHT / 2 - ((HEIGHT / 2 - 16) / 1.2) * (base + detail);
};

const project = (globalIndex: number) =>
  `${(globalIndex / TOTAL) * WIDTH},${curve(globalIndex / TOTAL)}`;

// Precompute every chunk's full and simplified polyline strings once (100k point
// projections at module load), so streaming and the serial swap stay cheap.
const FULL_PATHS: string[] = [];
const SIMPLE_PATHS: string[] = [];
for (let chunk = 0; chunk < CHUNK_COUNT; chunk += 1) {
  const start = chunk * CHUNK_SIZE;
  const full: string[] = [];
  for (let offset = 0; offset <= CHUNK_SIZE; offset += 1) {
    full.push(project(start + offset));
  }
  FULL_PATHS.push(full.join(' '));

  const simple: string[] = [];
  for (let step = 0; step < SIMPLE_PER_CHUNK; step += 1) {
    simple.push(project(start + Math.round((step / (SIMPLE_PER_CHUNK - 1)) * CHUNK_SIZE)));
  }
  SIMPLE_PATHS.push(simple.join(' '));
}

interface Chunk {
  index: number;
}

// Yield all 100 chunks at once, so the whole coarse chart is on screen before any
// detail swaps in.
const source: StreamSource<Chunk, void> = {
  mode: 'stream',
  async *stream(chunks, _options, signal) {
    if (signal.aborted) {
      return;
    }
    for (let index = 0; index < CHUNK_COUNT; index += 1) {
      chunks.push({ index });
    }
    yield;
  },
};

// Advances a front from 0 to `count`, one chunk per animation frame, so the detail
// swaps land serially as fast as the browser can paint them.
function useSerialFront(count: number): number {
  const [front, setFront] = React.useState(0);
  React.useEffect(() => {
    let raf = 0;
    let current = 0;
    const tick = () => {
      current += 1;
      setFront(current);
      if (current < count) {
        raf = requestAnimationFrame(tick);
      }
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [count]);
  return front;
}

function Segment({ index, detailed }: { index: number; detailed: boolean }) {
  return (
    <CoordinatedLazy
      ready={detailed}
      fallback={
        <polyline points={SIMPLE_PATHS[index]} fill="none" stroke="#cdbef0" strokeWidth={1} />
      }
      content={<polyline points={FULL_PATHS[index]} fill="none" stroke="#7c3aed" strokeWidth={1} />}
    />
  );
}

export default function Page() {
  const { chunks, Controller } = useStream<Chunk, void>({ source });
  const front = useSerialFront(CHUNK_COUNT);

  return (
    <Controller>
      <svg
        width={WIDTH}
        height={HEIGHT}
        style={{ border: '1px solid #d0cdd7', borderRadius: 8, background: '#faf9fc' }}
      >
        {chunks.map((chunk) => (
          // Detail sweeps right-to-left: the highest-index (rightmost) chunk swaps first.
          <Segment
            key={chunk.index}
            index={chunk.index}
            detailed={chunk.index >= CHUNK_COUNT - front}
          />
        ))}
      </svg>
    </Controller>
  );
}

Scatter chart

50,000 points tiled into adaptive square chunks (greedy, left-to-right top-to-bottom: each row's first chunk fixes the row height, the rest grow their width to a target point count). Each loading chunk clusters its points into representative dots sized by density; the clustered chart paints first, then each chunk swaps in its full points serially.

'use client';
import * as React from 'react';
import { ScatterChart, generateScatterPoints } from './scatterChart';

const points = generateScatterPoints(50_000);

export default function Page() {
  return (
    <ScatterChart.Root points={points}>
      <ScatterChart.Chunk>{(point) => <ScatterChart.Point point={point} />}</ScatterChart.Chunk>
    </ScatterChart.Root>
  );
}

100,000 points

The same scatter at double the point count — heavier, for comparing how the streaming and final render scale with dataset size (up to 100k SVG circles, intentionally a stress test).

'use client';
import * as React from 'react';
import { ScatterChart, generateScatterPoints } from './scatterChart';

const points = generateScatterPoints(100_000);

export default function Page() {
  return (
    <ScatterChart.Root points={points}>
      <ScatterChart.Chunk>{(point) => <ScatterChart.Point point={point} />}</ScatterChart.Chunk>
    </ScatterChart.Root>
  );
}