performance

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Performance 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 preview
Open 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
src/App.tsx
) and search for these patterns:
bash
grep -rn --include="*.tsx" \
  -E "value=\{\{|onClick=\{\(\)" src/
For each instance found, apply the fix directly:
Inline object/array creation in JSX → wrap with
useMemo
:
tsx
// 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
useCallback
:
tsx
// 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
React.memo
to pure presentational components that receive stable props. Do NOT wrap every component — only those confirmed to re-render unnecessarily via the Profiler.

遍历组件树(从
src/App.tsx
开始),搜索以下模式:
bash
grep -rn --include="*.tsx" \
  -E "value=\{\{|onClick=\{\(\)" src/
针对每个找到的实例,直接应用修复
JSX中的内联对象/数组创建 → 用
useMemo
包裹:
tsx
// 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} />
每次渲染都会重新创建的事件处理函数 → 用
useCallback
包裹:
tsx
// 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}>
React.memo
应用于接收稳定props的纯展示组件。切勿包裹所有组件——仅包裹那些经Profiler确认存在不必要重渲染的组件。

Step 3 — Find and fix DMS query patterns

步骤3 — 查找并修复DMS查询模式

For read-heavy workloads, prefer APIs that hit the search/Elasticsearch path (
query
or
search
on instances) rather than
list
paths that stress Postgres.
bash
undefined
对于读密集型工作负载,优先选择调用搜索/Elasticsearch路径的API(实例上的
query
search
),而非会给Postgres带来压力的
list
路径。
bash
undefined

Find 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 usedWhen it's correctWhen to rewrite
instances.query
Read with filters that map to Elasticsearch (text, equals, range)
instances.search
Full-text or fuzzy search
instances.list
Writing, syncing, or need for semantics not available on query/searchRewrite to
instances.query
if used for read-heavy UI display
instances.retrieve
Fetching by known external IDs
instances.aggregate
Counts, histograms
For deeper rationale on search vs relational paths, cardinality, and materialization tradeoffs, consult the
semantic-knowledge/
directory if available in the workspace.

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适用场景需要重写的场景
instances.query
适用于可映射到Elasticsearch的过滤条件(文本、等值、范围)
instances.search
全文搜索或模糊搜索
instances.list
写入、同步操作,或需要query/search不支持的语义时若用于读密集型UI展示,重写为
instances.query
instances.retrieve
根据已知外部ID获取数据
instances.aggregate
统计、直方图
若想深入了解搜索路径与关系路径的区别、基数以及物化权衡,可查阅工作区中若存在的
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
undefined

Find 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
IssueFix
.filter()
after SDK call on full result set
Move the filter into the API request's
filter
parameter and delete the
.filter()
No
properties
selection in DMS query
Add a
sources
or
properties
parameter to fetch only needed fields
Fetching all items then rendering a subsetAdd
limit
and
filter
to the API call to fetch only what's displayed
Client-side text search on fetched arrayReplace with the SDK's
search
endpoint
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调用后对完整结果集使用
.filter()
将过滤逻辑移入API请求的
filter
参数,并删除
.filter()
DMS查询中未选择
properties
添加
sources
properties
参数仅获取所需字段
获取所有条目后仅渲染子集在API调用中添加
limit
filter
仅获取需要展示的数据
对获取的数组进行客户端文本搜索替换为SDK的
search
端点
硬性规则:若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.
,
useQuery
,
useCogniteClient
).
bash
undefined
查看所有CDF SDK调用(搜索
sdk.
client.
useQuery
useCogniteClient
)。
bash
undefined

Find 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
undefined
bash
undefined

Find 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 });
}
IssueFix to apply
Search input fires query on every keystrokeAdd
useDebouncedValue
hook
with 300ms delay
Polling with no backoff or very short intervalSet interval to ≥30s with exponential backoff on errors
Re-fetching on every render (no caching)Add
staleTime: 30_000
(or appropriate) to useQuery options
refetchOnWindowFocus: true
for expensive queries
Set
refetchOnWindowFocus: false
or use a longer stale time
Duplicate parallel identical requestsLift the query to a shared hook and import from both components
Multiple components triggering the same fetchExtract to a shared hook in
hooks/
directory

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 });
}
问题修复方案
搜索输入每次按键都触发查询添加
useDebouncedValue
hook
,设置300ms延迟
无退避策略或间隔极短的轮询将间隔设置为≥30s,并在出错时使用指数退避
每次渲染都重新获取(无缓存)添加
staleTime: 30_000
(或合适的值)到useQuery选项中
昂贵查询设置
refetchOnWindowFocus: true
**设置
refetchOnWindowFocus: false
**或使用更长的stale时间
重复的并行相同请求将查询提升至共享hook,并在两个组件中导入
多个组件触发相同的获取操作提取至
hooks/
目录下的共享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
.map()
render with a virtualized list
. Install
@tanstack/react-virtual
if not present:
bash
pnpm add @tanstack/react-virtual
Apply 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-virtual
则先安装:
bash
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
React.lazy()
and
Suspense
:
tsx
// 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()
Suspense
的懒导入:
tsx
// 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
undefined
bash
undefined

Install 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
pnpm run build
and inspect the treemap. For any chunk > 100 KB (gzipped) that is not a necessary initial dependency, apply the fix:
IssueFix to apply
lodash
(full bundle)
Replace with
lodash-es
individual imports or native equivalents (e.g.,
Array.prototype.map
,
Object.entries
,
structuredClone
)
moment
Replace with
date-fns
or native
Intl.DateTimeFormat
Chart libraries not tree-shakenSwitch to named imports (e.g.,
import { LineChart } from "echarts/charts"
)
Large library used in one placeDynamically import it with
React.lazy
or inline
import()
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
vite.config.ts
and uninstall it:
bash
pnpm remove rollup-plugin-visualizer

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 }),
  ],
});
运行
pnpm run build
并查看树形图。对于任何大于100 KB(gzip压缩后)且非必要初始依赖的chunk,应用修复
问题修复方案
lodash
(完整包)
**替换为
lodash-es
**的单独导入或原生等效方法(例如
Array.prototype.map
Object.entries
structuredClone
moment
**替换为
date-fns
**或原生
Intl.DateTimeFormat
未进行树摇的图表库切换为命名导入(例如
import { LineChart } from "echarts/charts"
仅在一处使用的大型库
React.lazy
或内联
import()
动态导入
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");
分析完成后,从
vite.config.ts
中移除visualizer插件并卸载:
bash
pnpm remove rollup-plugin-visualizer

Step 10 — Find and fix memory leaks

步骤10 — 查找并修复内存泄漏

Search for
useEffect
hooks that set up subscriptions, timers, or event listeners without cleanup:
bash
grep -rn --include="*.tsx" --include="*.ts" -A 10 "useEffect" src/
For every
useEffect
that calls
addEventListener
,
setInterval
,
setTimeout
,
subscribe
, or sets up a CDF streaming connection, add the missing cleanup function:
Fetch 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);
}, []);

搜索未清理订阅、定时器或事件监听器的
useEffect
hook:
bash
grep -rn --include="*.tsx" --include="*.ts" -A 10 "useEffect" src/
对于每个调用
addEventListener
setInterval
setTimeout
subscribe
或建立CDF流连接的
useEffect
添加缺失的清理函数
未终止的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:
MetricBeforeAfterChange
Lighthouse Performance7291+19
Largest Contentful Paint3.2 s1.8 s−1.4 s
Total Blocking Time420 ms80 ms−340 ms
Bundle size (gzipped)410 KB290 KB−120 KB
AssetTable
render count (on filter change)
82−6
If a step produced no improvement, state that explicitly. Do not fabricate numbers.

重新运行步骤1中的Lighthouse审核和React Profiler会话。报告差值并列出所有修改的文件:
指标优化前优化后变化
Lighthouse性能分数7291+19
最大内容绘制(LCP)3.2 s1.8 s−1.4 s
总阻塞时间(TBT)420 ms80 ms−340 ms
包体积(gzip压缩后)410 KB290 KB−120 KB
AssetTable
渲染次数(过滤时)
82−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配置),需单独注明为超出范围的建议。