TuBrief
구독 채널
비디오
커뮤니티

如何用 TanStack Charts 瘦身因 Recharts 而臃肿的仪表盘包体积

TuBrief 편집팀
2026년 8월 12일
0
Computing/Software

원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.

中文한국어EnglishEspañolالعربيةहिन्दीDeutschFrançaisPortuguêsРусскийBahasa Indonesia日本語

관련 영상

TanStack 又双叒叕出手了... 它们把图表给整明白了8:14

TanStack 又双叒叕出手了... 它们把图表给整明白了

Better Stack

커뮤니티의 다른 글

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

2026년 9월 13일

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

2026년 9월 13일

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

2026년 9월 13일

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

2026년 9월 13일

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

2026년 9월 12일

Apple Won the AI Race

2026년 9월 12일

댓글 (0)

Log in to leave a comment

아직 작성된 글이 없습니다

© 2026 . All rights reserved.

TuBrief
구독 채널
비디오
커뮤니티
로그인

如何用 TanStack Charts 瘦身因 Recharts 而臃肿的仪表盘包体积

一进入仪表盘页面,光标却因加载图表而转个不停,十有八九是 Recharts 的问题。仅仅加了几个图表,包体积就暴增好几百 KB,这让人不禁叹气。Recharts 与 React DOM 协调周期的绑定过于紧密,UI 结构和数据处理交织在一起,很难轻量化地使用。

在这种情况下,TanStack Charts 是一个不错的替代方案。它将可视化逻辑独立抽离到了基于图形语法的核心中。Recharts 通常会占用超过 150KiB,而 TanStack Charts 仅在 40KiB 左右。包体积缩小了近 70%。

问题在于生产代码的迁移。推翻原本运行良好的页面总是让人心存顾虑。为了说服团队成员并实现无故障转换,我们需要明确的步骤。

安全移除 Recharts 代码

如果试图一次性全部替换,将很难排查出问题出在哪里。必须挑选一个组件,将其严格拆分为 4 个步骤进行过渡。

  1. 分离数据预处理: 将原本混杂在 Recharts 组件 JSX 内部的数据处理逻辑提取为纯函数移至外部。
  2. 标记映射: 将坐标轴设置以及线条、点等视觉元素移动到 defineChart 声明对象的 marks 数组中。
  3. 替换工具: 将 ResponsiveContainer 和 Tooltip 替换为 TanStack 的自动缩放器以及 @tanstack/charts/tooltip 模块。
  4. 记忆化(Memoization): 用 useMemo 包裹图表定义对象,并将其传递给最顶层的 Chart 组件。

只要遵循这个顺序,就能将替换第一个图表组件所需的时间缩减一半。

`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 }}>


);
}

`

适配暗黑模式时避免 JavaScript 重新渲染

每次切换暗黑模式时,整个图表组件都要重新绘制,这显得不够优雅。TanStack Charts 直接支持 CSS 自定义属性和 currentColor,而不是硬编码的颜色值。

不要用 JavaScript 状态来管理图表颜色,而是将它们推入 CSS 变量中。只需让图表默认调色板引用从 --ts-chart-1 到 --ts-chart-6 即可。

  1. 在 CSS 根目录和 [data-theme='dark'] 中预先定义图表所需的变量。
  2. 在 defineChart 选项的 stroke 或 fill 中传入形如 var(--ts-chart-1) 的字符串。
  3. 提示框(Tooltip)样式也通过 CSS 类进行管理,并将背景色作为变量处理。

这样编写后,当设计师要求更换图表颜色时,就无需修改 TSX 文件,只需修改 CSS 即可大功告成。

`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;
}

`

即使丢入 10 万条数据也不会崩溃的秘诀

如果用 SVG 渲染 10 万个数据点,就会生成 10 万个 DOM 节点,浏览器线程根本无法承受,卡顿肉眼可见。

解决方案是将两种方法结合起来:转换为 Canvas 渲染器以消除 DOM 生成开销,并使用 LTTB(Largest Triangle Three Buckets,最大三角形三桶)算法压缩屏幕像素宽度范围内无意义的密集数据。LTTB 能够在保留数据尖锐特征(最大值、最小值)的同时减少点的数量。

`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;
}

`

在处理大量时间序列数据时,请使用 LTTB 将其精简至 1000 个左右再传入。接着开启 spatialIndex: true 选项以降低鼠标指针检测的复杂度。最后,挂载 @tanstack/charts/react/canvas 包中的 Canvas 渲染器,便能轻松跑满 60fps。

如何让 Cursor 或 Copilot 代劳编写图表代码

像 Recharts 这样 JSX 标签深度嵌套的形态,很容易导致 AI 助手产生幻觉,例如创造出奇怪的属性或者忘记闭合标签。

像 TanStack Charts 这样采用对象声明式编写的库,只要提供良好的类型架构,AI 就能生成干净的代码。在项目中定义如下的架构类型,并将其放入 AI 提示词或规则文件(.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;
};
}

`

如果在提示词中写明诸如“从 @tanstack/charts/scales/linear 等精确子路径导入”的规则以及 useMemo 的使用规范,那么 AI 输出的代码就可以直接使用,无需修改。