performance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePerformance Fix
性能修复
Systematically find 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 fix must be measured against these.
运行生产构建并在修改前捕获指标:
bash
pnpm run build
pnpm run preview在Chrome中打开应用并捕获:
- Lighthouse分数(性能标签页 → 运行审核)
- React Profiler(React DevTools → Profiler → 记录交互)
- 记录渲染时间最长和渲染次数最多的组件
记录基准数值。每一项修复都必须与这些数值对比。
Step 2 — Find and fix unnecessary re-renders
步骤2 — 查找并修复不必要的重渲染
Read the component tree (start from ) and search for these patterns:
src/App.tsxbash
grep -rn --include="*.tsx" \
-E "value=\{\{|onClick=\{\(\)" src/For each instance found, apply the fix directly:
Inline object/array creation in JSX → wrap with :
useMemotsx
// BAD — new object on every render causes children to re-render
<Chart options={{ color: "red" }} />
// FIX — wrap with useMemo
const chartOptions = useMemo(() => ({ color: "red" }), []);
<Chart options={chartOptions} />Event handlers recreated on every render → wrap with :
useCallbacktsx
// BAD
<Button onClick={() => doSomething(id)} />
// FIX — wrap with useCallback
const handleClick = useCallback(() => doSomething(id), [id]);
<Button onClick={handleClick} />Context that changes on every render → memoize the context value:
tsx
// BAD — new object reference every render
<MyContext.Provider value={{ user, sdk }}>
// FIX — memoize the context value
const ctxValue = useMemo(() => ({ user, sdk }), [user, sdk]);
<MyContext.Provider value={ctxValue}>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.tsxbash
grep -rn --include="*.tsx" \
-E "value=\{\{|onClick=\{\(\)" src/针对每个找到的实例,直接应用修复:
JSX中的内联对象/数组创建 → 用包裹:
useMemotsx
// BAD — new object on every render causes children to re-render
<Chart options={{ color: "red" }} />
// FIX — wrap with useMemo
const chartOptions = useMemo(() => ({ color: "red" }), []);
<Chart options={chartOptions} />每次渲染都会重新创建的事件处理函数 → 用包裹:
useCallbacktsx
// BAD
<Button onClick={() => doSomething(id)} />
// FIX — wrap with useCallback
const handleClick = useCallback(() => doSomething(id), [id]);
<Button onClick={handleClick} />每次渲染都会变化的Context → 对Context值进行memo化:
tsx
// BAD — new object reference every render
<MyContext.Provider value={{ user, sdk }}>
// FIX — memoize the context value
const ctxValue = useMemo(() => ({ user, sdk }), [user, sdk]);
<MyContext.Provider value={ctxValue}>将应用于接收稳定props的纯展示组件。切勿包裹所有组件——仅包裹那些经Profiler确认存在不必要重渲染的组件。
React.memoStep 3 — Find and fix DMS query patterns
步骤3 — 查找并修复DMS查询模式
For read-heavy workloads, prefer APIs that hit the search/Elasticsearch path ( or on instances) rather than paths that stress Postgres.
querysearchlistbash
undefined对于读密集型工作负载,优先选择调用搜索/Elasticsearch路径的API(实例上的或),而非会给Postgres带来压力的路径。
querysearchlistbash
undefinedFind all DMS instance API calls
Find all DMS instance API calls
grep -rn --include=".ts" --include=".tsx" -E "instances.(list|search|query|aggregate|retrieve)" src/
grep -rn --include=".ts" --include=".tsx" -E "instances.(list|search|query|aggregate|retrieve)" src/
Find direct SDK calls to other CDF resources
Find direct SDK calls to other CDF resources
grep -rn --include=".ts" --include=".tsx" -E ".(assets|timeseries|events|files|sequences|relationships).(list|search|retrieve)" src/
For each `instances.list` call in a read-heavy path (e.g. populating a table, dropdown, or search results), **rewrite it to use `instances.query`** with the equivalent filter. Preserve the existing filter logic but express it in the query API format:
```ts
// BAD — instances.list hits Postgres, expensive for read-heavy UI
const result = await client.instances.list({
instanceType: "node",
filter: { equals: { property: ["node", "space"], value: "my-space" } },
limit: 100,
});
// FIX — rewrite to instances.query which hits Elasticsearch
const result = await client.instances.query({
with: {
nodes: {
nodes: {
filter: { equals: { property: ["node", "space"], value: "my-space" } },
},
limit: 100,
},
},
select: {
nodes: {},
},
});| API used | When it's correct | When to rewrite |
|---|---|---|
| Read with filters that map to Elasticsearch (text, equals, range) | — |
| Full-text or fuzzy search | — |
| Writing, syncing, or need for semantics not available on query/search | Rewrite to |
| Fetching by known external IDs | — |
| Counts, histograms | — |
For deeper rationale on search vs relational paths, cardinality, and materialization tradeoffs, consult the directory if available in the workspace.
semantic-knowledge/grep -rn --include=".ts" --include=".tsx" -E ".(assets|timeseries|events|files|sequences|relationships).(list|search|retrieve)" src/
针对读密集型路径(例如填充表格、下拉菜单或搜索结果)中的每个`instances.list`调用,**将其重写为使用`instances.query`**并保留等效的过滤逻辑,但以查询API的格式表达:
```ts
// BAD — instances.list hits Postgres, expensive for read-heavy UI
const result = await client.instances.list({
instanceType: "node",
filter: { equals: { property: ["node", "space"], value: "my-space" } },
limit: 100,
});
// FIX — rewrite to instances.query which hits Elasticsearch
const result = await client.instances.query({
with: {
nodes: {
nodes: {
filter: { equals: { property: ["node", "space"], value: "my-space" } },
},
limit: 100,
},
},
select: {
nodes: {},
},
});| 使用的API | 适用场景 | 需要重写的场景 |
|---|---|---|
| 适用于可映射到Elasticsearch的过滤条件(文本、等值、范围) | — |
| 全文搜索或模糊搜索 | — |
| 写入、同步操作,或需要query/search不支持的语义时 | 若用于读密集型UI展示,重写为 |
| 根据已知外部ID获取数据 | — |
| 统计、直方图 | — |
若想深入了解搜索路径与关系路径的区别、基数以及物化权衡,可查阅工作区中若存在的目录。
semantic-knowledge/Step 4 — Find and fix client-side filtering (move to server-side)
步骤4 — 查找并修复客户端过滤(迁移至服务端)
Filters, limits, and projections must be applied in the API request — not by downloading large result sets and filtering in the browser.
bash
undefined过滤、限制和投影必须在API请求中应用——而非下载大量结果集后在浏览器中过滤。
bash
undefinedFind client-side filtering after data fetch (common anti-pattern)
Find client-side filtering after data fetch (common anti-pattern)
grep -rn --include=".ts" --include=".tsx" -B 5 ".filter(" src/ | grep -B 5 "data|items|result|response|nodes"
grep -rn --include=".ts" --include=".tsx" -B 5 ".filter(" src/ | grep -B 5 "data|items|result|response|nodes"
Find .map() or .reduce() on full datasets that suggest client-side processing
Find .map() or .reduce() on full datasets that suggest client-side processing
grep -rn --include=".ts" --include=".tsx" -E ".(map|reduce|find|some|every)(" src/hooks/ src/services/ src/api/
For each client-side filter pattern, **move the filter logic into the SDK call's `filter` parameter and remove the `.filter()` call**:
```ts
// BAD — fetches all nodes then filters client-side
const result = await client.instances.query({ ... });
const activeNodes = result.items.nodes.filter(n => n.properties.status === "active");
// FIX — move filter into the API request, remove client-side .filter()
const result = await client.instances.query({
with: {
nodes: {
nodes: {
filter: {
and: [
existingFilters,
{ equals: { property: ["mySpace", "myView/v1", "status"], value: "active" } },
],
},
},
limit: 100,
},
},
select: { nodes: {} },
});
const activeNodes = result.items.nodes; // no client-side filter needed| Issue | Fix |
|---|---|
| Move the filter into the API request's |
No | Add a |
| Fetching all items then rendering a subset | Add |
| Client-side text search on fetched array | Replace with the SDK's |
Hard rule: If the API supports a filter for the criterion being applied client-side, move it server-side now. Client-side filtering is acceptable only for trivial local state (e.g. filtering a cached list of 10 user preferences). If the API does not support the exact filter, add a code comment explaining why client-side filtering is necessary.
grep -rn --include=".ts" --include=".tsx" -E ".(map|reduce|find|some|every)(" src/hooks/ src/services/ src/api/
针对每个客户端过滤模式,**将过滤逻辑移入SDK调用的`filter`参数中,并移除`.filter()`调用**:
```ts
// BAD — fetches all nodes then filters client-side
const result = await client.instances.query({ ... });
const activeNodes = result.items.nodes.filter(n => n.properties.status === "active");
// FIX — move filter into the API request, remove client-side .filter()
const result = await client.instances.query({
with: {
nodes: {
nodes: {
filter: {
and: [
existingFilters,
{ equals: { property: ["mySpace", "myView/v1", "status"], value: "active" } },
],
},
},
limit: 100,
},
},
select: { nodes: {} },
});
const activeNodes = result.items.nodes; // no client-side filter needed| 问题 | 修复方案 |
|---|---|
SDK调用后对完整结果集使用 | 将过滤逻辑移入API请求的 |
DMS查询中未选择 | 添加 |
| 获取所有条目后仅渲染子集 | 在API调用中添加 |
| 对获取的数组进行客户端文本搜索 | 替换为SDK的 |
硬性规则:若API支持客户端所应用的过滤条件,立即迁移至服务端。仅当处理 trivial 的本地状态(例如过滤包含10个用户偏好的缓存列表)时,客户端过滤才是可接受的。若API不支持精确的过滤条件,添加代码注释说明客户端过滤的必要性。
Step 5 — Find and fix CDF data fetching and pagination
步骤5 — 查找并修复CDF数据获取与分页
Read all CDF SDK calls (search for , , , ).
sdk.client.useQueryuseCogniteClientbash
undefined查看所有CDF SDK调用(搜索、、、)。
sdk.client.useQueryuseCogniteClientbash
undefinedFind pagination patterns
Find pagination patterns
grep -rn --include=".ts" --include=".tsx" -E "(nextCursor|cursor|hasNextPage|fetchNextPage|offset|skip|page)" src/
grep -rn --include=".ts" --include=".tsx" -E "(nextCursor|cursor|hasNextPage|fetchNextPage|offset|skip|page)" src/
Find "fetch all" loops
Find "fetch all" loops
grep -rn --include=".ts" --include=".tsx" -B 3 -A 3 "while.*cursor|while.*hasMore|while.*nextPage" src/
For each call, find the issue and **apply the fix**:
| Issue | Fix to apply |
|-------|-------------|
| No `limit` set | **Add `limit: 100`** (or the actual page size needed) to the SDK call |
| Fetching all properties | **Add a `properties` filter** to select only required fields |
| Fetching on every render | **Move inside `useQuery`/`useMemo`** with a stable dependency array |
| Sequential requests that could be parallel | **Rewrite to `Promise.all`** or batched SDK methods |
| Missing `limit` parameter | **Add explicit `limit`** matching the UI's page size (e.g. 25, 50, 100) |
| Offset-based pagination for large datasets | **Replace with cursor-based pagination** using `nextCursor` from the response |
| "Fetch all" loop (exhausts cursors up front) | **Replace with on-demand pagination** using TanStack Query's `useInfiniteQuery` |
**Fixing fetch-all loops** — replace the while loop with `useInfiniteQuery`:
```ts
// BAD — fetches ALL pages before rendering
let allItems = [];
let cursor = undefined;
while (true) {
const result = await client.instances.list({ limit: 1000, cursor });
allItems.push(...result.items);
if (!result.nextCursor) break;
cursor = result.nextCursor;
}
// FIX — paginate on demand with useInfiniteQuery
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["instances", filters],
queryFn: ({ pageParam }) =>
client.instances.list({ limit: 100, cursor: pageParam, ...filters }),
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
staleTime: 30_000,
});Fixing offset-based pagination — switch to cursor-based:
ts
// BAD — offset pagination degrades at scale
const result = await client.instances.list({ limit: 100, offset: page * 100 });
// FIX — cursor-based pagination
const result = await client.instances.list({ limit: 100, cursor: nextCursor });grep -rn --include=".ts" --include=".tsx" -B 3 -A 3 "while.*cursor|while.*hasMore|while.*nextPage" src/
针对每个调用,找出问题并**应用修复**:
| 问题 | 修复方案 |
|-------|-------------|
| 未设置`limit` | **添加`limit: 100`**(或实际所需的页面大小)到SDK调用中 |
| 获取所有属性 | **添加`properties`过滤器**仅选择所需字段 |
| 每次渲染都获取数据 | **移入`useQuery`/`useMemo`中**并使用稳定的依赖数组 |
| 可并行的串行请求 | **重写为`Promise.all`**或批量SDK方法 |
| 缺失`limit`参数 | **添加显式`limit`**匹配UI的页面大小(例如25、50、100) |
| 大型数据集使用基于偏移量的分页 | **替换为基于游标(cursor)的分页**,使用响应中的`nextCursor` |
| "获取全部"循环(预先遍历所有游标) | **替换为按需分页**,使用TanStack Query的`useInfiniteQuery` |
**修复"获取全部"循环**——用`useInfiniteQuery`替换while循环:
```ts
// BAD — fetches ALL pages before rendering
let allItems = [];
let cursor = undefined;
while (true) {
const result = await client.instances.list({ limit: 1000, cursor });
allItems.push(...result.items);
if (!result.nextCursor) break;
cursor = result.nextCursor;
}
// FIX — paginate on demand with useInfiniteQuery
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["instances", filters],
queryFn: ({ pageParam }) =>
client.instances.list({ limit: 100, cursor: pageParam, ...filters }),
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
staleTime: 30_000,
});修复基于偏移量的分页——切换为基于游标的分页:
ts
// BAD — offset pagination degrades at scale
const result = await client.instances.list({ limit: 100, offset: page * 100 });
// FIX — cursor-based pagination
const result = await client.instances.list({ limit: 100, cursor: nextCursor });Step 6 — Find and fix excessive API call rates
步骤6 — 查找并修复过高的API调用频率
bash
undefinedbash
undefinedFind search/filter inputs that trigger queries
Find search/filter inputs that trigger queries
grep -rn --include=".tsx" --include=".ts" -E "onChange|onInput|onSearch|onFilter" src/ | grep -i "search|filter|query"
grep -rn --include=".tsx" --include=".ts" -E "onChange|onInput|onSearch|onFilter" src/ | grep -i "search|filter|query"
Find debounce usage
Find debounce usage
grep -rn --include=".ts" --include=".tsx" -i -E "debounce|useDebouncedValue|useDebounce" src/
grep -rn --include=".ts" --include=".tsx" -i -E "debounce|useDebouncedValue|useDebounce" src/
Find polling/interval patterns
Find polling/interval patterns
grep -rn --include=".ts" --include=".tsx" -E "setInterval|refetchInterval|pollingInterval|refetchOnWindowFocus" src/
grep -rn --include=".ts" --include=".tsx" -E "setInterval|refetchInterval|pollingInterval|refetchOnWindowFocus" src/
Find useQuery options that control refetch behavior
Find useQuery options that control refetch behavior
grep -rn --include=".ts" --include=".tsx" -E "staleTime|cacheTime|gcTime|refetchOnMount|refetchOnWindowFocus" src/
For each issue found, **apply the fix**:
**Search inputs that fire on every keystroke → add debounce with 300ms delay:**
```tsx
// BAD — fires API call on every keystroke
const [search, setSearch] = useState("");
const { data } = useQuery({ queryKey: ["search", search], queryFn: () => api.search(search) });
// FIX — create or use a useDebouncedValue hook with 300ms delay
function useDebouncedValue<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
const [search, setSearch] = useState("");
const debouncedSearch = useDebouncedValue(search, 300);
const { data } = useQuery({
queryKey: ["search", debouncedSearch],
queryFn: () => api.search(debouncedSearch),
enabled: debouncedSearch.length > 0,
});useQuery calls without staleTime → add appropriate staleTime:
ts
// BAD — refetches on every mount/focus
useQuery({ queryKey: ["data"], queryFn: fetchData });
// FIX — add staleTime to prevent unnecessary refetches
useQuery({ queryKey: ["data"], queryFn: fetchData, staleTime: 30_000 });Duplicate parallel identical requests → lift the query to a shared hook:
ts
// BAD — multiple components each call the same query independently
// ComponentA.tsx: useQuery({ queryKey: ["assets"], queryFn: fetchAssets });
// ComponentB.tsx: useQuery({ queryKey: ["assets"], queryFn: fetchAssets });
// FIX — create a shared hook, import it from both components
// hooks/useAssets.ts
export function useAssets() {
return useQuery({ queryKey: ["assets"], queryFn: fetchAssets, staleTime: 30_000 });
}| Issue | Fix to apply |
|---|---|
| Search input fires query on every keystroke | Add |
| Polling with no backoff or very short interval | Set interval to ≥30s with exponential backoff on errors |
| Re-fetching on every render (no caching) | Add |
| Set |
| Duplicate parallel identical requests | Lift the query to a shared hook and import from both components |
| Multiple components triggering the same fetch | Extract to a shared hook in |
grep -rn --include=".ts" --include=".tsx" -E "staleTime|cacheTime|gcTime|refetchOnMount|refetchOnWindowFocus" src/
针对每个找到的问题,**应用修复**:
**每次按键都会触发查询的搜索输入 → 添加300ms延迟的防抖:**
```tsx
// BAD — fires API call on every keystroke
const [search, setSearch] = useState("");
const { data } = useQuery({ queryKey: ["search", search], queryFn: () => api.search(search) });
// FIX — create or use a useDebouncedValue hook with 300ms delay
function useDebouncedValue<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
const [search, setSearch] = useState("");
const debouncedSearch = useDebouncedValue(search, 300);
const { data } = useQuery({
queryKey: ["search", debouncedSearch],
queryFn: () => api.search(debouncedSearch),
enabled: debouncedSearch.length > 0,
});未设置staleTime的useQuery调用 → 添加合适的staleTime:
ts
// BAD — refetches on every mount/focus
useQuery({ queryKey: ["data"], queryFn: fetchData });
// FIX — add staleTime to prevent unnecessary refetches
useQuery({ queryKey: ["data"], queryFn: fetchData, staleTime: 30_000 });重复的并行相同请求 → 将查询提升至共享hook:
ts
// BAD — multiple components each call the same query independently
// ComponentA.tsx: useQuery({ queryKey: ["assets"], queryFn: fetchAssets });
// ComponentB.tsx: useQuery({ queryKey: ["assets"], queryFn: fetchAssets });
// FIX — create a shared hook, import it from both components
// hooks/useAssets.ts
export function useAssets() {
return useQuery({ queryKey: ["assets"], queryFn: fetchAssets, staleTime: 30_000 });
}| 问题 | 修复方案 |
|---|---|
| 搜索输入每次按键都触发查询 | 添加 |
| 无退避策略或间隔极短的轮询 | 将间隔设置为≥30s,并在出错时使用指数退避 |
| 每次渲染都重新获取(无缓存) | 添加 |
昂贵查询设置 | **设置 |
| 重复的并行相同请求 | 将查询提升至共享hook,并在两个组件中导入 |
| 多个组件触发相同的获取操作 | 提取至 |
Step 7 — Find and fix large un-virtualized lists
步骤7 — 查找并修复未虚拟化的大型列表
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. Install if not present:
.map()@tanstack/react-virtualbash
pnpm add @tanstack/react-virtualApply the virtualizer pattern directly:
tsx
// BAD — renders all items in the DOM
<div>
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
// FIX — replace with virtualized list
import { useVirtualizer } from "@tanstack/react-virtual";
const parentRef = useRef<HTMLDivElement>(null);
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(), position: "relative" }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);搜索渲染超过约50条数据的列表:
bash
grep -rn --include="*.tsx" -E "\.(map|forEach)\(" src/对于任何数据源可能超过50条的列表,将普通的渲染替换为虚拟化列表。若未安装则先安装:
.map()@tanstack/react-virtualbash
pnpm add @tanstack/react-virtual直接应用虚拟化模式:
tsx
// BAD — renders all items in the DOM
<div>
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
// FIX — replace with virtualized list
import { useVirtualizer } from "@tanstack/react-virtual";
const parentRef = useRef<HTMLDivElement>(null);
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(), position: "relative" }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);Step 8 — Find and fix missing code splitting
步骤8 — 查找并修复缺失的代码分割
Read the router setup and identify routes that are imported statically but not shown on the landing page.
For each statically imported heavy page, convert to lazy import with and :
React.lazy()Suspensetsx
// BAD — statically imported, loaded in initial bundle
import { ReportPage } from "./pages/ReportPage";
// FIX — convert to lazy import
import { lazy, Suspense } from "react";
const ReportPage = lazy(() => import("./pages/ReportPage"));
// In the route — wrap with Suspense
<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. Apply the transformation directly to each heavy import found.
查看路由设置,找出那些被静态导入但不在首页展示的路由。
针对每个静态导入的重型页面,转换为使用和的懒导入:
React.lazy()Suspensetsx
// BAD — statically imported, loaded in initial bundle
import { ReportPage } from "./pages/ReportPage";
// FIX — convert to lazy import
import { lazy, Suspense } from "react";
const ReportPage = lazy(() => import("./pages/ReportPage"));
// In the route — wrap with Suspense
<Suspense fallback={<PageSkeleton />}>
<ReportPage />
</Suspense>同样,大型第三方组件(图表库、PDF查看器、地图渲染器)应在需要它们的组件内部动态导入,而非在模块级别导入。对每个找到的重型导入直接应用转换。
Step 9 — Analyse and fix bundle size
步骤9 — 分析并修复包体积
bash
undefinedbash
undefinedInstall if not already present, then run
Install 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. For any chunk > 100 KB (gzipped) that is not a necessary initial dependency, apply the fix:
pnpm run build| Issue | Fix to apply |
|---|---|
| Replace with |
| Replace with |
| Chart libraries not tree-shaken | Switch to named imports (e.g., |
| Large library used in one place | Dynamically import it with |
ts
// BAD
import _ from "lodash";
const sorted = _.sortBy(items, "name");
// FIX — use lodash-es or native
import sortBy from "lodash-es/sortBy";
const sorted = sortBy(items, "name");
// OR native:
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));ts
// BAD
import moment from "moment";
const formatted = moment(date).format("YYYY-MM-DD");
// FIX — use date-fns
import { format } from "date-fns";
const formatted = format(date, "yyyy-MM-dd");After analysis, remove the visualizer plugin from and uninstall it:
vite.config.tsbash
pnpm remove rollup-plugin-visualizerpnpm 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压缩后)且非必要初始依赖的chunk,应用修复:
pnpm run build| 问题 | 修复方案 |
|---|---|
| **替换为 |
| **替换为 |
| 未进行树摇的图表库 | 切换为命名导入(例如 |
| 仅在一处使用的大型库 | 用 |
ts
// BAD
import _ from "lodash";
const sorted = _.sortBy(items, "name");
// FIX — use lodash-es or native
import sortBy from "lodash-es/sortBy";
const sorted = sortBy(items, "name");
// OR native:
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));ts
// BAD
import moment from "moment";
const formatted = moment(date).format("YYYY-MM-DD");
// FIX — use date-fns
import { format } from "date-fns";
const formatted = format(date, "yyyy-MM-dd");分析完成后,从中移除visualizer插件并卸载:
vite.config.tsbash
pnpm remove rollup-plugin-visualizerStep 10 — Find and fix memory leaks
步骤10 — 查找并修复内存泄漏
Search for hooks that set up subscriptions, timers, or event listeners without cleanup:
useEffectbash
grep -rn --include="*.tsx" --include="*.ts" -A 10 "useEffect" src/For every that calls , , , , or sets up a CDF streaming connection, add the missing cleanup function:
useEffectaddEventListenersetIntervalsetTimeoutsubscribeFetch without abort → add AbortController:
ts
// BAD — no cleanup, fetch continues after unmount
useEffect(() => {
fetchData(id);
}, [id]);
// FIX — add AbortController for cleanup
useEffect(() => {
const controller = new AbortController();
fetchData(id, controller.signal);
return () => controller.abort();
}, [id]);Timer without cleanup → add clearInterval/clearTimeout:
ts
// BAD — interval keeps running after unmount
useEffect(() => {
const id = setInterval(() => poll(), 5000);
}, []);
// FIX — add clearInterval cleanup
useEffect(() => {
const id = setInterval(() => poll(), 5000);
return () => clearInterval(id);
}, []);Event listener without cleanup → add removeEventListener:
ts
// BAD — listener accumulates on each render
useEffect(() => {
window.addEventListener("resize", handleResize);
}, []);
// FIX — add removeEventListener cleanup
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);搜索未清理订阅、定时器或事件监听器的 hook:
useEffectbash
grep -rn --include="*.tsx" --include="*.ts" -A 10 "useEffect" src/对于每个调用、、、或建立CDF流连接的,添加缺失的清理函数:
addEventListenersetIntervalsetTimeoutsubscribeuseEffect未终止的Fetch请求 → 添加AbortController:
ts
// BAD — no cleanup, fetch continues after unmount
useEffect(() => {
fetchData(id);
}, [id]);
// FIX — add AbortController for cleanup
useEffect(() => {
const controller = new AbortController();
fetchData(id, controller.signal);
return () => controller.abort();
}, [id]);未清理的定时器 → 添加clearInterval/clearTimeout:
ts
// BAD — interval keeps running after unmount
useEffect(() => {
const id = setInterval(() => poll(), 5000);
}, []);
// FIX — add clearInterval cleanup
useEffect(() => {
const id = setInterval(() => poll(), 5000);
return () => clearInterval(id);
}, []);未清理的事件监听器 → 添加removeEventListener:
ts
// BAD — listener accumulates on each render
useEffect(() => {
window.addEventListener("resize", handleResize);
}, []);
// FIX — add removeEventListener cleanup
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);Step 11 — Measure after and report the delta
步骤11 — 优化后测量并报告差值
Re-run the same Lighthouse audit and React Profiler session from Step 1. Report the delta and list every file changed:
| 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 |
| 最大内容绘制(LCP) | 3.2 s | 1.8 s | −1.4 s |
| 总阻塞时间(TBT) | 420 ms | 80 ms | −340 ms |
| 包体积(gzip压缩后) | 410 KB | 290 KB | −120 KB |
| 8 | 2 | −6 |
若某一步骤未带来性能提升,需明确说明。切勿编造数据。
Done
完成
List every file changed with the absolute path and a one-line explanation of what was fixed. 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配置),需单独注明为超出范围的建议。