译:101个React技巧#6React代码优化🚀

275 阅读6分钟

40. 使用 memo 防止不必要的重新渲染

当处理渲染成本高的组件且其父组件频繁更新时,使用 memo 可以带来显著改变。

memo 确保组件仅在 props 发生变化时重新渲染,而不是仅仅因为父组件重新渲染。

在下面的示例中,我通过 useGetDashboardData 从服务器获取数据。如果 posts 没有变化,用 memo 包裹 ExpensiveList 可以防止其他数据更新时重新渲染。

export function App() {
  const { profileInfo, posts } = useGetDashboardData();
  return (
    <div className="App">
      <h1>Dashboard</h1>
      <Profile data={profileInfo} />
      <ExpensiveList posts={posts} />
    </div>
  );
}

const ExpensiveList = memo(({ posts }) => {
  /// 其余实现
});

💡: 一旦 React 编译器 稳定后,这个技巧可能就无关紧要了 😅。

41. 使用 memo 指定相等性函数来指导 React 如何比较 props

默认情况下,memo 使用 Object.is 来比较每个 prop 与其之前的值。

然而,对于更复杂或特定的场景,指定自定义相等性函数可能比默认比较或重新渲染更高效。

示例 👇

const ExpensiveList = memo(
  ({ posts }) => {
    return <div>{JSON.stringify(posts)}</div>;
  },
  (prevProps, nextProps) => {
    // 仅当最后一篇文章或列表大小变化时重新渲染
    const prevLastPost = prevProps.posts[prevProps.posts.length - 1];
    const nextLastPost = nextProps.posts[nextProps.posts.length - 1];
    return (
      prevLastPost.id === nextLastPost.id &&
      prevProps.posts.length === nextProps.posts.length
    );
  }
);

42. 声明 memoized 组件时优先使用命名函数而非箭头函数

定义 memoized 组件时,使用命名函数而非箭头函数可以提高 React DevTools 中的清晰度。

箭头函数通常会导致通用名称如 _c2,使调试和分析更加困难。

❌ 不好: 对 memoized 组件使用箭头函数会导致 React DevTools 中显示的信息较少。

const ExpensiveList = memo(({ posts }) => {
  /// 其余实现
});

ExpensiveList 名称不可见

✅ 好: 组件名称将在 DevTools 中可见。

const ExpensiveList = memo(function ExpensiveListFn({ posts }) {
  /// 其余实现
});

你可以看到 DevTools 中的 ExpensiveListFn

43. 使用 useMemo 缓存昂贵计算或保留引用

我通常会在以下情况使用 useMemo

  • 当我有不应该在每次渲染时重复执行的昂贵计算时
  • 如果计算值是一个 非原始值,并且被用作 useEffect 等钩子的依赖项
  • 计算出的 非原始 值将作为 prop 传递给用 memo 包裹的组件;否则,这会破坏记忆化,因为 React 使用 Object.is 来检测 props 是否变化

❌ 不好: ExpensiveListmemo 不能防止重新渲染,因为样式在每次渲染时都被重新创建。

export function App() {
  const { profileInfo, posts, baseStyles } = useGetDashboardData();
  // 每次渲染都会得到一个新的 `styles` 对象
  const styles = { ...baseStyles, padding: "10px" };
  return (
    <div className="App">
      <h1>Dashboard</h1>
      <Profile data={profileInfo} />
      <ExpensiveList posts={posts} styles={styles} />
    </div>
  );
}

const ExpensiveList = memo(function ExpensiveListFn({ posts, styles }) {
  /// 其余实现
});

✅ 好: 使用 useMemo 确保 styles 仅在 baseStyles 变化时变化,让 memo 有效防止不必要的重新渲染。

export function App() {
  const { profileInfo, posts, baseStyles } = useGetDashboardData();
  // 仅在 `baseStyles` 变化时得到新的 `styles` 对象
  const styles = useMemo(
    () => ({ ...baseStyles, padding: "10px" }),
    [baseStyles]
  );
  return (
    <div className="App">
      <h1>Dashboard</h1>
      <Profile data={profileInfo} />
      <ExpensiveList posts={posts} styles={styles} />
    </div>
  );
}

44. 使用 useCallback 记忆化函数

useCallbackuseMemo 类似,但专门用于记忆化函数。

❌ 不好: 每当主题变化时,handleThemeChange 会被调用两次,我们会向服务器推送两次日志。

function useTheme() {
  const [theme, setTheme] = useState("light");

  // `handleThemeChange` 在每次渲染时都会变化
  // 因此,每次渲染后都会触发 effect
  const handleThemeChange = (newTheme) => {
    pushLog(["Theme changed"], {
      context: {
        theme: newTheme,
      },
    });
    setTheme(newTheme);
  };

  useEffect(() => {
    const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    handleThemeChange(dqMediaQuery.matches ? "dark" : "light");
    const listener = (event) => {
      handleThemeChange(event.matches ? "dark" : "light");
    };
    dqMediaQuery.addEventListener("change", listener);
    return () => {
      dqMediaQuery.removeEventListener("change", listener);
    };
  }, [handleThemeChange]);

  return theme;
}

✅ 好: 用 useCallback 包裹 handleThemeChange 确保它仅在必要时重新创建,减少不必要的执行。

const handleThemeChange = useCallback((newTheme) => {
  pushLog(["Theme changed"], {
    context: {
      theme: newTheme,
    },
  });
  setTheme(newTheme);
}, []);

45. 记忆化工具钩子返回的回调或值以避免性能问题

当你创建一个自定义钩子与他人共享时,记忆化返回的值和函数至关重要。

这种做法使你的钩子更高效,并防止使用它的人遇到不必要的性能问题。

❌ 不好: loadData 未被记忆化,导致性能问题。

function useLoadData(fetchData) {
  const [result, setResult] = useState({
    type: "notStarted",
  });

  async function loadData() {
    setResult({ type: "loading" });
    try {
      const data = await fetchData();
      setResult({ type: "loaded", data });
    } catch (err) {
      setResult({ type: "error", error: err });
    }
  }

  return { result, loadData };
}

✅ 好: 我们记忆化所有内容,因此没有意外的性能问题。

function useLoadData(fetchData) {
  const [result, setResult] = useState({
    type: "notStarted",
  });

  // 用 `useRef` 包裹并使用 `ref` 值,使函数永不变化
  const fetchDataRef = useRef(fetchData);
  useEffect(() => {
    fetchDataRef.current = fetchData;
  }, [fetchData]);

  // 用 `useCallback` 包裹并使用 `ref` 值,使函数永不变化
  const loadData = useCallback(async () => {
    setResult({ type: "loading" });
    try {
      const data = await fetchDataRef.current();
      setResult({ type: "loaded", data });
    } catch (err) {
      setResult({ type: "error", error: err });
    }
  }, []);

  return useMemo(() => ({ result, loadData }), [result, loadData]);
}

46. 利用懒加载和 Suspense 让你的应用加载更快

构建应用时,考虑对以下代码使用懒加载和 Suspense

  • 加载成本高的代码
  • 仅与部分用户相关的功能(如高级功能)
  • 初始用户交互不需要的代码

在下面的 沙盒 中 👇,Slider 资源(JS + CSS)仅在点击卡片后加载。

🏖 沙盒

47. 节流你的网络以模拟慢速网络

你知道可以直接在 Chrome 中模拟慢速互联网连接吗?

这在以下情况下特别有用:

  • 客户报告你无法在更快的网络上复现的慢加载时间
  • 你正在实现懒加载,并希望观察文件在较慢条件下的加载情况,以确保适当的加载状态

48. 使用 react-windowreact-virtuoso 高效渲染列表

切勿一次性渲染长列表项——如聊天消息、日志或无限列表。

这样做可能导致浏览器冻结。

相反,虚拟化列表。这意味着仅渲染用户可能看到的项目子集。

react-windowreact-virtuoso@tanstack/react-virtual 这样的库就是为此设计的。

❌ 不好: NonVirtualList 一次性渲染所有 50,000 条日志行,即使它们不可见。

function NonVirtualList({ items }) {
  return (
    <div style={{ height: "100%" }}>
      {items.map((log, index) => (
        <div
          key={log.id}
          style={{
            padding: "5px",
            borderBottom:
              index === items.length - 1 ? "none" : "1px solid #ccc",
          }}
        >
          <LogLine log={log} index={index} />
        </div>
      ))}
    </div>
  );
}

✅ 好: VirtualList 仅渲染可能可见的项目。

function VirtualList({ items }) {
  return (
    <Virtuoso
      style={{ height: "100%" }}
      data={items}
      itemContent={(index, log) => (
        <div
          key={log.id}
          style={{
            padding: "5px",
            borderBottom:
              index === items.length - 1 ? "none" : "1px solid #ccc",
          }}
        >
          <LogLine log={log} index={index} />
        </div>
      )}
    />
  );
}

你可以在下面的沙盒中切换这两个选项,并注意使用 NonVirtualList 时应用的性能有多差 👇。

🏖 沙盒