面试官:React性能优化有哪些方法?

Hello,大家好,今天的分享由团队的 uncle13 老师提供。

虽然 React 本身利用 virtual DOMdiff 算法实现了高效的性能,但在实际项目中,还有不少的方法或技巧,可以进一步提升 React 项目的性能。

今天我们就将分享几种能有效提升 React 性能的方法。

PS:React 中的组件分为 Function 组件和 Class 组件,优化的手段根据其特性不同也分为两类,由于篇幅有限,而且 Class 组件已逐渐退出历史舞台,我们今天主要针对 Function 组件的优化进行介绍。

1. React.memo

大家都知道,React 在组件触发刷新的时候,会深度遍历所有子组件,查找所有更新的节点。

也就是说,如果父组件刷新,子组件必然会跟着刷新。

假如这次的刷新,和我们子组件没有关系,该怎么减少这种波及呢?

这就可以用到 React.memo 了。

React.memo 是 React 提供的一个高阶组件,用于优化组件的性能。它可以在某些情况下避免不必要的组件重新渲染,从而提高应用程序的性能。

其使用方式分为两种:

  • 基础使用:函数组件直接包裹 React.memo 默认使用浅层比较。
  • 高阶使用:如果需要更精确地控制何时重新渲染组件,可以通过传递第二个参数给 React.memo 来指定自定义的比较函数。这个比较函数接收两个参数,分别是前一次的 props 和当前的 props ,返回一个布尔值表示是否需要重新渲染组件
import React from 'react';

const areEqual = (prevProps, nextProps) => {
  // 自定义比较逻辑
  // 返回 true 表示两个 props 相等,不需要重新渲染
  // 返回 false 表示两个 props 不相等,需要重新渲染
  return prevProps.value === nextProps.value;
};

const MyComponent = React.memo((props) => {
  console.log('Rendering MyComponent');
  return <div>{props.value}</div>;
}, areEqual);

如果是在 Class 组件中,则一般使用 shouldComponentUpdatePureComponent实现类似的优化。

2. 列表项使用 key

key 属性是一个特殊的属性,它是出现不是给开发者用的,而是给react自己用的。比如你给一个子组件设置 key 之后,并不能通过子组件的 props 获取到 key 属性。

更具体地来说,组件的key属性是为了提高 diff算法在渲染列表时候的性能。有了它,react 内部就知道相比上一个渲染周期,当前的渲染周期插入,移动或者删除哪些节点。然后,我们就通过复用相应的组件实例来复用之前的 DOM 对象,减少不必要的 DOM 操作所产生的开销,从而提高界面更新的性能。

在项目开发中,key属性的使用场景最多的还是由数组动态创建的子组件的情况,需要为每个子组件添加唯一的 key 属性值。

举个简单的例子,我们可以给 Todo List 中的每个待办事项元素,提供唯一的 key。

const TodoList = ({ todos }) => {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>

  );
};

注意事项:为了正确使用key属性,确保所提供的key是稳定、唯一且可预测的。

不推荐使用索引作为key,因为索引可能会随着列表的增删而改变,导致性能问题和渲染错误。更好的做法是使用具有稳定唯一性的唯一标识符作为key,例如数据库分配的ID或其他全局唯一标识符。

3. 合理使用 useCallback 和 useMemo

在介绍本节内容前,先问大家一个问题:useCallbackuseMemo 有什么关系?

可能很多人会说,useCallback 返回一个记忆化的回调函数,这个函数只有在依赖项改变时才会发生变化。而useMemo 是用来缓存计算结果的,返回的是数值。

其实这种理解有些片面,因为 useMemo 的返回值可以是函数,我们可以把 useCallback 当成是 useMemo 缓存函数时的一种语法糖,使用 useCallback 可以减少些额外的嵌套函数。

// 在 React 内部的简化实现
function useCallback(fn, dependencies{
  return useMemo(() => fn, dependencies);
}

介绍 useMemouseCallback 的文章有很多,我们今天只做下简单的总结:

  • useMemo和useCallback只是针对重新渲染才是有帮助的,对第一次的渲染是有害的,消耗性能的。
  • 大多数情况下,单独使用useMemo和useCallback或memo是没有帮助的,需要结合父组件具体情况来看。
  • 其实大多数情况下,我们并不需要这两个hook,使用它们只会影响初始化的渲染。

比如下面的代码,我们使用 React.memo 对子组件进行缓存,同时使用 useMemouseCallback 对传递给子组件的属性值进行缓存。

const OtherCompMemo = React.memo(OtherComp)

const App = ({val}) => {
  const [count, setCount] = useState(0)
  const onClick = useCallback(() => {
    // ...
  }, [])

  const data = useMemo(() => val, [val])

  return (
    <>
      <button onClick={() => setCount(count+1)}>点我</button>
      <OtherCompMemo onClick={onClick} data={data} />
    </>

  )
}

4. 使用懒加载和代码分割

通过使用React.lazy()Suspense,在需要时才加载组件,从而减少初始加载时间并提高性能。

实际项目中的应用程序,其中包含多个页面,并且每个页面都有自己的组件。我们可以使用懒加载和代码分割来延迟加载这些页面组件,以减少初始加载时间并提高性能。

首先,我们使用 React.lazy() 函数对页面组件进行懒加载和代码分割。

例如,我们有一个名为HomePage的页面组件:

const HomePage = () => {
  // 页面内容
};

现在,我们将使用React.lazy()将其包装起来,以便在需要时进行延迟加载:

const HomePage = React.lazy(() => import('./HomePage'));

接下来,在应用程序的路由配置中使用Suspense组件,以在加载懒加载组件时显示加载指示器或其他备选内容:

import { BrowserRouter as Router, Route, Switch, Suspense } from 'react-router-dom';

const App = () => {
  return (
    <Router>
      <Switch>
        <Suspense fallback={<div>Loading...</div>}>
          <Route exact path="/" component={HomePage} />
          {/* 其他路由配置 */}
        </Suspense>
      </Switch>
    </Router>

  );
};

在上述示例中,当用户访问首页时,才会触发HomePage组件的加载。在加载期间,Suspense组件将显示"Loading..."文本作为加载指示器。一旦HomePage组件加载完成,它将被渲染到页面上。

通过使用懒加载和代码分割,我们可以将页面组件的加载延迟到实际需要时,而不是在初始加载时一次性加载所有组件。这减少了初始加载时间,并且只有当用户访问相应的页面时才会加载相关组件,提高了性能和用户体验。

不过 React.lazy()目前仅支持默认导出(default exports)。如果要懒加载具名导出(named exports),可以结合使用React.lazy()import()函数:

const HomePage = React.lazy(() => import('./HomePage').then(module => ({ defaultmodule.HomePage })));

5. 使用虚拟化

虚拟列表,是很多优化系列文章中提到的一种方式。原理其实也很简单,对长列表或大型数据集,使用虚拟化库(如react-virtualized)可以仅渲染可见部分,而不是全部内容,实现性能的提升。

比如在社交媒体应用程序中,其中有一个帖子列表页面,用户可以浏览和滚动大量的帖子。为了提高性能,并避免在渲染大量帖子时产生性能问题,我们可以使用虚拟化库(如react-virtualized)来仅渲染可见部分内容。

首先,我们需要安装并导入 react-virtualized库:

import { List, AutoSizer } from 'react-virtualized';

接下来,我们准备帖子数据,并创建一个帖子列表组件PostList,使用List组件来进行虚拟化渲染:

const PostList = ({ posts }) => {
  const rowRenderer = ({ index, key, style }) => {
    const post = posts[index];
    return (
      <div key={key} style={style}>
        <PostItem post={post} />
      </div>

    );
  };

  return (
    <AutoSizer>
      {({ height, width }) => (
        <List
          height={height}
          width={width}
          rowCount={posts.length}
          rowHeight={100}
          rowRenderer={rowRenderer}
        />

      )}
    </AutoSizer>

  );
};

通过指定height和width属性,我们告诉List组件列表的可视区域大小。rowCount属性表示帖子的总数量,而rowHeight属性表示每个帖子项的高度。

当用户滚动页面时,库会根据需要动态加载和卸载帖子项,从而提供平滑的滚动体验,并避免性能问题。

6. 使用 <Profiler> 测量性能

<Profiler> 用于编程式测量 React 树的渲染性能。

用法也很简单:

<Profiler id="App" onRender={onRender}>
  <App />
</Profiler>

当调用 onRender 回调函数时,React 会告诉你相关信息。

function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime{
  // 对渲染时间进行汇总或记录...
}

它可以帮助我们定位哪些组件渲染较慢,以及哪些组件触发了不必要的渲染。

PS:进行性能分析会增加一些额外的开销,因此在默认情况下,它在生产环境中是被禁用的。

最后

顺便也给我们的辅导服务打个广告,现在报名支持指定导师哦~