React 性能优化技巧

2025-05-27

React 性能优化技巧

在这篇文章中,我们将研究如何提高需要在屏幕上渲染大量组件的 React 应用程序的性能。

我们通常考虑在大多数应用程序中使用paginationvirtualization来提供更好的用户体验,并且这适用于大多数用例,但如果我们有一个用例需要在屏幕上呈现大量组件,同时又不放弃用户体验和性能,该怎么办?

为了演示,我设计了一个简单的应用,在屏幕上渲染 3 万个方块,并在用户点击这些方块时更新计数。我使用了react 17.0.0带有钩子的函数式组件。

这是应用的预览图。它包含一个App组件和一个Square组件。点击方块时有明显的延迟。

Stackblitz 预览
Stackblitz 代码

// App.jsx
import React, { useState } from "react";

import Square from "./components/square/square";

const data = Array(30000)
  .fill()
  .map((val, index) => {
    return { id: index, key: `square-${index}` };
  });

const App = () => {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState(data);

  return (
    <div>
      <p>Count: {count}</p>
      {items.map(({ key, id, clicked }) => (
        <Square
          key={key}
          id={id}
          clicked={clicked}
          onClick={id => {
            const newItems = [...items];

            newItems[id].clicked = true;

            setCount(val => val + 1);
            setItems(newItems);
          }}
        />
      ))}
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode
// Square.jsx
import React from "react";

import "./square.css";

const Square = ({ onClick, id, clicked }) => {
  return (
    <div
      className={`square ${clicked && "clicked"}`}
      onClick={() => onClick(id)}
    />
  );
};

export default Square;
Enter fullscreen mode Exit fullscreen mode

让我们在两个组件中添加控制台语句,检查它们是否出现了不必要的渲染,然后点击其中一个方块。我们发现Square组件函数被调用了 3 万次。

另外,我们可以看到600ms在 React Dev tools Profiler Tab 上重新渲染 UI 所花费的时间。在页面加载时开始分析 -> 单击任意方块 -> 停止分析。

配置文件屏幕截图显示重新渲染耗时 600 毫秒

我们需要避免重新渲染Square组件,因为它的 for 属性没有任何props变化。我们将使用React.memo它。

什么是React.memo

React.memo是一个高阶组件,它通过记住初始渲染的结果来帮助跳过重新渲染。React.memo仅当发生变化时才重新渲染组件prop

注意:React.memo进行的是浅比较。为了更好地控制,我们可以传递如下所示的比较函数。

  React.memo(Component, (prevProps, nextProps) => {
    // return true if the props are same, this will skip re-render
    // return false if the props have changed, will re-render
  });

这是Square带有React.memo

// Square component with React.memo
import React from "react";

import "./square.css";

const Square = ({ onClick, id, clicked }) => {
  return (
    <div
      className={`square ${clicked && "clicked"}`}
      onClick={() => onClick(id)}
    />
  );
};

export default React.memo(Square);
Enter fullscreen mode Exit fullscreen mode

现在让我们尝试使用如下所示的附加设置再次进行分析。

个人资料截图

个人资料截图

我们目前还没有看到任何变化。但是当我们将鼠标悬停在Square组件上时,它会显示onClickprop 已更改,从而触发了这次重新渲染。这是因为我们在每次渲染 prop 时都会传递一个新函数onClick。为了避免这种情况,我们使用useCallback

什么是useCallback

useCallback是一个返回记忆回调的钩子。

// App component with useCallback
import React, { useState, useCallback } from "react";

import Square from "./components/square/square";

const data = Array(30000)
  .fill()
  .map((val, index) => {
    return { id: index, key: `square-${index}` };
  });

const App = () => {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState(data);

  const onClick = useCallback(
    id => {
      const newItems = [...items];
      newItems[id].clicked = true;
      setCount(val => val + 1);
      setItems(newItems);
    },
    [items]
  );

  return (
    <div>
      <p>Count: {count}</p>
      {items.map(({ key, id, clicked }) => (
        <Square key={key} id={id} clicked={clicked} onClick={onClick} />
      ))}
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

让我们再次进行分析。我们现在避免了重新渲染Squares,这减少了 的时间118ms

个人资料截图

Square现在我们看到了更好的性能。我们使用 memoization避免了组件的重新渲染,但React仍然需要比较所有 3 万个元素的 props。这是我们应用的组件树。

替代文本

如果您仍然发现性能问题,我们可以更进一步。组件Square下有 3 万个元素App。为了减少 React 比较 props 的时间,我们需要减少这一层的组件数量。这该怎么办?我们可以引入另一层组件吗?是的,我们将把 3 万个元素的列表拆分成更小的块,并使用中间组件进行渲染。

替代文本

在实际应用中,我们可以找到一个合理的位置将列表拆分成更小的块。但在这里,我们先将它们拆分成每个块包含 500 个方格的块。

// App component
import React, { useState, useCallback } from "react";

import Row from "./components/row/row";

let num = 0;

const data = Array(30000)
  .fill()
  .map((val, index) => {
    if (index % 500 === 0) {
      num = 0;
    }
    return { id: num++, key: `square-${index}` };
  });

const chunkArray = (array, chunkSize) => {
  const results = [];
  let index = 1;

  while (array.length) {
    results.push({
      items: array.splice(0, chunkSize),
      key: String(index)
    });
    index++;
  }

  return results;
};

const chunks = chunkArray(data, 500);

const App = () => {
  const [count, setCount] = useState(0);
  const [allItems, setAllItems] = useState(chunks);

  const onClick = useCallback(
    (id, index) => {
      const chunk = [...allItems[index].items];
      chunk[id].clicked = true;
      setCount(val => val + 1);
      allItems[index].items = chunk;
      setAllItems(allItems);
    },
    [allItems]
  );

  return (
    <div>
      <p>Count: {count}</p>
      {allItems.map(({ items, key }, index) => (
        <Row items={items} onClick={onClick} key={key} index={index} />
      ))}
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode
// Row component
import React, { useCallback } from "react";

import Square from "../square/square";

const Row = ({ items, onClick, index }) => {
  const onItemClick = useCallback(
    id => {
      onClick(id, index);
    },
    [onClick, index]
  );

  return (
    <>
      {items.map(({ id, key, clicked }) => (
        <Square key={key} onClick={onItemClick} id={id} clicked={clicked} />
      ))}
    </>
  );
};

export default React.memo(Row);
Enter fullscreen mode Exit fullscreen mode

我们再来分析一下。现在没有发现任何延迟。我们的组件数量少了很多,Row所以 props 的比较非常快,而且如果props 没有变化, React 甚至可以跳过Squareprops 的比较。Row

个人资料截图

这是最终的应用程序
Stackblitz 预览
Stackblitz 代码

React.memo可以useCallback用来提升性能。这是否意味着我们应该用 包裹所有组件,用React.memo包裹所有函数useCallbackReact.memo而是useCallback使用 memoization,这会增加内存占用,而且函数本身也需要运行时间,并且会产生诸如 prop 比较之类的开销。我们所做的拆分也会增加内存占用。

何时使用React.memouseCallback

除非您发现特定组件或整个应用存在延迟,否则无需使用它们。如果存在延迟,请尝试分析该屏幕上的操作,并检查是否有可以避免的组件重新渲染。useCallback当我们将函数用作钩子的依赖项以避免运行不必要的代码块时,它也很有用。

结论

虽然React.memouseCallbackuseMemo可以用来优化 React 应用程序的性能,但大多数情况下它们并不是必需的。请谨慎使用它们。

文章来源:https://dev.to/harshdand/react-performance-optimization-tips-4238
PREV
我希望在开始职业生涯时就知道的资源
NEXT
使用 Three.JS 制作音乐可视化工具