点击上方“前端技术砖家”关注

性能优化不是一个简单的事情,但在 95% 以上的 React 项目中,是不需要考虑的,按自己的想法奔放的使用就可以了。

我认为性能优化最好的时候是项目启动时。在项目启动时,需要充分考虑页面的复杂度,如果非常复杂,则必须提前制定各种措施,防止出现性能问题。如果前期评估页面不复杂,那大概率不会出现什么性能问题。最惨的事情就是前期没有评估,中后期碰到了性能问题,解决起来就相当棘手了。

这篇文章会分享 React 项目常见的性能分析手段及优化手段,碰到性能问题的同学可以看看,没碰到性能问题的同学也需要提前预警了。

性能分析

Performance

说到性能分析,当然要有一些指标,来度量现在网页“卡”的程度,并指导我们持续改进。chrome 自带的 Performance,一般就足够我们进行分析了。


我写了一个简单的卡顿的例子,我们尝试通过 Performance 来分析出这个例子中哪一行代码卡。首先你可以打开这个示例页面(https://h1e7f.sse.codesandbox.io/demo1),在这个页面的 input 框中输入的时候,你能明显感觉到非常卡顿。


从上面的动图可以看到,最后上面一栏出现很多红线,这就代表性能出问题了。



我们看下 Frames(帧) 这一栏,能看到红框中在一次输入中,776.9 ms 内都是 1 fps 的。这代表什么意思?我们知道正常网页刷新频率一般是 60 帧,也就是 16.67ms(1s/60)必须要刷新一次,否则就会有卡顿感,刷新时间越长,就越卡顿,在当前例子中,我们输入字符后,776.9 ms 后才触发更新,可以说是相当相当卡了。我们知道 JS 是单线程的,也就是执行代码与绘制是同一个线程,必须等代码执行完,才能开始绘制。那具体是那一块代码执行时间长了呢?这里我们就要看 Main 这一栏,这一栏列出了 JS 调用栈。


在 Main 这一栏中,可以看到我们的 KeyPress 事件执行了 771.03ms,然后往上拖动,就能看到 KeyPress 中 JS 的执行栈,能找到每个函数的执行时间。


拖动到最下面,你可以看到 onChange 函数执行了很长时间,点击它,你可以在下面看到这个函数的具体信息,点击 demo1.js:7 甚至能看到每一行执行了多长时间。


罪魁祸首找到了,第九行代码执行了 630ms,找到问题所在,就好解决了。这是一个最简单的例子,这种由单个地方引起的性能问题,也是比较好解决的。找到它、修改它、解决它!

React Profiler

React.Profiler 是 React 提供的,分析组件渲染次数、开始时间及耗时的一个 API,你可以在官网找到它的文档(https://zh-hans.reactjs.org/docs/profiler.html)。

当然我们不需要每个组件都去加一个 React.Profiler 包裹,在开发环境下,React 会默记录每个组件的信息,我们可以通过 Chrome Profiler Tab 整体分析。

当然我们的 Chrome 需要安装 React 扩展,才能在工具栏中找到 Profiler 的 Tab。


Profiler 的用法和 Performance 用法差不多,点击开始记录,操作页面,然后停止记录,就会产出相关数据。


我找了一张比较复杂的图来做个示例,图中的数字分别表示:本次操作 React 做了 26 次 commit,第 14 次 commit 耗时最长,该次 commit 从 3.4s 时开始,消耗了 89.1 ms。


同时我们切换到 Ranked 模式,可以看到该次 commit,每个组件的耗时排名。比如下图表示 MarkdownText 组件耗时最长,达到 13.7 ms。


通过 React.Profiler,我们可以清晰的看到 React 组件的执行次数及时间,为我们优化性能指明了方向。
但我们需要注意的是,React.Profiler 记录的是 commit 阶段的数据。React 的执行分为两个阶段:

  • render 阶段:该阶段会确定例如 DOM 之类的数据需要做哪些变化。在这个阶段,React 将会执行 render 及 render 之前的生命周期。

  • commit 阶段:该阶段 React 会提交更新,同时在这个阶段,React 会执行像 componentDidMount 和 componentDidUpdate 之类的生命周期函数。

所以 React.Profiler 的分析范围是有限的,比如我们最开始的 input 示例,通过 React Profiler 是分析不出来性能问题的。

性能改进

如果所有的性能问题都像上面这么简单就好了。某个点耗时极长,找到它并改进之,皆大欢喜。但在 React 项目中,最容易出现的问题是组件太多,每个组件执行 1ms,一百个组件就执行了 100ms,怎么优化?没有任何一个突出的点可以攻克,我们也不可能把一百个组件都优化成 0.01 ms。

class App extend React.Component{    constructor(props){      super(props);    this.state={      count: 0    }  }  render(){      return (      <div><A /><B /><C /><D /><Button onClick={()=>{ this.setState({count: 1}) }}>clickButton>div>    )  }}

就像上面这个组件一样,当我们点击 Button 更新 state 时,A/B/C/D 四个组件均会执行一次 render 计算,这些计算完全是无用的。当我们组件够多时,会逐渐成为性能瓶颈!我们目标是减少不必要的 render。

PureComponent/ShouldComponentUpdate

说到避免 Render,当然第一时间想到的就是 ShouldComponentUpdate 这个生命周期,该生命周期通过判断 props 及 state 是否变化来手动控制是否需要执行 render。当然如果使用 PureComponent,组件会自动处理 ShouldComponentUpdate。

使用 PureComponent/ShouldComponentUpdate 时,需要注意几点:

  1. PureComponent 会对 props 与 state 做浅比较,所以一定要保证 props 与 state 中的数据是 immutable 的

  2. 如果你的数据不是 immutable 的,或许你可以自己手动通过 ShouldComponentUpdate 来进行深比较。当然深比较的性能一般都不好,不到万不得已,最好不要这样搞。

React.memo

React.memo 与 PureComponent 一样,但它是为函数组件服务的。React.memo 会对 props 进行浅比较,如果一致,则不会再执行了。

const App = React.memo(()=>{    return <div>div>});

当然,如果你的数据不是 immutable 的,你可以通过 React.memo 的第二个参数来手动进行深比较,同样极其不推荐。

React.memo 对 props 的变化做了优化,避免了无用的 render。那 state 要怎么控制呢?

const [state, setState] = useState(0);

React 函数组件的 useState,其 setState 会自动做浅比较,也就是如果你在上面例子中调用了 setState(0) ,函数组件会忽略这次更新,并不会执行 render 的。一般在使用的时候要注意这一点,经常有同学掉进这个坑里面。

善用 React.useMemo

React.useMemo 是 React 内置 Hooks 之一,主要为了解决函数组件在频繁 render 时,无差别频繁触发无用的昂贵计算 ,一般会作为性能优化的手段之一。

const App = (props)=>{  const [boolean, setBoolean] = useState(false);  const [start, setStart] = useState(0);

  // 这是一个非常耗时的计算  const result = computeExpensiveFunc(start);}

在上面例子中, computeExpensiveFunc 是一个非常耗时的计算,但是当我们触发 setBoolean 时,组件会重新渲染, computeExpensiveFunc 会执行一次。这次执行是毫无意义的,因为 computeExpensiveFunc 的结果只与 start 有关系。

React.useMemo 就是为了解决这个问题诞生的,它可以指定只有当 start 变化时,才允许重新计算新的 result 。

const result = useMemo(()=>computeExpensiveFunc(start), [start]);

我建议 React.useMemo 要多用,能用就用,避免性能浪费。

合理使用 React.useCallback

在函数组件中,React.useCallback 也是性能优化的手段之一。

const OtherComponent = React.memo(()=>{    ...});

const App = (props)=>{  const [boolan, setBoolean] = useState(false);  const [value, setValue] = useState(0);

  const onChange = (v)=>{        axios.post(`/api?v=${v}&state=${state}`)  }

  return (      <div>        {/* OtherComponent 是一个非常昂贵的组件 */}<OtherComponent onChange={onChange}/>div>  )}

在上面的例子中, OtherComponent 是一个非常昂贵的组件,我们要避免无用的 render。虽然 OtherComponent 已经用 React.memo 包裹起来了,但在父组件每次触发 setBoolean 时, OtherComponent 仍会频繁 render。

因为父级组件 onChange 函数在每一次 render 时,都是新生成的,导致子组件浅比较失效。通过 React.useCallback,我们可以让 onChange 只有在 state 变化时,才重新生成。

  const onChange = React.useCallback((v)=>{        axios.post(`/api?v=${v}&state=${state}`)  }, [state])

通过 useCallback 包裹后, boolean 的变化不会触发 OtherComponent ,只有 state 变化时,才会触发,可以避免很多无用的 OtherComponent 执行。
但是仔细想想, state 变化其实也是没有必要触发 OtherComponent 的,我们只要保证 onChange 一定能访问到最新的 state ,就可以避免 state 变化时,触发 OtherComponent 的 render。

 const onChange = usePersistFn((v)=>{        axios.post(`/api?v=${v}&state=${state}`)})

上面的例子,我们使用了 Umi Hooks 的 usePersistFn(https://hooks.umijs.org/zh-CN/hooks/advanced/use-persist-fn),它可以保证函数地址永远不会变化,无论何时, onChange 地址都不会变化,也就是无论何时, OtherComponent 都不会重新 render 了。

谨慎使用 Context

Context 是跨组件传值的一种方案,但我们需要知道,我们无法阻止 Context 触发的 render。

不像 props 和 state,React 提供了 API 进行浅比较,避免无用的 render,Context 完全没有任何方案可以避免无用的渲染。

有几点关于 Context 的建议:

  • Context 只放置必要的,关键的,被大多数组件所共享的状态。

  • 对非常昂贵的组件,建议在父级获取 Context 数据,通过 props 传递进来。

小心使用 Redux

Redux 中的一些细节,稍不注意,就会触发无用的 render,或者其它的坑。

精细化依赖

const App = (props)=>{  return (      <div>        {props.project.id}div>  )}export default connect((state)=>{    layout: state.layout,  project: state.project,  user: state.user})(App);

在上面的例子中,App 组件显示声明依赖了 redux 的 layout 、 project 、 user 数据,在这三个数据变化时,都会触发 App 重新 render。

但是 App 只需要监听 project.id 的变化,所以精细化依赖可以避免无效的 render,是一种有效的优化手段。

const App = (props)=>{  return (      <div>        {props.projectId}div>  )}export default connect((state)=>{  projectId: state.project.id,})(App);

不可变数据

我们经常会不小心直接操作 redux 源数据,导致意料之外的 BUG。

我们知道,JS 中的 数组/对象 是地址引用的。在下面的例子中,我们直接操作数组,并不会改变数据的地址。

const list = ['1'];const oldList = list;list.push('a');

list === oldList; //true

在 Redux 中,就经常犯这样的错误。下面的例子,当触发 PUSH 后,直接修改了 state.list ,导致 state.list 的地址并没有变化。

let initState = {  list: ['1']}

function counterReducer(state, action) {  switch (action.type) {    case 'PUSH':      state.list.push('2');      return {        list: state.list      }    default:          return state;  }}

如果组件中使用了 ShouldComponentUpdate 或者 React.memo ,浅比较 props.list === nextProps.list ,会阻止组件更新,导致意料之外的 BUG。

所以如果大量使用了 ShouldComponentUpdate 与 React.memo ,则一定要保证依赖数据的不可变性!建议使用 immer.js 来操作复杂数据。

总结

在项目初期,一定要考虑项目的复杂度,及早采取有效的措施,防止产生性能问题。如果在中后期才考虑性能问题,则难度会增加数十倍不止。

前端技术砖家

关注公众号加交流群

在看点这里

webuploader 怎么在react中_React 项目性能分析及优化相关推荐

  1. React 项目性能分析及优化

    我们看下 Frames(帧) 这一栏,能看到红框中在一次输入中,776.9 ms 内都是 1 fps 的.这代表什么意思?我们知道正常网页刷新频率一般是 60 帧,也就是 16.67ms(1s/60) ...

  2. 强大的django-debug-toolbar,django项目性能分析工具

    强大的django-debug-toolbar,django项目性能分析工具 给大家介绍一个用于django中debug模式下查看网站性能等其他信息的插件django-debug-toolbar 首先 ...

  3. 直扩 单音干扰抑制 matlab,单频干扰在直扩系统中的误码性能分析

    收稿日期 :2004 - 05 - 08 收修改稿日期 :2004 - 07 - 28 单频干扰在直扩系统中的误码性能分析 许 靖 谷春燕 易克初 (西安电子科技大学综合业务网国家重点实验室 ,西安 ...

  4. CPython解释器性能分析与优化

    原文来自微信公众号"编程语言Lab":CPython 解释器性能分析与优化 搜索关注 "编程语言Lab"公众号(HW-PLLab)获取更多技术内容! 欢迎加入 ...

  5. .NET Core引入性能分析引导优化

    "性能分析引导优化(Profile Guided Optimization,缩写PGO)"是一项原生编译技术,可用于生成高度优化的代码.它通过一个两步编译过程实现优化--用第一步记 ...

  6. 让oracle跑得更快——oracle 10g性能分析与优化思路,[让Oracle跑得更快.Oracle.10g性能分析与优化思路]概要1.doc...

    [让Oracle跑得更快.Oracle.10g性能分析与优化思路]概要1 在线事务(OLTP) 在线分析(OLAP) 在Oracle数据库中,凡是分配了存储空间的,都称为段,所有段并不一定指的是表,也 ...

  7. oracle 10g 速度慢,让Oracle跑得更快—Oracle 10g性能分析与优化思路_数据库教程

    资源名称:让Oracle跑得更快-Oracle 10g性能分析与优化思路 内容简介: 在这本书里读者将会学到作者在性能优化方面的一些思路和思考,一些故障处理的方法和原则,这些东西是作者在实践中长期积累 ...

  8. 由浅入深探究mysql索引结构原理_性能分析与优化_由浅入深探究mysql索引结构原理、性能分析与优化...

    由浅入深探究mysql索引结构原理.性能分析与优化 第一部分:基础知识第二部分:MYISAM和INNODB索引结构1, 简单介绍B-tree B+ tree树 2, MyisAM索引结构 3, Ann ...

  9. 基于linux服务器的性能分析与优化

    基于linux服务器的性能分析与优化 方面:硬件系统软件网络 现象:系统不稳定相应速度慢 web无法打开打开速度慢 方案:硬件故障更换硬件或升级硬件 系统问题修改系统参数和配置 软件问题修改和升级软件 ...

最新文章

  1. AI 不可以作为专利认证发明人,“因为它不是人”
  2. BCE或能成为BCH的一个侧链
  3. cassandra集群搭建
  4. DevExpress RichEditControl 上下翻页功能 z
  5. 12 月机器学习新书:《可解释机器学习方法的局限》,免费下载!
  6. MFC 获取命令行参数
  7. Atom ctrl+atl+b 快捷键修复
  8. c语言指针改良,重新认识C语言指针(上)(示例代码)
  9. Android项目实战(三十八):2017最新 将AndroidLibrary提交到JCenter仓库(图文教程)...
  10. Oracle分区技术特性详细解读
  11. 使用ZooKeeper实现分布式队列、分布式锁和选举详解
  12. 工业标准的品质也已成为开源世界中的范例之一
  13. 如何使用 AirPlay 在 Mac 上使用 HomePod?
  14. Pandas数据分析与处理补充习题
  15. java获取当前年第一天_java中如何获取系统时间的当前年份以及年份的第一天和最后一天...
  16. 完美起点更重要:青立方超融合易捷版 助力企业一步云就绪
  17. html中背景色优先级,CSS背景颜色优先级
  18. AD21覆铜,包地,补泪滴,有图
  19. 量子计算机英语的ppt,量子计算机简介.ppt
  20. Linux中的进程、线程和文件描述符

热门文章

  1. python计算身体质量指数_利用Python计算身体质量指数BMI来判断体型
  2. SPAdes混合组装二代、三代测序数据
  3. GEO/SRA数据库
  4. Python - 输出格式 (学习小结)
  5. Linux下su与su -命令的区别
  6. 5.Collection集合 List集合 泛型
  7. Chart.js-饼状图分析(参数分析+例图)
  8. LeetCode 102. Binary Tree Level Order Traversal--递归,迭代-Python,Java解法
  9. 管理距离 路由与交换_动态路由选择原理(距离矢量路由协议RIP)
  10. java csv格式文件写入_java csv文件写入