React 函数式组件性能优化指南

简介: 本文只介绍函数式组件特有的性能优化方式,类组件和函数式组件都有的不介绍,比如 key 的使用。另外本文不详细的介绍 API 的使用,后面也许会写,其实想用好 hooks 还是蛮难的。

640 (1).jpg

前言


目的


本文只介绍函数式组件特有的性能优化方式,类组件和函数式组件都有的不介绍,比如 key 的使用。另外本文不详细的介绍 API 的使用,后面也许会写,其实想用好 hooks 还是蛮难的。


面向读者


有过 React 函数式组件的实践,并且对 hooks 有过实践,对 useState、useCallback、useMemo API 至少看过文档,如果你有过对类组件的性能优化经历,那么这篇文章会让你有种熟悉的感觉。


React 性能优化思路


我觉得 React 性能优化的理念的主要方向就是这两个:


  1. 减少重新 render 的次数。因为在 React 里最重(花时间最长)的一块就是 reconction(简单的可以理解为 diff),如果不 render,就不会 reconction。


  1. 减少计算的量。主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执行函数调用。


在使用类组件的时候,使用的 React 优化 API 主要是:shouldComponentUpdatePureComponent,这两个 API 所提供的解决思路都是为了减少重新 render 的次数,主要是减少父组件更新而子组件也更新的情况,虽然也可以在 state 更新的时候阻止当前组件渲染,如果要这么做的话,证明你这个属性不适合作为 state,而应该作为静态属性或者放在 class 外面作为一个简单的变量 。


但是在函数式组件里面没有声明周期也没有类,那如何来做性能优化呢?


React.memo


首先要介绍的就是 React.memo,这个 API 可以说是对标类组件里面的

PureComponent,这是可以减少重新 render 的次数的。


可能产生性能问题的例子


举个 🌰,首先我们看两段代码:


在根目录有一个 index.js,代码如下,实现的东西大概就是:上面一个 title,中间一个 button(点击 button 修改 title),下面一个木偶组件,传递一个 name 进去。


// index.js
import React, { useState } from"react";
import ReactDOM from"react-dom";
import Child from'./child'
function App() {
  const [title, setTitle] = useState("这是一个 title")
  return (
    <div className="App">
      <h1>{ title }</h1>
      <button onClick={() => setTitle("title 已经改变")}>改名字</button>
      <Child name="桃桃"></Child>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


在同级目录有一个 child.js


// child.js
import React from"react";
function Child(props) {
  console.log(props.name)
  return<h1>{props.name}</h1>
}
exportdefault Child


当首次渲染的时候的效果如下:


640 (4).png


并且控制台会打印"桃桃”,证明 Child 组件渲染了。


接下来点击改名字这个 button,页面会变成:


640 (5).png


title 已经改变了,而且控制台也打印出"桃桃",可以看到虽然我们改的是父组件的状态,父组件重新渲染了,并且子组件也重新渲染了。你可能会想,传递给 Child 组件的 props 没有变,要是 Child 组件不重新渲染就好了,为什么会这么想呢?


我们假设 Child 组件是一个非常大的组件,渲染一次会消耗很多的性能,那么我们就应该尽量减少这个组件的渲染,否则就容易产生性能问题,所以子组件如果在 props 没有变化的情况下,就算父组件重新渲染了,子组件也不应该渲染。


那么我们怎么才能做到在 props 没有变化的时候,子组件不渲染呢?


答案就是用 React.memo 在给定相同 props 的情况下渲染相同的结果,并且通过记忆组件渲染结果的方式来提高组件的性能表现。


React.memo 的基础用法


把声明的组件通过React.memo包一层就好了,React.memo其实是一个高阶函数,传递一个组件进去,返回一个可以记忆的组件。


function Component(props) {
   /* 使用 props 渲染 */
}
const MyComponent = React.memo(Component);


那么上面例子的 Child 组件就可以改成这样:


import React from"react";
function Child(props) {
  console.log(props.name)
  return<h1>{props.name}</h1>
}
exportdefault React.memo(Child)


通过 React.memo 包裹的组件在 props 不变的情况下,这个被包裹的组件是不会重新渲染的,也就是说上面那个例子,在我点击改名字之后,仅仅是 title 会变,但是 Child 组件不会重新渲染(表现出来的效果就是 Child 里面的 log 不会在控制台打印出来),会直接复用最近一次渲染的结果。


这个效果基本跟类组件里面的 PureComponent效果极其类似,只是前者用于函数组件,后者用于类组件。


React.memo 高级用法


默认情况下其只会对 props 的复杂对象做浅层对比(浅层对比就是只会对比前后两次 props 对象引用是否相同,不会对比对象里面的内容是否相同),如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。


function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
exportdefault React.memo(MyComponent, areEqual);


此部分来自于 React 官网[1]


如果你有在类组件里面使用过 `shouldComponentUpdate()`[2] 这个方法,你会对 React.memo 的第二个参数非常的熟悉,不过值得注意的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。


useCallback


现在根据上面的例子,再改一下需求,在上面的需求上增加一个副标题,并且有一个修改副标题的 button,然后把修改标题的 button 放到 Child 组件里。


把修改标题的 button 放到 Child 组件的目的是,将修改 title 的事件通过 props 传递给 Child 组件,然后观察这个事件可能会引起性能问题。


首先看代码:


父组件 index.js


// index.js
import React, { useState } from"react";
import ReactDOM from"react-dom";
import Child from"./child";
function App() {
  const [title, setTitle] = useState("这是一个 title");
  const [subtitle, setSubtitle] = useState("我是一个副标题");
  const callback = () => {
    setTitle("标题改变了");
  };
  return (
    <div className="App">
      <h1>{title}</h1>
      <h2>{subtitle}</h2>
      <button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
      <Child onClick={callback} name="桃桃" />
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


子组件 child.js


import React from"react";
function Child(props) {
  console.log(props);
  return (
    <>
      <button onClick={props.onClick}>改标题</button>
      <h1>{props.name}</h1>
    </>
  );
}
export default React.memo(Child);


首次渲染的效果

640.png


这段代码在首次渲染的时候会显示上图的样子,并且控制台会打印出桃桃


然后当我点击改副标题这个 button 之后,副标题会变为「副标题改变了」,并且控制台会再次打印出桃桃,这就证明了子组件又重新渲染了,但是子组件没有任何变化,那么这次 Child 组件的重新渲染就是多余的,那么如何避免掉这个多余的渲染呢?


找原因


我们在解决问题的之前,首先要知道这个问题是什么原因导致的?


咱们来分析,一个组件重新重新渲染,一般三种情况:


  1. 要么是组件自己的状态改变


  1. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件的 props 没有改版


  1. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件传递的 props 改变


接下来用排除法查出是什么原因导致的:


第一种很明显就排除了,当点击改副标题 的时候并没有去改变 Child 组件的状态;


第二种情况好好想一下,是不是就是在介绍 React.memo 的时候情况,父组件重新渲染了,父组件传递给子组件的 props 没有改变,但是子组件重新渲染了,我们这个时候用 React.memo 来解决了这个问题,所以这种情况也排除。


那么就是第三种情况了,当父组件重新渲染的时候,传递给子组件的 props 发生了改变,再看传递给 Child 组件的就两个属性,一个是 name,一个是 onClickname 是传递的常量,不会变,变的就是 onClick 了,为什么传递给 onClick 的 callback 函数会发生改变呢?在文章的开头就已经说过了,在函数式组件里每次重新渲染,函数组件都会重头开始重新执行,那么这两次创建的 callback 函数肯定发生了改变,所以导致了子组件重新渲染。


如何解决


找到问题的原因了,那么解决办法就是在函数没有改变的时候,重新渲染的时候保持两个函数的引用一致,这个时候就要用到 useCallback 这个 API 了。


useCallback 使用方法


const callback = () => {
  doSomething(a, b);
}
const memoizedCallback = useCallback(callback, [a, b])


把函数以及依赖项作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新。


那么可以将 index.js 修改为这样:


// index.js
import React, { useState, useCallback } from"react";
import ReactDOM from"react-dom";
import Child from"./child";
function App() {
  const [title, setTitle] = useState("这是一个 title");
  const [subtitle, setSubtitle] = useState("我是一个副标题");
  const callback = () => {
    setTitle("标题改变了");
  };
  // 通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
  const memoizedCallback = useCallback(callback, [])
  return (
    <div className="App">
      <h1>{title}</h1>
      <h2>{subtitle}</h2>
      <button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
      <Child onClick={memoizedCallback} name="桃桃" />
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


这样我们就可以看到只会在首次渲染的时候打印出桃桃,当点击改副标题和改标题的时候是不会打印桃桃的。


如果我们的 callback 传递了参数,当参数变化的时候需要让它重新添加一个缓存,可以将参数放在 useCallback 第二个参数的数组中,作为依赖的形式,使用方式跟 useEffect 类似。


useMemo


在文章的开头就已经介绍了,React 的性能优化方向主要是两个:一个是减少重新 render 的次数(或者说减少不必要的渲染),另一个是减少计算的量。


前面介绍的 React.memouseCallback 都是为了减少重新 render 的次数。对于如何减少计算的量,就是 useMemo 来做的,接下来我们看例子。


function App() {
  const [num, setNum] = useState(0);
  // 一个非常耗时的一个计算函数
  // result 最后返回的值是 49995000
  function expensiveFn() {
    let result = 0;
    for (let i = 0; i < 10000; i++) {
      result += i;
    }
    console.log(result) // 49995000
    return result;
  }
  const base = expensiveFn();
  return (
    <div className="App">
      <h1>count:{num}</h1>
      <button onClick={() => setNum(num + base)}>+1</button>
    </div>
  );
}


首次渲染的效果如下:


640 (1).png                                                           useMemo


这个例子功能很简单,就是点击 +1 按钮,然后会将现在的值(num) 与 计算函数 (expensiveFn) 调用后的值相加,然后将和设置给 num 并显示出来,在控制台会输出 49995000


可能产生性能问题


就算是一个看起来很简单的组件,也有可能产生性能问题,通过这个最简单的例子来看看还有什么值得优化的地方。


首先我们把 expensiveFn 函数当做一个计算量很大的函数(比如你可以把 i 换成 10000000),然后当我们每次点击 +1 按钮的时候,都会重新渲染组件,而且都会调用 expensiveFn 函数并输出 49995000。由于每次调用 expensiveFn 所返回的值都一样,所以我们可以想办法将计算出来的值缓存起来,每次调用函数直接返回缓存的值,这样就可以做一些性能优化。


useMemo 做计算结果缓存


针对上面产生的问题,就可以用 useMemo 来缓存 expensiveFn 函数执行后的值。


首先介绍一下 useMemo 的基本的使用方法,详细的使用方法可见官网[3]


function computeExpensiveValue() {
  // 计算量很大的代码
  return xxx
}
const memoizedValue = useMemo(computeExpensiveValue, [a, b]);


useMemo 的第一个参数就是一个函数,这个函数返回的值会被缓存起来,同时这个值会作为 useMemo 的返回值,第二个参数是一个数组依赖,如果数组里面的值有变化,那么就会重新去执行第一个参数里面的函数,并将函数返回的值缓存起来并作为 useMemo 的返回值 。


了解了 useMemo 的使用方法,然后就可以对上面的例子进行优化,优化代码如下:


function App() {
  const [num, setNum] = useState(0);
  function expensiveFn() {
    let result = 0;
    for (let i = 0; i < 10000; i++) {
      result += i;
    }
    console.log(result)
    return result;
  }
  const base = useMemo(expensiveFn, []);
  return (
    <div className="App">
      <h1>count:{num}</h1>
      <button onClick={() => setNum(num + base)}>+1</button>
    </div>
  );
}


执行上面的代码,然后现在可以观察无论我们点击 +1多少次,只会输出一次 49995000,这就代表 expensiveFn 只执行了一次,达到了我们想要的效果。


小结


useMemo 的使用场景主要是用来缓存计算量比较大的函数结果,可以避免不必要的重复计算,有过 vue 的使用经历同学可能会觉得跟 Vue 里面的计算属性有异曲同工的作用。


不过另外提醒两点


一、如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值;


二、计算量如果很小的计算函数,也可以选择不使用 useMemo,因为这点优化并不会作为性能瓶颈的要点,反而可能使用错误还会引起一些性能问题。


总结


对于性能瓶颈可能对于小项目遇到的比较少,毕竟计算量小、业务逻辑也不复杂,但是对于大项目,很可能是会遇到性能瓶颈的,但是对于性能优化有很多方面:网络、关键路径渲染、打包、图片、缓存等等方面,具体应该去优化哪方面还得自己去排查,本文只介绍了性能优化中的冰山一角:运行过程中 React 的优化。


  1. React 的优化方向:减少 render 的次数;减少重复计算。


  1. 如何去找到 React 中导致性能问题的方法,见 useCallback 部分。


  1. 合理的拆分组件其实也是可以做性能优化的,你这么想,如果你整个页面只有一个大的组件,那么当 props 或者 state 变更之后,需要 reconction 的是整个组件,其实你只是变了一个文字,如果你进行了合理的组件拆分,你就可以控制更小粒度的更新。


合理拆分组件还有很多其他好处,比如好维护,而且这是学习组件化思想的第一步,合理的拆分组件又是一门艺术了,如果拆分得不合理,就有可能导致状态混乱,多敲代码多思考。


推荐文章


我这里只介绍了函数式组件的优化方式,更多的 React 优化技巧可以阅读下面的文章:


  • 21 个 React 性能优化技巧[4]


  • 浅谈 React 性能优化的方向[5]


参考资料


[1]

React 官网: https://zh-hans.reactjs.org/docs/react-api.html#reactmemo


[2]

shouldComponentUpdate(): https://zh-hans.reactjs.org/docs/react-component.html#shouldcomponentupdate


[3]

官网: https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo


[4]

21 个 React 性能优化技巧: https://www.infoq.cn/article/KVE8xtRs-uPphptq5LUz


[5]

浅谈React性能优化的方向:  https://juejin.im/post/5d045350f265da1b695d5bf2#heading-0

目录
相关文章
|
8天前
|
移动开发 前端开发 API
React 音频播放器组件 Audio Player
本文介绍如何使用React创建音频播放器组件,涵盖核心功能如播放/暂停、进度条、音量控制和时间显示。通过HTML5 `&lt;audio&gt;` 元素和React的声明式状态管理,实现交互式音频播放。常见问题包括控件不响应、进度条无法更新和音量控制失灵,并提供解决方案。此外,还讨论了浏览器兼容性、异步错误处理和性能优化等易错点及避免方法。
322 123
|
20天前
|
移动开发 前端开发 JavaScript
React 视频播放控制组件 Video Controls
本文介绍了如何使用 React 构建视频播放控制组件(Video Controls),涵盖基本概念、创建步骤和常见问题解决。首先,通过 HTML5 `&lt;video&gt;` 标签和 React 组件化思想,实现播放/暂停按钮和进度条等基础功能。接着,详细讲解了初始化项目、构建 `VideoControls` 组件及与主应用的集成方法。最后,针对视频无法播放、控制器状态不同步、进度条卡顿和音量控制失效等问题提供了具体解决方案,并介绍了全屏播放和自定义样式等进阶功能。希望这些内容能帮助你在实际项目中更好地实现和优化视频播放功能。
97 40
|
6天前
|
移动开发 前端开发 UED
React 音频预览组件 Audio Preview
在现代Web开发中,React框架下的音频播放功能日益重要。本文介绍如何使用React创建交互式音频预览组件,涵盖基础构建、常见问题及解决方案。通过HTML5音频标签实现基本播放控制,使用状态管理增强功能。解决跨域资源共享(CORS)、格式兼容性、自动播放限制等问题,并探讨性能优化、样式定制、事件处理、移动端适配、错误处理、国际化支持及组件集成等关键点,帮助开发者提升组件稳定性和用户体验。
26 10
|
1月前
|
存储 编解码 前端开发
React 视频上传组件 Video Upload
随着互联网的发展,视频内容在网站和应用中愈发重要。本文探讨如何使用React构建高效、可靠的视频上传组件,涵盖基础概念、常见问题及解决方案。通过React的虚拟DOM和组件化开发模式,实现文件选择、进度显示、格式验证等功能,并解决跨域请求、并发上传等易错点。提供完整代码案例,确保用户能顺畅上传视频。
136 92
|
1月前
|
前端开发 UED 索引
React 图片灯箱组件 Image Lightbox
图片灯箱组件是一种常见的Web交互模式,用户点击缩略图后弹出全屏窗口展示大图,并提供导航功能。本文介绍了基于React框架的图片灯箱组件开发,涵盖初始化状态管理、图片加载与预加载、键盘和鼠标事件处理等常见问题及解决方案。通过`useState`和`useEffect`钩子管理状态,使用懒加载和预加载优化性能,确保流畅的用户体验。代码案例展示了组件的基本功能实现,包括打开/关闭灯箱、切换图片及键盘操作。
132 80
|
1月前
|
移动开发 前端开发 JavaScript
React 图片裁剪组件 Image Cropper
本文介绍了在React中实现图片裁剪功能的方法,涵盖基础知识、常见问题及解决方案。首先,通过第三方库如`react-image-crop`或`cropperjs-react`可轻松实现图片裁剪。接着,针对性能和兼容性问题,提供了优化图片加载、处理裁剪区域响应慢、解决浏览器差异等方案。最后,通过代码案例详细解释了如何创建一个基本的图片裁剪组件,并提出了优化建议,如使用`React.memo`、添加样式支持及处理大图片预览,帮助开发者避免常见错误并提升用户体验。
134 67
|
8天前
|
缓存 前端开发 JavaScript
React 视频弹幕组件 Video Danmaku
视频弹幕(Danmaku)是在线视频平台中实时显示用户评论的方式,增强互动体验。本文介绍如何在React中实现视频弹幕组件,涵盖基本结构、常见问题及解决方案,如避免弹幕重叠、优化性能、确保同步等,并通过代码示例详细解释。帮助开发者解决样式不一致、输入验证不足和加载延迟等问题,提供实用参考。
51 20
|
15天前
|
移动开发 前端开发 UED
React 音频预览组件:Audio Preview
本文介绍如何使用 React 构建音频预览组件,涵盖基础实现、常见问题及解决方案。通过 HTML5 `&lt;audio&gt;` 标签和 React 状态管理,实现播放控制。解决文件路径、浏览器兼容性等问题,并优化性能,避免状态不同步和内存泄漏,提升用户体验。
59 22
|
2月前
|
前端开发 UED 开发者
React 悬浮按钮组件 FloatingActionButton
悬浮按钮(FAB)是常见的UI元素,用于提供突出的操作。本文介绍如何在React中使用Material-UI创建美观的FAB组件,涵盖基本概念、实现方法及常见问题解决。通过代码示例和优化技巧,帮助开发者提升用户体验,确保按钮位置、颜色、交互反馈等方面的表现,同时避免无障碍性和性能问题。
142 80
|
10天前
|
移动开发 前端开发 JavaScript
React 视频播放控制组件 Video Controls
本文深入探讨了如何使用React创建功能丰富的视频播放控制组件(Video Controls)。首先介绍了React与HTML5 `&lt;video&gt;` 标签的基础知识,展示了如何通过状态管理和事件处理实现自定义控件。接着分析了常见问题如视频加载失败、控件样式不一致、状态管理混乱和性能问题,并提供了相应的解决方案。最后通过完整代码案例详细解释了播放、暂停、进度条和音量控制的实现方法,帮助开发者在React中构建高质量的视频播放组件。
48 17