定位性能瓶颈
首先用React DevTools的Profiler,这个最直观。录制一段操作,看哪些组件渲染时间长、渲染次数多。我之前发现一个表格组件每次输入都重渲染,耗时200多ms。
然后看Chrome DevTools的Performance面板,找Long Task,看是JS执行慢还是渲染慢。有次发现是个图表计算数据太重了,一次要300ms。
还会在代码里打console.time,手动测关键函数耗时,比如数据处理、过滤这些。
看Network面板,确认是不是接口慢。有时候不是代码问题,是接口返回慢导致的。
优化手段
定位到问题后,我主要这么优化:
1. 减少不必要的重渲染
用React.memo包一些纯展示组件,用useMemo缓存计算结果,useCallback缓存函数。我把那个表格组件用React.memo包了,渲染次数直接降了80%。
2. 拆分组件,状态下沉
如果一个状态只有局部用,就别放父组件。我之前把一个搜索框的state从顶层移到局部,整个页面就不会因为输入而重渲染了。
3. 虚拟列表
长列表用react-window,我们地址列表有几千条数据,用了虚拟列表后,从卡顿变丝滑,渲染时间从2秒降到50ms。
4. 懒加载和代码分割
大组件、图表这些用React.lazy按需加载,首屏快很多。
5. 优化数据处理
把重计算放worker里,或者用Web Worker。实在不行就debounce、throttle控制频率。
6. Context优化
Context的value用useMemo包起来,避免每次都是新对象导致所有消费者重渲染。
核心就是先用工具定位,别盲目优化。找到问题再对症下药,效果最好。
技巧一:尽量避免重新render
1. 使用PureComponent
React.PureComponent中浅层对比了prop和state来避免重新渲染,但是假如props和state的属性值是对象的情况下,并不能阻止不必要的渲染,因为只是比较了地址,所以在使用PureComponent的时候要确保数据类型是值的类型,如果是引用类型,最好不要有深层次的变化。
2. 使用ShouldComponentUpdate
这个函数可以决定是否要重新渲染组件,也属于一个生命周期函数,如果props更改或者调用setState这个函数会返回一个布尔值,true表示会重新渲染,如果为false则不会重新渲染。
3. 使用React.memo
如果组件在相同的props的情况下渲染结果相同时,可以通过将其包装在React.memo中,React将跳过渲染组件并直接复用最近一次渲染的结果。React.memo对比的时props的变化,如果一个组件被这个钩子函数包裹,但是其内部有useState或者useReducer之类的,仍会进行重新渲染,这个也是进行的浅层比较,如果想要控制对比的过程,可以将自定义的函数通过第二个参数进行传递。
4. 使用useMemo缓存计算结果
如果一个组件中有一个计算量比较大的函数,重新渲染每次都调用比较消耗性能,所以我们可以使用useMemo来缓存这个函数的计算结果,这样只有传入的参数发生变化才会重新进行计算。
5. 使用useCallback来缓存函数
假如一个组件中有一个函数,只要状态发生变化,这个函数就会被重新定义,使用useCallback可以进行缓存。
6. 使用发布订阅模式来避免中间组件不必要的渲染
如果组件的嵌套层级比较深,可能造成中间组件不必要的渲染,可能中间组件只是传递了props,这种情况我们可以通过发布订阅模式,让只关心某个状态的组件去更新,可以借助一些类似的第三方库:redux。
7. 尽量将状态放到子组件中(状态下方)
如果一个状态只是某部分子组件在使用,可以将其提取为一个组件,然后状态定义到这个组件中,避免中间组件不必要的渲染。
8. 列表的每个item加上key属性
通过添加key属性可以更好的辅助Diff算法进行虚拟DOM计算,避免不必要的渲染。
技巧二:尽量减少要渲染的节点
1. 组件懒加载
组件懒加载实现的效果是让真正需要真是这个组件的时候才渲染,主要是通过React.lazy和React.Suspense这两个组件来进行组件懒加载。主要是使用React.lazy来定义一个动态加载的组件,React.Suspense主要是用来包裹要懒加载的组件的。
2. 使用虚拟列表
虚拟列表可以根据滚动容器的元素的高度来渲染长列表的数据,尤其是在一些没有直接分页的场景,主要是使用第三方库:react-window、react-virtualized。
技巧三:避免添加额外的DOM
使用React.fragment来避免不必要的div
因为React规定一个组件只能有一个父元素,我们可以通过React.Fragment来代替不必要的div。