如何对 React 函数式组件进行优化?

时间: 2019-10-19阅读: 379标签: 优化

目的

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


面向读者

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


react 性能优化思路

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

  1. 减少重新 render 的次数。因为在 react 里最重(花时间最长)的一块就是 reconction(简单的可以理解为 diff),如果不 render,就不会 reconction。
  2. 减少计算的量。主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执行函数调用。

在使用类组件的时候,使用的 react 优化 API 主要是: shouldComponentUpdate 和 PureComponent ,这两个 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>
}

export default Child

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


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

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


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>
}

export default 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
  */
}
export default React.memo(MyComponent, areEqual);

此部分来自于 React 官网 。

如果你有在类组件里面使用过 shouldComponentUpdate() 这个方法,你会对 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);

首次渲染的效果


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

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


找原因

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

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

  1. 要么是组件自己的状态改变
  2. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件的 props 没有改版
  3. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件传递的 props 改变

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

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

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

那么就是第三种情况了,当父组件重新渲染的时候,传递给子组件的 props 发生了改变,再看传递给 Child 组件的就两个属性,一个是 name ,一个是 onClick , name 是传递的常量,不会变,变的就是 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.memo 和 useCallback 都是为了减少重新 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>
  );
}

首次渲染的效果如下:


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


可能产生性能问题

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

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


useMemo 做计算结果缓存

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

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

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 的次数;减少重复计算。
  2. 如何去找到 React 中导致性能问题的方法,见 useCallback 部分。
  3. 合理的拆分组件其实也是可以做性能优化的,你这么想,如果你整个页面只有一个大的组件,那么当 props 或者 state 变更之后,需要 reconction 的是整个组件,其实你只是变了一个文字,如果你进行了合理的组件拆分,你就可以控制更小粒度的更新。

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

原文 http://taoweng.site/index.php/archives/280/

站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/6502

关闭

工程架构的优化

client端和server端的配置有重复的地方,优化的手段就是将两个文件中重复的配置项提取出来,然后利用webpack-merge包去合并配置。提取公共的配置到webpack.config.base.js文件

web系统整体优化提速总结

随着公司业务的拓展,随之而来就是各种系统横向和纵向的增加,PV、UV也都随之增加,原有的系统架构和模式慢慢遇上了瓶颈,需要逐步的对系统从整体上进行改造升级,通过一段时间的整理思路

前端性能优化总结

原则:多使用内存,缓存或者其他方法;减少CPU计算,减少网络请求;减少IO操作(硬盘读写).加载资源优化:静态资源的合并和压缩。静态资源缓存(浏览器缓存策略)。使用CDN让静态资源加载更快。

网站性能优化实战

读取缓存→DNS查询→TCP链接→发起请求→接收响应→处理HTML元素→加载完成。CSS属性读写分离:浏览器每次对元素样式进行读操作时,都必须进行一次重新渲染(重排 + 重绘)

网站优化重点注意事项

可能很多的小白还不清楚,网站优化的重点在哪里,到底是站内优化重要还是站外优化重要,要知道的是,百度除了相关说明的,外链对网站的帮助已经越来越小,但是并不是代表就可以不去做

Node学习笔记:优化crud增删改查

在安装过程中需要特别注意,需要将 Install MongoDB Compass 前默认的勾选取消,可以使用 mongod --version 命令查看 MongoDB 版本,若版本号输出正常则安装完毕

关于Js防抖与节流的思考

防抖:就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间,防抖注重结果;节流::是让一个函数无法在很短的时间间隔内连续调用

Javascript代码优化的8个知识点

本篇文章给大家分享了关Javascript代码优化的8点总结,希望我整理的内容能够帮助到大家。写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。

react性能优化之bind(this)

bind在react组件中使用不当也会影响性能,bind在render里面直接onClick = this.onClick.bind(this),这样写的话,render每次都会执行这段

网站搜索引擎优化,值得关注的4个策略有哪些?

在做网站搜索引擎优化的过程中,对于企业站而言,由于SEO人员都是处于执行层面,甚至即使你有权制定SEO优化方案,偶尔也是草草就上手操作。当你运营到一定阶段的时候

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!

文章投稿关于web前端网站点搜索站长推荐网站地图站长QQ:522607023

小程序专栏: 土味情话心理测试脑筋急转弯幽默笑话段子句子语录成语大全运营推广