Skip to content
刘恩义的技术博客
返回

React 使用技巧:把性能与可维护性做“默认正确”

Edit page

很多 React 项目“写得很顺”,但过了 3 个月会慢慢变成两类问题:一类是性能(渲染太多、交互卡顿、列表滚动掉帧),另一类是可维护性(状态散、组件耦合、边界不清)。下面这 12 条技巧偏实战,目标是把“默认正确”内置进你的写法里。

1. 先做状态建模,再写组件

写 UI 之前先回答三个问题:

一个很好用的经验:把“派生状态”从 useState 里移除,改用计算(并用 useMemo 作为性能优化而非正确性依赖)。

function ProductList({ products }: { products: { price: number }[] }) {
  const [minPrice, setMinPrice] = useState(0);

  const filtered = useMemo(() => {
    return products.filter(p => p.price >= minPrice);
  }, [products, minPrice]);

  return (
    <>
      <input
        value={minPrice}
        onChange={e => setMinPrice(Number(e.target.value))}
        type="number"
      />
      <ul>{filtered.map((p, i) => <li key={i}>{p.price}</li>)}</ul>
    </>
  );
}

这里 filtered 就是典型的派生状态:不需要额外同步、不会出现“两个状态相互打架”。

2. 别把对象/数组字面量直接塞进依赖数组

依赖数组的核心不是“我写了啥”,而是“引用是否稳定”。如果你在 render 里创建了新对象,useEffect/useMemo/useCallback 的依赖就会每次变化,等于失效。

function Search() {
  const [q, setQ] = useState("");

  const params = useMemo(() => ({ q, limit: 20 }), [q]);

  useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/search?q=${encodeURIComponent(params.q)}&limit=${params.limit}`, {
      signal: controller.signal,
    });
    return () => controller.abort();
  }, [params]);

  return <input value={q} onChange={e => setQ(e.target.value)} />;
}

更进一步的做法是把依赖拆到基础类型上([q])来避免引用问题,但上面的模式可读性更强。

3. 用 useCallback 之前先问:我真的需要稳定引用吗?

useCallback 不是“性能银弹”,它是“缓存函数引用”的工具,适用场景通常只有两个:

否则,滥用 useCallback 往往只是在增加复杂度与内存占用,还可能造成“依赖漏写”的 bug。

4. 把 “渲染频率” 当成一等公民:从输入事件开始

输入框 onChange 里做重计算是常见卡顿来源。处理方式通常是“分离即时输入与昂贵渲染”:

React 18+ 可以用 useDeferredValuestartTransition

function SearchPage({ data }: { data: string[] }) {
  const [q, setQ] = useState("");
  const deferredQ = useDeferredValue(q);

  const result = useMemo(() => {
    const keyword = deferredQ.trim().toLowerCase();
    if (!keyword) return data;
    return data.filter(x => x.toLowerCase().includes(keyword));
  }, [data, deferredQ]);

  return (
    <>
      <input value={q} onChange={e => setQ(e.target.value)} />
      <ResultList items={result} />
    </>
  );
}

5. key 的职责是“身份”,不是“索引”

用索引做 key 会让 React 在插入/删除时错误复用 DOM 与组件实例,常见后果是输入框跳字、动画错乱、局部状态串线。

规则很简单:只要列表会重排/增删,就用稳定且业务唯一的 id。

6. 大列表:先虚拟化,再谈 memo

列表卡顿通常不是因为某个组件“没 memo”,而是因为 DOM 数量太多、布局/绘制太重。优先考虑:

memo/useMemo 是在“已经把 DOM 数量控制住”之后的第二层手段。

7. 数据获取:把“并发、取消、竞态”写进默认路径

即使你不用专门的数据层库,也应该至少把两件事做好:

前者用 AbortController,后者可以用递增 requestId 或者只在同一 controller 下处理结果。

8. 把“边界”显式化:Error Boundary + Suspense

对于稳定线上体验来说,“出错时如何退化”比“永不出错”更现实。

如果你用的是 React Router / Next.js 这类框架,通常都有更完整的边界能力,优先用框架提供的方式保持一致。

9. Context 不是状态管理库,Context 是“依赖注入”

把频繁变化的状态(例如输入、滚动位置、实时数据)塞进 Context,往往会导致一整棵子树一起 re-render。

更稳的做法:

10. 用 TypeScript 把“允许的状态”收紧到类型里

与其在代码里写一堆 if (!x) return null,不如把状态机写进联合类型,让“不可达状态”变成编译错误。

type LoadState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

写 UI 时你会天然得到穷举检查与更清晰的分支渲染。

11. 性能优化从“测量”开始:React DevTools Profiler

不要靠感觉优化。建议按这个顺序做:

如果你在“还没定位问题”时就开始把所有组件都包一层 memo,大概率只会让代码更难读。

12. 组件设计:优先“组合”,慎用“万能组件”

“万能组件”通常会带来三类长期成本:

更推荐的写法是把通用部分做小、做纯,把差异点暴露为组合(children/render props)或更细的子组件。

收尾:一个可复制的自检清单

每次提交前,快速过一遍:

把这些习惯变成默认动作,React 项目通常会在性能与维护成本上同时变好。你会更少“救火”,更多“推进”。


Edit page

Previous Post
自定义 AstroPaper 主题配色方案
Next Post
Fabric.js Canvas 操作实战