performance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePerformance Optimization
性能优化
Systematically identify and fix performance issues in $ARGUMENTS (or the whole app if no argument is given). Always measure first — never optimize blindly.
系统性地识别并修复**$ARGUMENTS**中的性能问题(若未指定参数则针对整个应用)。务必先进行测量——切勿盲目优化。
Step 1 — Measure baseline before touching anything
步骤1——修改前先测量基准值
Run the production build and capture metrics before making any changes:
bash
pnpm run build
pnpm run previewOpen the app in Chrome and capture:
- Lighthouse score (Performance tab → Run audit)
- React Profiler (React DevTools → Profiler → Record an interaction)
- Note the components with the longest render times and highest render counts
Record baseline numbers. Every optimization must be measured against these.
运行生产构建并在做出任何更改前捕获指标:
bash
pnpm run build
pnpm run preview在Chrome中打开应用并捕获:
- Lighthouse评分(性能标签页 → 运行审核)
- React Profiler(React DevTools → Profiler → 记录交互)
- 记录渲染时间最长和渲染次数最多的组件
记录基准数值。所有优化都必须以此为参照进行衡量。
Step 2 — Identify unnecessary re-renders
步骤2——识别不必要的重渲染
Read the component tree (start from ) and look for these patterns:
src/App.tsxInline object/array/function creation in JSX:
tsx
// BAD — new object on every render causes children to re-render
<Chart options={{ color: "red" }} />
// GOOD
const chartOptions = useMemo(() => ({ color: "red" }), []);
<Chart options={chartOptions} />Event handlers recreated on every render:
tsx
// BAD
<Button onClick={() => doSomething(id)} />
// GOOD
const handleClick = useCallback(() => doSomething(id), [id]);
<Button onClick={handleClick} />Context that changes on every render:
tsx
// BAD — new object reference every render
<MyContext.Provider value={{ user, sdk }}>
// GOOD — memoize the context value
const ctxValue = useMemo(() => ({ user, sdk }), [user, sdk]);
<MyContext.Provider value={ctxValue}>Search for common culprits:
bash
grep -rn --include="*.tsx" \
-E "value=\{\{|onClick=\{\(\)" src/Apply to pure presentational components that receive stable props. Do NOT wrap every component — only those confirmed to re-render unnecessarily via the Profiler.
React.memo查看组件树(从开始),寻找以下模式:
src/App.tsxJSX中内联创建对象/数组/函数:
tsx
// 不良写法——每次渲染都会创建新对象,导致子组件重渲染
<Chart options={{ color: "red" }} />
// 良好写法
const chartOptions = useMemo(() => ({ color: "red" }), []);
<Chart options={chartOptions} />每次渲染都会重新创建事件处理函数:
tsx
// 不良写法
<Button onClick={() => doSomething(id)} />
// 良好写法
const handleClick = useCallback(() => doSomething(id), [id]);
<Button onClick={handleClick} />每次渲染都会变化的Context:
tsx
// 不良写法——每次渲染都会创建新的对象引用
<MyContext.Provider value={{ user, sdk }}>
// 良好写法——对Context值进行记忆化处理
const ctxValue = useMemo(() => ({ user, sdk }), [user, sdk]);
<MyContext.Provider value={ctxValue}>搜索常见问题代码:
bash
grep -rn --include="*.tsx" \
-E "value=\{\{|onClick=\{\(\)" src/为接收稳定props的纯展示组件应用。请勿包裹所有组件——仅包裹那些经Profiler确认存在不必要重渲染的组件。
React.memoStep 3 — Optimize CDF data fetching
步骤3——优化CDF数据获取
Read all CDF SDK calls (search for , , , ).
sdk.client.useQueryuseCogniteClientFor each call, check:
| Issue | Fix |
|---|---|
No | Add |
| Fetching all properties | Add a |
| Fetching on every render | Move inside |
| Sequential requests that could be parallel | Use |
| Polling without debounce | Add debounce (300–500 ms) for search inputs before firing CDF queries |
Example — scoped instance query:
ts
// BAD — fetches everything
const result = await client.instances.list({ instanceType: "node" });
// GOOD — scoped and limited
const result = await client.instances.list({
instanceType: "node",
sources: [{ source: { type: "view", space: "my-space", externalId: "Asset", version: "v1" } }],
filter: { equals: { property: ["node", "space"], value: "my-space" } },
limit: 100,
});查看所有CDF SDK调用(搜索、、、)。
sdk.client.useQueryuseCogniteClient针对每个调用,检查以下内容:
| 问题 | 修复方案 |
|---|---|
未设置 | 添加 |
| 获取所有属性 | 添加 |
| 每次渲染都进行获取 | 将调用移至 |
| 可并行执行的串行请求 | 使用 |
| 无防抖的轮询 | 为搜索输入添加防抖(300–500毫秒)后再触发CDF查询 |
示例——限定范围的实例查询:
ts
// 不良写法——获取所有数据
const result = await client.instances.list({ instanceType: "node" });
// 良好写法——限定范围并设置数量限制
const result = await client.instances.list({
instanceType: "node",
sources: [{ source: { type: "view", space: "my-space", externalId: "Asset", version: "v1" } }],
filter: { equals: { property: ["node", "space"], value: "my-space" } },
limit: 100,
});Step 4 — Virtualize large lists
步骤4——虚拟化大型列表
Search for lists that render more than ~50 items:
bash
grep -rn --include="*.tsx" -E "\.(map|forEach)\(" src/For any list where the data source could exceed 50 items, replace the plain render with a virtualized list. Use (already a Dune ecosystem dependency):
.map()@tanstack/react-virtualtsx
import { useVirtualizer } from "@tanstack/react-virtual";
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: rowVirtualizer.getTotalSize() }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{ transform: `translateY(${virtualRow.start}px)` }}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);搜索渲染超过约50条数据的列表:
bash
grep -rn --include="*.tsx" -E "\.(map|forEach)\(" src/对于数据源可能超过50条的列表,将普通的渲染替换为虚拟化列表。使用(已作为Dune生态系统依赖):
.map()@tanstack/react-virtualtsx
import { useVirtualizer } from "@tanstack/react-virtual";
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: rowVirtualizer.getTotalSize() }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{ transform: `translateY(${virtualRow.start}px)` }}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);Step 5 — Code-split heavy pages and components
步骤5——代码分割重型页面和组件
Any page or modal that is not needed on the initial load should be lazy-loaded. Read the router setup and identify routes that are imported statically but not shown on the landing page.
tsx
// BEFORE
import { ReportPage } from "./pages/ReportPage";
// AFTER
import { lazy, Suspense } from "react";
const ReportPage = lazy(() => import("./pages/ReportPage"));
// In the route:
<Suspense fallback={<PageSkeleton />}>
<ReportPage />
</Suspense>Similarly, large third-party components (chart libraries, PDF viewers, map renderers) should be dynamically imported inside the component that needs them, not at the module level.
任何初始加载时不需要的页面或模态框都应懒加载。查看路由配置,识别那些被静态导入但不在首页显示的路由。
tsx
// 优化前
import { ReportPage } from "./pages/ReportPage";
// 优化后
import { lazy, Suspense } from "react";
const ReportPage = lazy(() => import("./pages/ReportPage"));
// 在路由中使用:
<Suspense fallback={<PageSkeleton />}>
<ReportPage />
</Suspense>同样,大型第三方组件(图表库、PDF查看器、地图渲染器)应在需要它们的组件内部动态导入,而非在模块级别导入。
Step 6 — Analyse and reduce bundle size
步骤6——分析并缩小包体积
bash
undefinedbash
undefinedInstall if not already present, then run
若尚未安装则先安装,然后运行
pnpm add -D rollup-plugin-visualizer
Add to `vite.config.ts` temporarily:
```ts
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
react(),
visualizer({ open: true, gzipSize: true, brotliSize: true }),
],
});Run and inspect the treemap. Flag any chunk > 100 KB (gzipped) that is not a necessary initial dependency.
pnpm run buildCommon wins:
- Replace with individual
lodashimports or native equivalentslodash-es - Replace with
momentor nativedate-fnsIntl - Ensure chart libraries (e.g., ECharts, Recharts) are tree-shaken correctly
Revert the visualizer plugin after analysis.
pnpm add -D rollup-plugin-visualizer
临时添加到`vite.config.ts`中:
```ts
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
react(),
visualizer({ open: true, gzipSize: true, brotliSize: true }),
],
});运行并查看树形图。标记任何大于100 KB(gzip压缩后)且非必要初始依赖的代码块。
pnpm run build常见优化点:
- 将替换为单独的
lodash导入或原生等效方法lodash-es - 将替换为
moment或原生date-fnsIntl - 确保图表库(如ECharts、Recharts)被正确摇树优化
分析完成后移除visualizer插件。
Step 7 — Fix memory leaks
步骤7——修复内存泄漏
Search for hooks that set up subscriptions, timers, or event listeners without cleanup:
useEffectbash
grep -rn --include="*.tsx" --include="*.ts" -A 10 "useEffect" src/Every that calls , , , , or sets up a CDF streaming connection must return a cleanup function:
useEffectaddEventListenersetIntervalsetTimeoutsubscribets
useEffect(() => {
const controller = new AbortController();
fetchData(controller.signal);
return () => controller.abort();
}, [id]);搜索钩子中设置了订阅、定时器或事件监听器但未清理的代码:
useEffectbash
grep -rn --include="*.tsx" --include="*.ts" -A 10 "useEffect" src/所有调用、、、或建立CDF流式连接的都必须返回清理函数:
addEventListenersetIntervalsetTimeoutsubscribeuseEffectts
useEffect(() => {
const controller = new AbortController();
fetchData(controller.signal);
return () => controller.abort();
}, [id]);Step 8 — Measure after and report
步骤8——优化后测量并报告
Re-run the same Lighthouse audit and React Profiler session from Step 1. Report the delta:
| Metric | Before | After | Change |
|---|---|---|---|
| Lighthouse Performance | 72 | 91 | +19 |
| Largest Contentful Paint | 3.2 s | 1.8 s | −1.4 s |
| Total Blocking Time | 420 ms | 80 ms | −340 ms |
| Bundle size (gzipped) | 410 KB | 290 KB | −120 KB |
| 8 | 2 | −6 |
If a step produced no improvement, state that explicitly. Do not fabricate numbers.
重新运行步骤1中的Lighthouse审核和React Profiler会话。报告差值:
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| Lighthouse性能评分 | 72 | 91 | +19 |
| 最大内容绘制 | 3.2 s | 1.8 s | −1.4 s |
| 总阻塞时间 | 420 ms | 80 ms | −340 ms |
| 包体积(gzip压缩后) | 410 KB | 290 KB | −120 KB |
| 8 | 2 | −6 |
若某一步骤未带来提升,请明确说明。请勿编造数据。
Done
完成
List every change made with the file path and a one-line explanation. If further gains require server-side or infrastructure changes (e.g., CDF response caching, CDN configuration), note them separately as out-of-scope recommendations.
列出所有已做的更改,包含文件路径和一行说明。若进一步优化需要服务端或基础设施变更(如CDF响应缓存、CDN配置),请单独列出这些超出范围的建议。