壹 ❀ 引

在过去的一段时间,我一直围绕项目中体验不好或者无效渲染较为严重的组件做性能优化,多少积累了一些经验所以想着整理成一片文章,下图就是优化后的一个组件,可以对比优化前一次切换与优化后多次切换的渲染颜色深度与按钮的切换速度:

关于减少组件无效渲染,与其说是提几点建议,不如说是在优化过程中所记录的一些不规范的写法,能写出更好的代码总是更棒的,也希望这几点建议能对大家能有些许帮助。当然,以下建议不管class组件还是hooks中其实都会犯,所以都有参考意义,那么本文开始。

贰 ❀ 减少无效渲染

在介绍如何减少无效渲染之前,我觉得有必要先声明两点,这对于理解如何减少无效渲染很有帮助:

第一点,所有的无效渲染都是由于数据的不稳定所造成的,而这里的不稳定一般分为两种,一种情况是数据前后不管是值还是引用都是完全相同的,可以说就是同一份数据;另一种是前后数据引用每次都不同,但它们的数据结构和值看起来又完全一样。所以对于第一种情况,既然你的值完全相同那我们完全没必要重复渲染,而对于第二种情况当你的值的结构没变化时我们保证其引用稳定就好了。

说到这里有同学可能就会想,那是不是组件内只要产生新引用数据的行为就不对呢?其实并不是,当数据本身就应该更新时,它在这一刻产生一个全新的引用很合情合理,不然我们项目里什么filter、map之类的岂不是都用不了。你也可以想想常见的state更新,我们更新state时本身也是得传入一个全新的对象而不是直接修改,所以要更新时产生新对象很合理:

const App = () => {
const [state, setState] = useState({ name: "听风", age: 29 }); const handleClick = () => {
// 错误做法,直接修改 state, 不会更新
// state.name = '行星飞行';
// 正确做法就得重新赋予一个全新的对象,不然 state 不会更新
setState({ ...state, name: '行星飞行' });
} return (
<div>
<div>{state.name}</div>
<div>{state.age}</div>
<button onClick={handleClick}>change name</button>
</div>
)
};

第二,当我们发现某个组件无效渲染严重时,你的关注点应该往上看,简单来说,我们的优化应该自上而下,当上层组件数据稳定了,在做下层组件优化时会方便很多,不然你在某个中间组件一顿操作,结果数据加工的入参自身就不稳定,这就很头疼。

强调了这两点,我们正式来了解如何减少无效渲染。

贰 ❀ 壹 合理使用memo与PureComponent

我们知道class组件的PureComponent以及函数组件的memo都具有浅比较的作用,所谓浅比较就是直接比较前后两个数据是否相等,比如:

const a = [];
const b = a;
// 因为 a b 引用和值都相同,所以相等
a === b; // true
const c = [];
// 虽然 c 也是空数组,但是引用不同所以不相等
a === c; // false

我们假设组件前后都接收了一个空数组,且它们引用也相同,那么此时如果我们组件套用了memo,那么组件就不会因为这个完全相同的数据重复渲染。因为PureComponentmemo效果相同,这里我写了一个在线的memo例子方便大家理解效果,大家可以点击按钮查看控制台,直接对比加与不加memo的差异。

在这个例子中,我在组件外层定义了一份引用始终相同的数据user,之后通过点击按钮故意改变父组件P的状态让其渲染,以此带动子组件C1 C2渲染,可见加了memoC2除了初次渲染之后并不会跟随父组件重复渲染,这就是memo的作用。

当然,假设我们的user每次都是重新创建的新对象,那我们加了memo也没任何作用,毕竟引用不同浅比较判断为false,还是会重复渲染。

另外,请合理使用这两个api,并不是所有场景都需要这么做,假设你的组件的数据流足够简单甚至没有props,你完全没必要在组件外层套一层memo

其次,某些情况下,因为我们为组件嵌套了memo会导致我们通过react插件查看组件时显示组件名为Unkown,不利于调试,所以如果你要用,你可以通过displayName显示声明组件的名称,比如:

import React, { memo } from 'react';
const Echo = memo((props) => {});
// 某些情况下,使用 memo 会导致控制台调试时组件名显示为 Unkown ,可通过 displayName 显示声明组件名解决
Echo.displayName = 'Echo';
export {Echo};

在了解了如何减少引用相同引用时的无效渲染,接下来看看那些造成引用不同的问题场景。

贰 ❀ 贰 props直接传递新对象

第一种也是最直接也最容易看出来的一种不规范写法,一般存在于对于react不太了解的新人或者一些老旧代码中,比如:

const App = () => {
return (
// 这里每次都会传递一个新的空数组过去,导致Child每次都会渲染,加了 memo 都救不了
<Child userList={[]} />
)
};

当然,它也可能不是一个空数组,但注定每次都是一个全新引用的数据:

const App = (props) => {
return (
<Child userList={[...props.list]} />
)
};

贰 ❀ 叁 不稳定的默认值

正常来说,比如子组件的userList属性规定类型是数组,在父组件加工数据时提供数据默认值是非常好的习惯,于是我常常在组件内部或者mapStateToProps中看到类似的写法:

const App = (props) => {
// 当存在时赋予空数组,保证下层数组类型的正确性
const userList = props.userList || [];
return (
<Child userList={userList} />
)
};

App多次渲染且props.userList为假值时,此时的userList也会被不断的赋予全新的空数组。还记得前文说的吗,当你结构没变化时,我们保证其引用不变不就好了,所以结合问题一与问题二,对于空数组都可以在全局赋予一个空数组,比如:

const emptyArr = [];
const App = (props) => {
// 当存在时赋予空数组,保证下层数组类型的正确性
const userList = props.userList || emptyArr;
return (
<Child userList={userList} />
)
};

这样不管App如何渲染,当userList被赋予为空数组时也能让前后引用相同。

贰 ❀ 肆 合理使用useMemo与useCallback

我们知道useMemouseCallback都能起到缓存的作用,比如下面这个例子:

// 只要 App 自身重复渲染,此时 handleClick 与 list 都会重新创建,导致引用不同,所以 C 即便加了 memo 还是会重复渲染
const App = (props)=> {
const handleClick = () => {};
const fn = () => {}
const list = [];
const user = userList.filter();
return <C onClick={handleClick} list={list} user={user} />
}

只要组件App自身重复渲染,组件内的这些属性方法本质上会被重新创建一遍,这就导致子组件C即便添加memo也无济于事,所以对于函数组件而言,一般要往下传递的数据我们可以通过useMemouseCallback包裹,保证其引用稳定性。当然,如果一份数据只是App组件自己用,那就没必要特意包裹了:

// 常量提到外层,保证引用唯一
const list = []; const App = ()=> {
// 使用 useCallback 缓存函数
const handleClick = useCallback(() => {}); // 只是自己使用,不作为props传递时,没必要使用 useCallback 嵌套
const handleOther = () => {} // 使用 useMemo 缓存结果
const user = useMemo(()=>{
return userList.filter();
},[userList]) return <C onClick={handleClick} list={list} user={user} />
}

一般useMemo、useCallbackmemo会联合使用,既然你的下层组件都会做浅比较,我们尽可能稳定上层数据引用的稳定性就很有必要;而假设组件连memo都没有,即便我们做了缓存子组件还是一样会重复渲染,所以要不要用以及为何而用大家一定要搞清楚。

贰 ❀ 伍 更稳定的useSelector

我们可以使用useSelector监听全局store的变化并从中取出我们想要的数据,而相同的数据获取如果是在class组件中则应该写在mapStateToProps中,但不管哪种写法,当我们从state中获取数据后就应该注意保持数据的稳定性,来看个例子:

const userList = useSelector((state) => {
const users = state.userList;
return users.filter((user) => user.age > 18);
});

在上述例子中,我们从state中获取了userList,之后又进行了数据加工过滤出年龄大于18的用户,这个写法看似没什么问题,但事实上全局state的状态并没有我们的想的那么稳定,所以useSelector执行的次数要比你想的要多,此时只要useSelector执行一次,我们都会从state中获取数据,并通过filter加工成一个全新的数组,这对于子组件而言是非常致命的。

如何改善呢?其实很简单,将加工的行为提到外部即可,比如:

const users = useSelector((state) => {
return state.userList;
}); const userList = useMemo(() => {
return users.filter(user => user.age > 18);
}, [users])

有同学可能就要说了,这不对吧,此时的useSelector每次都返回state.userList难道不是一个全新的对象?那useMemo不还是每次都会执行,导致userList每次都是全新的数组吗?其实并不是。

对于redux而言,我们可以将整个react appstore理解成一颗巨大的树,而树有很多分支的树根,每一枝树根都可以理解成某个组件所依赖的state,那么请问假设A组件的树根被更新了,它会对store的其它树根的引用造成影响吗?此时树还是这颗树啊,而那些没变的树根依旧是之前的树根。

我们可以通过下面的例子来理解这个过程:

const store = {
A: [],
B: {},
};
const b = store.B;
store.A = [1, 2];
const c = store.B;
c === b; // true

所以回到上文的代码,假设state中关于state.userList就没有变化,那么前后不管取多少次,因为引用相同,useMemo除了初始化会执行一次之外,之后都不会重新执行,这就能让userList彻底稳定下来。

而假设我们因为成员接口让state.userList进行了更新,正常来说应该在reducer中重新生成一个新数组再赋予给store,那么在下次useSelector执行时,我们也能拿到全新引用的users,而监听usersuseMemo就能按照正确的预期再度更新了。

其实在实际项目中,大家可能还有使用useSelector + createSelector的用法,useSelector用于监听statecreateSelector中负责提取state中的部分数据进行加工以及缓存,所以我在我们项目中多次看到类似如下的代码:

const userList = useSelector((state) => {
// 这里你就理解成一个具有缓存效果的函数就好了,入参不变结果就不变
const users = userSelector.getUser(state, userIDs);
return users.filter(user => user.age > 18);
});

本来userSelector就具备缓存的作用,当你userIDs没变化时,我失踪走缓存给你稳定的数据,结果前面刚帮你稳定完,后面通过filter又产生了一个全新的数据,数据的稳定性直接被破坏了,所以改法还是跟之前一样:

const users = useSelector((state) => {
return userSelector.getUser(state, userIDs);
}); const userList = useMemo(() => {
return users.map(user => user.age > 18);
}, [users])

贰 ❀ 陆 贪心的createSelector

既然上文提到了createSelector,这里再讲一个项目中大家很容易犯的错误写法。前文已经说了createSelector你可以理解成一个缓存函数,只是一般我们会将其与state挂钩,用于在加工state时初步做一些缓存,但实际开发中我发现了部分同事写了类似这样的代码:

export const getUserListSelector = createSelector([a, b], (a, b) => {
// 一些加工
});

这里我们定义了一个名为getUserListSelector的方法,它接受a,b两个参数,只要这两个参数不变,那么紧跟其后的callback就不会重复执行,这样就起到缓存的作用。

但同事在调用时是这么用的:

// a场景
const users = useSelector((state) => {
return getUserListSelector(state, userIDs);
}); // b场景
const users = useSelector((state) => {
return getUserListSelector(state);
});

简单来理解就是,其实存在两种取数据的场景,我们将其糅合到了getUserListSelector中,当a场景时我们需要传递两个参数,而b场景我们只用一个即可,有问题吗?有很大的问题。

createSelector这个东西与传统的缓存函数不同,一般的缓存函数是,只要你的参数不同,我们就用你参数的作为key,让结果作为value存起来,对应到上面的场景执行两次后,我们最终缓存可能是这样:

const cache = {
a-b:value1,
a:value2
}

两次入参不同,导致2次不同的缓存结果。但很尴尬的是,createSelector永远只缓存最新一次的缓存结果,也就是说对于上述createSelector只要a b两个场景都会调用,那么这个最终的数据永远都稳定不下来,两个场景始终会影响彼此。

怎么解决呢?解耦即可,我们应该让createSelector去取更稳定的数据,即便这个数据不够精准,返回后再分别在a b两个场景中单独去加工,为什么强调这一点呢?要知道createSelector经常存在嵌套关系,某个selector可能是另一个selector的入参,假设上述这个不稳定的selector返回的数据又成了其它selector的参数,这就会导致多条数据源全部不稳定,这是非常糟糕的。

OK,关于如何提升数据的稳定性,我们先介绍到这里,我也相信通过本文的阅读对于你之后的开发多少会有一些清晰的帮助。

叁 ❀ 如何排查不稳定数据?

其实聊到这里,我想大家多少都有了一些体会,可能有同学就想提问了,我自己新写一个组件我知道如何去规避这些点,那一个现有的组件假设渲染很严重,我又该如何去排查是哪些不稳定的数据导致了重复渲染呢?

方法肯定是有的,我们可以借助why-did-you-update或者why-did-you-render性能监测库,具体用法可见GitHub文档,这里就不赘述,当配置好后打开控制台刷新页面,你就能看到对应组件重复渲染的原因,比如:

上图就是因为新旧props引用不同所导致的无效渲染,至于如何减少无效渲染我想你现在也有了一些答案。

除了上面这两个库之外,我们还能利用react官网的插件Profiler,我们可以点击设置将记录组件渲染原因的选项勾上:

之后通过点击录制组件的渲染,hover到对应的组件上去就知道渲染的原因了,这也是一种排查手段,比如有如下组件:

const C = memo((props) => {
return <div>{props.num}</div>
}); const Parent = () => {
const [state, setState] = useState(1);
const handleClick = () => {
setState(state + 1);
};
return (
<>
<C num={state} />
<button onClick={handleClick}>changeState</button>
</>
);
}

我们点击父组件的按钮每次修改state,并将state传递给子组件C,通过录制,我们可以很清晰的看到是因为props num变化所导致的渲染。

有了排查手段以及修复手段,其实只要大家耐心和细心,我相信大家都能写出更优雅的组件。

叁 ❀ 总

那么到这里,本文围绕如何减少无效渲染的介绍就结束了,其实说了那么多,核心点还是关注在如何保证数据引用的稳定性,除此之外,我们也顺带介绍了几点如何排查组件渲染的小技巧。下周就离职回武汉了,希望一切能顺利,本文结束。

React性能优化,六个小技巧教你减少组件无效渲染的更多相关文章

  1. Java性能优化的十条小技巧

    1 System.nanoTime 测试性能时,System.nanoTime比System.currentTimeMills更精确,前者使用纳秒计时,且对系统影响更小. 具体来说: System.c ...

  2. UITableView的性能优化10个小技巧

    通常你会发现一个图片类的app会在一个imageView上做下面这些事情: 1  下载图片(主要的内容图片+用户头像图片)2  更新时间戳3  展示评论4  计算动态的cell的高度 Tip#1 学习 ...

  3. iOS-UITableView的性能优化10个小技巧

    通常你会发现一个图片类的app会在一个imageView上做下面这些事情: 1  下载图片(主要的内容图片+用户头像图片)2  更新时间戳3  展示评论4  计算动态的cell的高度 Tip#1 学习 ...

  4. IOS 性能优化的建议和技巧

    IOS 性能优化的建议和技巧 本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员.这是他的个人网站:http://www.marcelo ...

  5. s性能优化方面的小知识

    总结的js性能优化方面的小知识 前言 一直在学习javascript,也有看过<犀利开发Jquery内核详解与实践>,对这本书的评价只有两个字犀利,可能是对javascript理解的还不够 ...

  6. MySQL性能优化(六):分区

    原文:MySQL性能优化(六):分区 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/vbi ...

  7. react性能优化

    前面的话 本文将详细介绍react性能优化 避免重复渲染 当一个组件的props或者state改变时,React通过比较新返回的元素和之前渲染的元素来决定是否有必要更新实际的DOM.当他们不相等时,R ...

  8. React性能优化记录(不定期更新)

    React性能优化记录(不定期更新) 1. 使用PureComponent代替Component 在新建组件的时候需要继承Component会用到以下代码 import React,{Componen ...

  9. 关于React性能优化

    这几天陆陆续续看了一些关于React性能优化的博客,大部分提到的都是React 15.3新加入的PureComponent ,通过使用这个类来减少React的重复渲染,从而提升页面的性能.使用过Rea ...

随机推荐

  1. 聊聊C#中的Mixin

    写在前面 Mixin本意是指冰淇淋表面加的那些草莓酱,葡萄干等点缀物,它们负责给冰淇淋添加风味.在OOP里面也有Mixin这个概念,和它的本意相似,OOP里面的Mixin意在为类提供一些额外功能--在 ...

  2. dotnet core 也能协调分布式事务啦!

    2022 年 5 月 24 日,我们发布了 DBPack v0.1.0 版本,该版本主要 release 了分布式事务功能.在我们的规划里,DBPack 是要支持所有微服务开发语言协调分布式事务的,但 ...

  3. VmWare安装Centos8注意事项

    VmWare安装Centos8注意事项 1.需选择稍后安装操作系统 2.选择操作系统版本 3.修改虚拟机配置 4.配置完成点击开启虚拟机(注意要将鼠标放在屏幕中央,点击一下后才能使用上下键进行选择) ...

  4. Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent &插件解析

    3 Skywalking源码导入 接上文,已经学习了Skywalking的应用,接下来我们将剖析Skywalking源码,深度学习Skywalking Agent. 3.1 源码环境搭建 当前最新版本 ...

  5. 从开发一款基于Vue技术栈的全栈热重载生产环境脚手架,我学到了什么

    浏览文章前 这一期,我分享给大家三点看源码的小技巧,这也是从别的大佬那总结的. 被反复使用的代码 这样的代码是一个软件的重点函数,一个大神的写法有很多精华值得学习. 穿越时间的代码 如果一段代码10年 ...

  6. SAP 定义用户组

    SUGR,可进行创建.查看.删除等维护性操作,并可指定本组的用户

  7. XXXX系统测试计划

    XXXX系统测试计划 目录 XXXX系统测试计划 目标 概述 项目背景 适用范围 组织形式 组织架构图 角色及职责 测试工作分工 团队协作 测试对象 应测试特性 不被测试特性 测试任务安排 系统测试任 ...

  8. Oracle数据库常用查询语句

    1.[oracle@dbserver ~]$ sqlplus / as sysdbaSQL*Plus: Release 11.2.0.4.0 Production on Tue Mar 15 15:1 ...

  9. 【cartogarpher_ros】一: ros系统下的快速安装

    Cartographer是一个跨多个平台和传感器配置提供 2D 和 3D实时同步定位和映射 ( SLAM ) 的系统. 使用Cartographer有Ros集成环境和无Ros环境,对于新手快速入门,推 ...

  10. Go死锁——当Channel遇上Mutex时

    背景 用metux lock for循环,在for循环中又 向带缓冲的Channel 写数据时,千万要小心死锁! 最近,我在测试ws长链接网关,平均一个星期会遇到一次服务假死问题,因为并不是所有rou ...