一个用于解决 React 常见问题的 Checklist

Posted YITA90

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个用于解决 React 常见问题的 Checklist相关的知识,希望对你有一定的参考价值。

原文地址:https://logrocket-blog.ghost.io/death-by-a-thousand-cuts-a-checklist-for-eliminating-common-react-performance-issues/

这是一份非常实用的,一步步解决 React 性能问题的清单。

你想不想让你的 React 应用程序运行更快?

想不想有一份清单来检查常见的 React 性能问题?

如果你的答案,都是 yes~那就抓紧时间来读一下这篇文章。这是一份非常实用的解决 React 性能问题的清单

在本文中,我将为大家介绍一个逐步消除常见性能问题的指南。

我会先描述问题,然后再给出解决方案。在这同时,你可以将同样的问题带入到你自己的项目中审查。

下面就开始动手吧~

项目

为了大家都方便学习,我会在同一个 React 应用上来介绍各种问题的情况。
项目名称为 Cardie
可以在 Github 上下载Cardie 源码,跟着我一起学习。

Cardie 是一个功能非常简单的项目。就是常见的个人简介页,用来展示一个用户的个人介绍。

页面内包含一个可点击的按钮,可以操作更改用户的职业信息。

Cardie in Action ?

点击按钮,用户的职业信息会发生改变。

看到这里,你可能会有点忍不住想笑了。这个应用相比于真实的项目也太过于简单了。这个的项目的性能问题,能和我们真实世界的应用比吗?

现在,我们继续。

1. 辨别无用的渲染

解决性能问题第一步,最好的方式就是从分辨哪些是你项目里,没有用的渲染。

检查方式多种多样,但最简单的方式,就是打开你的React devtools 面板,切换 highlight updates 这个选项。

如何在 React devtools 中开启 highlight updates

这个时候,App 上更新渲染的地方,会有一个闪烁。

在蒙层下面的组件,会被 React 重新渲染。

点击切换职业,会发现,整个父级 App 都被重新渲染了。

注意用户卡片边缘的闪烁

这就不太对了。
可以看到 App 虽然运行正常,但是更改很小的地方,不应该整个组件都重新渲染啊。

实际发生改变的是 App 内很小的一部分

理想的更新应该像下面这样:

注意更新只发生在很小的区域内

无用的重新渲染对于复杂的项目来说,更是会引发不小的性能问题。

发现问题了吗?解决了吗?

2. 将需要频繁更新的区域单独创建为组件

一旦在你的应用里,发现了无用的渲染,重新去整理你的组件树,是非常好的解决方式。

下面我们来详细说明一下。

Cardie 中,App 组件通过 react-redux 里的 connect 方法连接到 redux store。从 store 中获取属性:name, location, likesdescription

<App/> 直接操作 redux store 获取数据

在个人介绍页目前定义了 description 属性。
原因就是因为,点击按钮的时候, description 属性发生了改变。改变引发了 App 组件的重新渲染。

记不记得 React 101 里有句话,一个组件中无论是 props 还是 state 发生改变,都会触发重新渲染。

React 组件的元素渲染树。这些元素是通过 propsstate 定义的。如果 props 或者 state 发生改变,元素树会重新渲染。结果就会是一个新的树。

我们应该如何让一个特定的 React 组件元素更新,而不是 App 组件?

例如: 我们可以创建一个新组件,名字叫 Profession。渲染它自己的元素。

在这个例子里,如果将职业点击切换为“我是一个程序员”的时候, Profession 组件会被重新渲染。
在 组件内的 组件会重新渲染

新的组件树如图:

组件渲染了包括 组件的元素

也就是说之前是 组件在关心 profession 属性,现在变成了 <Profession/> 这个组件的事情。

组件 <Profession/> 会直接从 redux store 获取 profession 属性。

不管你是否使用了 redux,这里的关键点是 App 组件不再会因为 profession 属性的改变,而引发重绘。而是被组件 <Profession/> 取代了。

更改之后,我们再来看一下 update highlighted 的表现:

注意只有 <Profession /> 内部发生了更新

详细的代码更改,可以看项目的这个分支 isolated-component branch

3. 在恰当的地方使用静态组件

React 提到性能问题,不得不说的就是静态组件。那么,怎样正确使用静态组件

当然,你可以把每个组件都写成静态组件,但是你要记得,有一个方法特殊。 shouldComponentUpdate 方法。

我们假定只有 props 和之前的 propsstate 不相同的时候,会引发组件的重新渲染。

React.PureComponent 相对的就是默认的 React.Component 组件。

React.PureComponent 替代 React.Component

为了解释 Cardie 项目里,对于这个具体使用的区别,

我们将 Profession 组件插入更小的其他组件。

目前,我们的 Profession 代码是这样的:

const Description = ( description ) => 
  return (
    <p>
     <span className="faint">I am</span> a description
    </p>
  );

我们想改成这样

const Description = ( description ) => 
  return (
    <p>
      <I />
      <Am />
      <A />
      <Profession profession=description />
    </p>
  );
;

这样,Description 组件就会有 4 个子组件。

注意 Description 组件里的 profession 属性,它传递这个属性值给到 Profession 组件。从理论上来说,其他三个组件不需要关心 profession 的值。

我们假设新的子组件内容是非常简单的。<I /> 组件 就返回一个 span元素,内部包裹着一个字母 I, <span >I </span>

项目可以正常运行,结果也没有问题。

但是当你修改 description 这个属性的时候,Profession 下所有的子组件都跟着重新渲染了

一旦有新的属性值传递给 Description 组件时,其他子组件也会被重新渲染。

我在render 方法里加了日志输出,我们可以真实的在控制台看到,每个子组件都被重新渲染了。

你也可以在 react dev tools 里查看 highlighted updates

注意这几个词 I, ama. 都被重新渲染过

这是意料之中的事情。无论是 props 还是 state 发生改变,元素树都会被重排。
和重绘是一样的。

在这个例子里,我们需要提前约定好, <I/>, <Am/><A/> 三个组件是不需要重绘的。没错, props 来自父组件,<Description/> 变化了。这是不可避免的。但是假设我们的应用足够大,这个现象就会引发构成性能威胁。

假设我们使用静态组件作为子组件呢?

<I /> 组件:

import React, Component, PureComponent from "react"

//before 
class I extends Component 
  render() 
    return <span className="faint">I </span>;


//after 
class I extends PureComponent 
  render() 
    return <span className="faint">I </span>;

补充说明一下, React 通过 hood 通知到这些子组件,即使属性值有变化,他们也不需要更新渲染。

就是说无论父元素属性值如何变化,都不重新渲染!

了解原理之后,我们来看 highlighted updates, 如图所示,子组件不再重新渲染了。
prop 值变化时,只有 Profession 组件重新渲染了。

在字母 “I”, “am” and “a”. 这三个周围没有出现边框,只有包裹的容器,profession 组件闪烁。

在更大型的应用里,通过设置为静态组件,可能可以大幅度提升性能。

想查看代码改动,可以看项目分支 pure-component branch

4. 避免给 props 传新对象

再次强调一下,props 变化,会导致组件重新渲染。


不管是 props 还是 state 发生变化,VDOM 树都会重新渲染,导致生成新的 VDOM 树

万一你的组件没改变 props, 但 React 认为它改变了呢?

是的,也会重新渲染!

但是这不是很奇怪吗?

这种情况的发生是和 javascript 的运行原理, React 处理新旧值比较的实现方式是有关系的。

让我们看一个例子。

下面是 Description 组件的代码内容:

const Description = ( description ) => 
  return (
  <p>
     <I />
     <Am />
     <A />
     <Profession profession=description />
  </p>
  );
;

接下来,我们给 I 组件定义一个 i 属性值。定义为一个对象:

const i =  
  value: "i"
;

不管 value 里,具体值是什么,我们都可以直接取变量名渲染到组件 I 中。

class I extends PureComponent 
  render() 
    return <span className="faint">this.props.i.value </span>;
  

在组件 Description 中,属性 i 将会如下定义和使用:

class Description extends Component 
  render() 
  const i = 
    value: "i"
  ;
  return (
    <p>
            <I i=i />
      <Am />
      <A />
      <Profession profession=this.props.description />
    </p>
  );
  

这段代码可以正常运行,但是这里面有一个问题,不知道你有没有注意到?

尽管 I 是一个静态组件,但是现在用户的职业变更时,它也跟着重新渲染了

点击按钮,会发现 <I/><Profession/> 组件都重新渲染了。但是<I/> 组件真实的属性并没有变化啊,为什么会这样?

这是为什么呢?

Description 组件接收了一个新的属性值, render 方法被调用生成了新的 VDOM 树。

为了给 render 方法传参,定义了一个新常量 i:

const i =  
  value: "i"
;

React 执行这行代码的时候,<I i=i /> ,它将 i 当作一个不同的属性,一个新的对象进行传参的,所以重新渲染了。

如果你记得 React 101React 会对新旧 props 进行对比。

像字符串和数字这种值类型的,是针对值来进行对比。对象类型是引用对比。

尽管常量 i 每次都是渲染相同的值,但是引用地址不同,内存不记录它上次的位置。

每次渲染都会被新建对象,所以, prop每次传给 <I /> 组件的都是”新“ 的对象,所以不断的重新渲染。

在一个大型应用场景中,这会导致无用渲染,导致潜在的性能陷阱。

如何避免呢?

可以给每一个 prop 加一个事件处理。

如果你想避免这种情况的发生,你就不能这样写:

... 
render() 
  <div onClick=() => //do something here

... 

你可以新建一个方法对象,每次渲染的时候去调用方法。像这样:

...
handleClick:() =

render() 
  <div onClick=this.handleClick

...

明白了吗?

同样,我们需要把 prop 传递给 <I>

class Description extends Component 
  i = 
    value: "i"
  ;
  render() 
    return (
      <p>
       <I i=this.i />
       <Am /> 
       <A />
       <Profession profession=this.props.description />
      </p>
    );
  

这样的话,引用就是同一个了, this.i

渲染的时候就不会有新对象了。

同样,代码变更可以在项目分支new-objects branch 这里看到。

5. 使用构建工具

当你需要发生产环境的时候,要使用构建工具。使用很简单,效果很显著。

development build 是在提醒你, react 开发者工具在开发环境使用的

如果你是用 create-react-app 来构建你的应用的,可以直接执行命令 npm run build

这样会打包出适合线上环境的优化后的文件。

6. 使用代码分割

当你打包你的应用的时候,你很可能会得到一个很大的文件。

随着你应用的复杂程度的增加, bundle 也会越来越大。
一旦用户访问网页,就会接收到整个 app 的全部代码。

代码分割是指,将代码分成一部分一部分,当用户需要的时候,再去请求,而不是一下子全部返回给用户。

最常见的例子就是路由的代码分割。在这种方式下,应用会根据路由变化,返回对应的代码片段。
/home 会看的一部分代码,/about 会看到一部分。

另一个应用场景就是基于组件的代码分割。在这种情况下,给用户展示的就不是一个组件,而是延迟将组件发送给用户。

这里最重要的是理解和权衡应用功能和用户体验之间的关系,采用用哪种方式并不重要。

代码分割的方式是可以提供你的应用的性能的。

这里我就不展开来说了,如果你对代码分割感兴趣,你可以参考官方文档 React docs.。他们给出的解释更专业。

结论

现在你有了解决 react 应用的性能问题,常用的跟踪和修复问题的方法,快去试试让你的App 体验更快吧!

以上是关于一个用于解决 React 常见问题的 Checklist的主要内容,如果未能解决你的问题,请参考以下文章

React Native reanimated 不适用于插值

用于资源文件 (CSS/JS) 的 Laravel React-router 404

简单的计数器不适用于 React 上下文

未找到用于 react-native 的 Android SDK

React.js 常见问题

Tailwind CSS 不适用于 React App