React 项目性能分析及优化

招聘:《宇宙第三前端团队招人啦!javascript

招聘:《宇宙第三前端团队招人啦!html

招聘:《宇宙第三前端团队招人啦!前端

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

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

这篇文章会分享 React 项目常见的性能分析手段及优化手段,碰到性能问题的同窗能够看看,没碰到性能问题的同窗也须要提早预警了。ios

性能分析

Performance

说到性能分析,固然要有一些指标,来度量如今网页“卡”的程度,并指导咱们持续改进。chrome 自带的 Performance,通常就足够咱们进行分析了。git

image.png

我写了一个简单的卡顿的例子,咱们尝试经过 Performance 来分析出这个例子中哪一行代码卡。首先你能够打开这个示例页面,在这个页面的 input 框中输入的时候,你能明显感受到很是卡顿。github

2020-03-29 20.38.01.gif

从上面的动图能够看到,最后上面一栏出现不少红线,这就表明性能出问题了。web

image.png

image.png

咱们看下 Frames(帧) 这一栏,能看到红框中在一次输入中,776.9 ms 内都是 1 fps 的。这表明什么意思?咱们知道正常网页刷新频率通常是 60 帧,也就是 16.67ms(1s/60)必需要刷新一次,不然就会有卡顿感,刷新时间越长,就越卡顿,在当前例子中,咱们输入字符后,776.9 ms 后才触发更新,能够说是至关至关卡了。chrome

咱们知道 JS 是单线程的,也就是执行代码与绘制是同一个线程,必须等代码执行完,才能开始绘制。那具体是那一块代码执行时间长了呢?这里咱们就要看 Main 这一栏,这一栏列出了 JS 调用栈。

image.png

在 Main 这一栏中,能够看到咱们的 KeyPress 事件执行了 771.03ms,而后往上托动,就能看到 KeyPress 中 JS 的执行栈,能找到每一个函数的执行时间。

image.png

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

image.png

罪魁祸首找到了,第九行代码执行了 630ms,找到问题所在,就好解决了。

这是一个最简单的例子,这种由单个地方引发的性能问题,也是比较好解决的。找到它、修改它、解决它!

React Profiler

React.Profiler 是 React 提供的,分析组件渲染次数、开始时间及耗时的一个 API,你能够在官网找到它的文档

固然咱们不须要每一个组件都去加一个 React.Profiler 包裹,在开发环境下,React 会默记录每一个组件的信息,咱们能够经过 Chrome Profiler Tab 总体分析。

固然咱们的 Chrome 须要安装 React 扩展,才能在工具栏中找到 Profiler 的 Tab。

image.png

Profiler 的用法和 Performance 用法差很少,点击开始记录,操做页面,而后中止记录,就会产出相关数据。

2020-03-29 22.18.35.gif

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

image.png

同时咱们切换到 Ranked 模式,能够看到该次 commit,每一个组件的耗时排名。好比下图表示 MarkdownText 组件耗时最长,达到 13.7 ms。

image.png

经过 React.Profiler,咱们能够清晰的看到 React 组件的执行次数及时间,为咱们优化性能指明了方向。

但咱们须要注意的是,React.Profiler 记录的是 commit 阶段的数据。React 的执行分为两个阶段:

  • render 阶段:该阶段会肯定例如 DOM 之类的数据须要作那些变化。在这个阶段,React 将会执行 render 及 render 以前的生命周期。
  • commit 阶段:该阶段 React 会提交更新,同时在这个阶段,React 会执行像 componentDidMountcomponentDidUpdate 之类的生命周期函数。

因此 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}) }}>click</Button> </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,它能够保证函数地址永远不会变化,不管什么时候, 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 的 layoutprojectuser 数据,在这三个数据变化时,都会触发 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。

因此若是大量使用了 ShouldComponentUpdateReact.meo ,则必定要保证依赖数据的不可变性!建议使用 immer.js 来操做复杂数据。

总结

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

相关文章
相关标签/搜索