日常优化,React 避免不必要的重复渲染

简介: 在 React 中性能问题有两类,长列表和重复渲染。长列表指的是你的页面渲染了很长的列表,通常有上百、上千甚至几千行数据。长列表本身不是 React 框架特有的问题,无论是什么技术栈,都可能遇到。它的通用解决方案是采用虚拟滚动,业界做得比较好的解决方案有 react-virtualized 和 react-window,已经非常成熟了。那对于重复渲染有没有好的解决方案了,今天我们就一起来看看吧。

网络异常,图片无法展示
|


这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战


前言


大家在用 React 开发时,有没有遇到过这样的问题“我们有时候只想更新单个组件,却触发了大量无关组件的渲染”,正常情况,我们只想重新渲染有数据变化的组件,而不涉及其他无关组件。所以我们需要避免无效的重复渲染,毕竟 React 的协调成本很昂贵。


在 React 中性能问题有两类,长列表和重复渲染。长列表指的是你的页面渲染了很长的列表,通常有上百、上千甚至几千行数据。长列表本身不是 React 框架特有的问题,无论是什么技术栈,都可能遇到。它的通用解决方案是采用虚拟滚动,业界做得比较好的解决方案有 react-virtualized 和 react-window,已经非常成熟了。

那对于重复渲染有没有好的解决方案了,今天我们就一起来看看吧。


1. 定位小工具


为大家介绍一个 React 中常用的小工具 React Developer Tools。通过 React Developer Tools 中的 Profiler 分析组件渲染次数、开始时间及耗时。React Profiler 的详细使用方式建议阅读官方文档: zh-hans.reactjs.org/blog/2018/0…

如果你已经知道这个小工具 React Developer Tools 的使用,请跳过这一小节。

在工具的设置中,开启【Highlight updates when components render.】,当我们组建渲染了,会在页面中高亮显示。

网络异常,图片无法展示
|
网络异常,图片无法展示
|

或者你可以开发录制功能,在页面操作完之后,会看到相应的渲染情况,不渲染的内容,会直接标记为 Did not render。重复渲染的内容可直接查看渲染耗时等消息。

网络异常,图片无法展示
|

react-dom 16.5+ 在 DEV 模式下支持性能分析。在生产环境也可以使用 react-dom/profiling 代码包进行性能分析, 查阅 fb.me/react-profiling 了解更多如何使用这个代码包。


2. 一个测试例子


下面的代码是一个简单的示例代码(示例代码来源于网络代码),功能很简单表格的上移下移,


  • List,组件用于展示列表,执行上下移动的逻辑;
  • ListItem,也就是列表中展示的行,渲染每行的内容。

列子测试代码:codesandbox.io/s/test-rese…


import React from "react";
const initListData = [];
for (let i = 0; i < 10; i++) {
  initListData.push({ text: i, id: i });
}
const ListItem = ({ text, onMoveUp, onMoveDown, index }) => (
  <div>
    <span>{text}</span>
    <button onClick={() => onMoveUp(index)}>上移</button>
    <button onClick={() => onMoveDown(index)}>下移</button>
  </div>
);
class List extends React.Component {
  state = {
    listData: initListData
  };
  handleMoveUp = (index) => {
    const _listData = [...this.state.listData];
    // 部分实现
    if (index === 0) {
      const data = _listData.shift();
      _listData.push(data);
      this.setState({
        listData: _listData
      });
    }
  };
  handleMoveDown = (index) => {
    const _listData = [...this.state.listData];
    // 部分实现
    if (index === 0) {
      const data = _listData.pop();
      _listData.unshift(data);
      this.setState({
        listData: _listData
      });
    }
  };
  render() {
    const { listData } = this.state;
    return (
      <div>
        {listData.map(({ text, id }, index) => (
          <ListItem
            text={text}
            index={index}
            onMoveUp={this.handleMoveUp}
            onMoveDown={this.handleMoveDown}
          />
        ))}
      </div>
    );
  }
}
export default function App() {
  return (
    <div className="App">
      <List />
    </div>
  );
}


当我们执行列表行的上下移动时,发现当操作一行,整个 ListItem 的其他行也会被重新渲染。如文章开始说道,我们只想重新渲染有数据变化的组件,而不涉及其他无关组件,毕竟 React 的协调成本很昂贵。 那如何避免不必要的重复渲染了,接着往下看。


网络异常,图片无法展示
|


3. React.memo


在 React16.6 加入的一个专门用来优化 函数组件 (Functional Component)性能的方法: React.memo。React.memo() 是一个高阶函数,它与 React.PureComponent 类似,但是一个函数组件而非一个类。

为 ListItem 添加 React.memo 就可以阻止每行内容重新渲染。如下代码所示:

const ListItem =  React.memo(({ text, onMoveUp, onMoveDown, index }) => (
  <div>
    <span>{text}</span>
    <button onClick={() => onMoveUp(index)}>上移</button>
    <button onClick={() => onMoveDown(index)}>下移</button>
  </div>
));

网络异常,图片无法展示
|

发现加上 React.memo 之后,并不是每一行都会渲染。但是 React.memo 是通过浅比较的方式对比变化前后的 props 与 state,但是这种方式很容易失效,那就是使用箭头函数。箭头函数在每次调用 render 时都会动态生成一个新的函数,函数的引用变化了,这时即便使用 React.memo 也是无效的,所以大家在使用的时候要注意。作者也会经常的使用箭头函数来写,但是这其实是一种不好的习惯,比较好的书写方式是将整个函数提取为一个类属性的函数。


当然如果你使用的 React Hooks,你可以使用 useMemo,useMemo 被称为更加精细的 memo。React.memo 是对组件级别的重新渲染进行管控。React.useMemo 是对组件的某个或者几个部分的重渲染进行管控。React.memo 控制是否需要重渲染一个组件,而 useMemo 控制的则是是否需要重复执行某一段逻辑。


4. PureComponent


PureComponent 它内置了对 shouldComponentUpdate 的实现:PureComponent 将会在 shouldComponentUpdate 中对组件更新前后的 props 和 state 进行浅比较,并根据浅比较的结果,决定是否需要继续更新流程。还有 PureComponent 有和 React.memo 一样的尿性很容易失效,那就是使用箭头函数。


class ListItem extends React.PureComponent{
  render() {
    const { text, onChange, index } = this.props;
    return (
      <div>
        <input value={text} />
        <span>{text}</span>
        <button onClick={() => onChange(index)}>修改 text </button>
      </div>
    );
  }
};

网络异常,图片无法展示
|


5. ImmutableJS-不可变数据



React.memoPureComponentPureRenderMixin都是用过浅比较的方式来对比变化前后的 props 与 state。它们只能针对值类型数据对比其值是否相等,而针对数组、对象等引用类型的数据对比时只会对比它的引用。这样的话比较的时候就不会太准确。


  • 如果数据内容没有变,但是数据的引用改变了,浅比较仍然会认为“数据发生了变化”,进而触发一次不必要的更新,导致过度渲染;
  • 如果深层嵌套,数据内容改变,引用没变,浅比较则会认为“数据没有发生变化”,进而阻断一次更新,导致不渲染。 而 ImmutableJS 可以保证修改操作返回一个新引用,并且只修改需要修改的节点。Immutable 的结构不可变性&&结构共享性,能够快速进行数据的比较:


function deepCompare(instance, nextProps, nextState) {
  return !Immutable.is(instance.props, nextProps) ||
    !Immutable.is(instance.state, nextState);
}
class ListItem extends React.Component{
  shouldComponentUpdate(nextProps, nextState) {
    return deepCompare(this, nextProps, nextState);
  };
  render() {
    const { text, onChange, index } = this.props;
    return (
      <div>
        <input value={text} />
        <span>{text}</span>
        <button onClick={() => onChange(index)}>修改 text </button>
      </div>
    );
  }
};


虽然 ImmutableJS 可以在某些情况解决重复渲染,但是如果需要频繁地与服务器交互,那么 Immutable 对象就需要不断地与原生 js 进行转换,操作起来显得很繁琐,并且这种方案某种层面上来说有一定心智成本。所以如今 immerjs 更为流行。


6. reselect-缓存


reselect 会将输入与输出建立映射,缓存函数产出结果。只要输入一致,那么会直接吐出对应的输出结果,从而保证计算结果不变,以此来保证不会被破防。这种方式是通过缓存,使用 reselect 缓存函数执行结果,来避免产生新的对象。小编本人多这种方案了解不深,也没有使用过,但是如果你有兴趣可以自己去尝试一下。

github: github.com/garylesueur…


7. 手动控制


自己手动控制,通过使用 shouldComponentUpdate API 来处理,但是 shouldComponentUpdate 可能会带来意想不到的 Bug,所以这个方案应该放到最后考虑。而且这种方法仅限于类组件。


class ListItem extends React.Component{
  shouldComponentUpdate(nextProps, nextState) {
    if(nextProps.text === this.props.text) {
      return false
    }
    return true
  }
  render() {
    const { text, onChange, index } = this.props;
    return (
      <div>
        <input value={text} />
        <span>{text}</span>
        <button onClick={() => onChange(index)}>修改 text </button>
      </div>
    );
  }
};


使用前


网络异常,图片无法展示
|


使用后


网络异常,图片无法展示
|


总结


避免重复渲染常用的解决方案是使用 PureComponent 或者使用 React.memo(useMemo) 等组件缓存 API,减少重新渲染。但错误的使用方式会使其完全无效,比如使用箭头函数或者每次都生成新的对象,那基本就是做了无用功。针对这些问题,使用 ImmutableJS、immerjs 转换数据结构或者 reselect 缓存函数执行结果 都可以解决。亦或者自己手动控制,自己实现 shouldComponentUpdate 函数,但这类方案一般不推荐,因为容易带来意想不到的 Bug,可以作为保底手段使用。

码字不易,如果对你有帮助,帮忙点个赞吧。


参考



目录
相关文章
|
2月前
|
前端开发 JavaScript
React学习之——条件渲染
【10月更文挑战第16天】React 中没有像Vue中v-if这种指令。React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 if 或者条件运算符去创建元素来表现当前的状态,然后让 React 根据它们来更新 UI。
|
2月前
|
前端开发 JavaScript 容器
React 元素渲染
10月更文挑战第7天
34 1
|
1月前
|
存储 缓存 JavaScript
如何优化React或Vue应用的性能
需要注意的是,性能优化是一个持续的过程,需要根据具体的应用场景和性能问题进行针对性的优化。同时,不同的项目和团队可能有不同的优化重点和方法,要结合实际情况灵活运用这些优化策略,以达到最佳的性能效果。
111 51
|
17天前
|
前端开发 UED 开发者
React 选项卡组件 Tabs:从基础到优化
本文详细介绍了如何在React中构建一个功能丰富的选项卡组件,包括基础实现、样式美化、常见问题及解决方法。通过逐步讲解,从简单的选项卡组件结构开始,逐步引入样式、性能优化、动态内容加载、键盘导航支持和动画效果,最后讨论了自定义样式的实现。旨在帮助开发者在React项目中高效构建高质量的选项卡组件。
57 18
|
22天前
|
前端开发 UED
React 文本区域组件 Textarea:深入解析与优化
本文介绍了 React 中 Textarea 组件的基础用法、常见问题及优化方法,包括状态绑定、初始值设置、样式自定义、性能优化和跨浏览器兼容性处理,并提供了代码案例。
49 8
|
2月前
|
监控 前端开发 UED
在 React 18 中利用并发渲染提高应用性能
【10月更文挑战第12天】利用并发渲染需要综合考虑应用的特点和需求,合理运用相关特性和策略,不断进行优化和调整,以达到最佳的性能提升效果。同时,要密切关注 React 的发展和更新,以便及时利用新的技术和方法来进一步优化应用性能。你还可以结合具体的项目实践来深入理解和掌握这些方法,让应用在 React 18 的并发渲染机制下发挥出更好的性能优势。
126 59
|
1月前
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
2月前
|
JavaScript 前端开发 算法
前端优化之超大数组更新:深入分析Vue/React/Svelte的更新渲染策略
本文对比了 Vue、React 和 Svelte 在数组渲染方面的实现方式和优缺点,探讨了它们与直接操作 DOM 的差异及 Web Components 的实现方式。Vue 通过响应式系统自动管理数据变化,React 利用虚拟 DOM 和 `diffing` 算法优化更新,Svelte 通过编译时优化提升性能。文章还介绍了数组更新的优化策略,如使用 `key`、分片渲染、虚拟滚动等,帮助开发者在处理大型数组时提升性能。总结指出,选择合适的框架应根据项目复杂度和性能需求来决定。
|
2月前
|
前端开发 JavaScript 算法
React 渲染优化策略
【10月更文挑战第6天】React 是一个高效的 JavaScript 库,用于构建用户界面。本文从基础概念出发,深入探讨了 React 渲染优化的常见问题及解决方法,包括不必要的渲染、大量子组件的渲染、高频事件处理和大量列表渲染等问题,并提供了代码示例,帮助开发者提升应用性能。
60 6
|
2月前
|
JSON 前端开发 JavaScript
【简单粗暴】如何使用 React 优化 AG 网格性能
【简单粗暴】如何使用 React 优化 AG 网格性能
35 3