本文会分享一个React性能优化的故事,这也是我在工作中实在遇到的故事,最终咱们是通过魔改第三方库源码将它性能进步了几十倍。这个第三方库也是很有名的,在GitHub上有4.5k star,这就是:react-big-calendar。
这个工作不是我一个人做的,而是咱们团队几个月前共同完成的,我感觉挺有意思,就将它复盘总结了一下,分享给大家。
在本文中你能够看到:
- React罕用性能剖析工具的应用介绍
- 性能问题的定位思路
- 常见性能优化的形式和成果:
PureComponent
,shouldComponentUpdate
,Context
,按需渲染
等等 - 对于第三方库的问题的解决思路
对于我工作中遇到的故事,我后面其实也分享过两篇文章了:
- 速度进步几百倍,记一次数据结构在理论工作中的使用
- 应用mono-repo实现跨我的项目组件共享
特地是速度进步几百倍,记一次数据结构在理论工作中的使用,这篇文章在某平台单篇浏览都有三万多,有些敌人也提出了质疑。感觉我这篇文章外面提到的问题事实中不太可能遇到,外面的性能优化更多是偏实践的,有点杞人忧天。这个观点我根本是认可的,我在那篇文章正文也提到过可能是个伪需要,然而技术问题原本很多就是实践上的,咱们在leetcode上刷题还是纯理论呢,实践结合实际能力施展其真正的价值,即便是杞人忧天,然而性能的确快上了那么一点点,也给大家提供了另一个思路,我感觉也是值得的。
与之绝对的,本文提到的问题齐全不是杞人忧天了,而是实打实的用户需要,咱们通过用户调研,发现用户的确有这么多数据量,需要上不可能再压缩了,只能技术上优化,这也是逼得咱们去改第三方库源码的起因。
需要背景
老规矩,为了让大家疾速了解咱们遇到的问题,我会简略讲一下咱们的需要背景。我还是在那家外企,不久前咱们接到一个需要:做一个体育场馆治理Web App
。这外面有一个外围性能是场馆日程的治理,有点相似于大家Outlook
外面的Calendar
。大家如果用过Outlook
,应该对他的Calendar
有印象,基本上咱们的会议及其他日程安排都能够很不便的放在外面。咱们要做的这个也是相似的,体育场馆的老板能够用这个日历来治理他上面场地的预约。
假如你当初是一个羽毛球场的老板,来了个客户说,嘿,老板,这周六场地有空吗,我订一个小时呢!场馆每天都很多预约,你也不记得周六有没有空,所以你关上咱们的网站,看了下日历:
你发现1月15号,也就是星期五有两个预约,周六还全是闲暇的,于是给他说:你运气真好,周六目前还没人预约,时段轻易挑!下面这个截图是react-big-calendar
的官网示例,咱们也是选定用他来搭建咱们本人的利用。
实在场景
下面这个例子只是阐明下咱们的利用场景,外面预约只有两个,场地只有一块。然而咱们实在的客户可比这个大多了,依据咱们的调研,咱们较大的客户有数百块场地,每个场地每天的预约可能有二三十个。下面那个例子咱们换个生意比拟好的老板,假如这个老板有20块羽毛球场地,每天客户都很多,某天还是来了个客户说,嘿,老板,这周六场地有空吗,我订一个小时呢!然而这个老板生意很好,他看到的日历是这样的:
本周场馆1全满!!如果老板想要为客户找到一个有空的场地,他须要间断切换场馆1,场馆2。。。始终到场馆20,手都点酸了。。。为了缩小老板手的累赘,咱们的产品经理提出一个需要,同时在页面上显示10个场馆的日历,好在react-big-calendar
自身就是反对这个的,他把这个叫做resources。
性能爆炸
看起来咱们要的基本功能react-big-calendar
都能提供,前途还是很美妙的,直到咱们将实在的数据渲染到页面上。。。咱们的预约不仅仅是展现,还须要反对一系列的操作,比方编辑,复制,剪切,粘贴,拖拽等等。当然这所有操作的前提都是选中这个预约,上面这个截图是我选中某个预约的耗时:
仅仅是一个最简略的点击事件,脚本执行耗时6827ms
,渲染耗时708ms
,总计耗时7.5s
左右,这TM!这玩意儿还想卖钱?送给我,我都不想用!
可能有敌人不晓得这个性能怎么看,这其实是Chrome自带的性能工具,根本步骤是:
- 关上Chrome调试工具,点到
Performance
一栏 - 点击左上角的小圆点,开始录制
- 执行你想要的操作,我这里就是点击一个预约
- 等你想要的后果进去,我这里就是点击的预约色彩加深
- 再点击左上角的小圆点,完结录制就能够看到了
为了让大家看得更分明,我这里录制了一个操作的动图,这个图能够看到,点击操作的响应花了很长时间,Chrome加载这个性能数据也花了很长时间:
测试数据量
下面仅仅一个点击耗时就七八秒,是因为我成心用了很大数据量吗?不是!我的测试数据量是齐全依照用户实在场景计算的:同时显示10个场馆,每个场馆每天20个预约,下面应用的是周视图,也就是能够同时看到7天的数据,那总共显示的预约就是:
10 * 20 * 7 = 1400
,总共1400个预约显示在页面上。
为了跟下面这个龟速点击做个比照,我再放下优化后的动图,让大家对前面这个简明扼要实现的成果先有个预期:
定位问题
咱们个别印象中,React不至于这么慢啊,如果慢了,大概率是写代码的人没写好!咱们都晓得React有个虚构树,当一个状态扭转了,咱们只须要更新与这个状态相干的节点就行了,呈现这种状况,是不是他干了其余不必要的更新与渲染呢?为了解决这个纳闷,咱们装置了React专用调试工具:React Developer Tools。这是一个Chrome的插件,Chrome插件市场能够下载,装置胜利后,Chrome的调试工具上面会多两个Tab页:
在Components
这个Tab下有个设置,关上这个设置能够看到你每次操作触发哪些组件更新,咱们就是从这外面发现了一点惊喜:
为了看清楚点击事件触发哪些更新,咱们先缩小数据量,只保留一两个预约,而后关上这个设置看看:
哼,这有点意思。。。我只是点击一个预约,你把整个日历的所有组件都给我更新了!那整个日历有多少组件呢?下面这个图能够看出10:00 AM
到10:30 AM
之间是一个大格子,其实这个大格子两头还有条分割线,只是色彩较淡,看的不显著,也就是说每15分钟就是一个格子。这个15分钟是能够配置的,你也能够设置为1分钟,然而那样格子更多,性能更差!咱们是依据需要给用户提供了15分钟,30分钟,1小时等三个选项。当用户抉择15分钟的时候,渲染的格子最多,性能最差。
那如果一个格子是15分钟,总共有多少格子呢?一天是24 * 60 = 1440
分钟,15分钟一个格子,总共96
个格子。咱们周视图最多展现7天,那就是7 * 96 = 672
格子,最多能够展现10个场馆,就是672 * 10 = 6720
个格子,这还没算日期和工夫自身占据的组件,四舍五入一下权且就算7000
个格子吧。
我仅仅是点击一下预约,你就把作为背景的7000个格子全副给我更新一遍,怪不得性能差!
再认真看下下面这个动图,我点击的是小的那个事件,当我点击他时,留神大的那个事件也更新了,里面也有个蓝框,不是很显著,然而的确是更新了,在我前面调试打Log的时候也证实了这一点。所以在实在1400条数据下,被更新的还有另外1399个事件,这其实也是不必要的。
我这里提到的事件
和前文提到的预约
是一个货色,react-big-calendar
外面将这个称为event
,也就是事件
,对应咱们业务的意义就是预约
。
为什么会这样?
这个景象我如同似曾相识,也是咱们常常会犯的一个性能上的问题:将一个状态放到最顶层,而后一层一层往下传,当上面某个元素更新了这个状态,会导致根节点更新,从而触发上面所有子节点的更新。这里说的更新并不一定要从新渲染DOM节点,然而会运行每个子节点的render
函数,而后依据render
函数运行后果来做diff
,看看要不要更新这个DOM节点。React在这一步会帮咱们省略不必要的DOM操作,然而render
函数的运行却是必须的,而成千上万次render
函数的运行也会耗费大量性能。
说到这个我想起以前看到过的一个材料,也是讲这个问题的,他用了一个一万行的列表来做例子,原文在这里:high-performance-redux。上面这个例子来源于这篇文章:
function itemsReducer(state = initial_state, action) { switch (action.type) { case 'MARK': return state.map((item) => action.id === item.id ? {...item, marked: !item.marked } : item ); default: return state; }}class App extends Component { render() { const { items, markItem } = this.props; return ( <div> {items.map(item => <Item key={item.id} id={item.id} marked={item.marked} onClick={markItem} /> )} </div> ); }};function mapStateToProps(state) { return state;}const markItem = (id) => ({type: 'MARK', id});export default connect( mapStateToProps, {markItem})(App);
下面这段代码不简单,就是一个App
,接管一个items
参数,而后将这个参数全副渲染成Item
组件,而后你能够点击单个Item
来扭转他的选中状态,运行成果如下:
这段代码所有数据都在items
外面,这个参数从顶层App
传进去,当点击Item
的时候扭转items
数据,从而更新整个列表。这个运行后果跟咱们下面的Calendar
有相似的问题,当单条Item
状态扭转的时候,其余没有波及的Item
也会更新。起因也是一样的:顶层的参数items
扭转了。
说实话,相似的写法我见过很多,即便不是从App
传入,也会从其余大的组件节点传入,从而引起相似的问题。当数据量少的时候,这个问题不显著,很多时候都被忽略了,像下面这个图,即便一万条数据,因为每个Item
都很简略,所以运行一万次render
你也不会显著感知进去,在控制台看也就一百多毫秒。然而咱们面临的Calendar
就简单多了,每个子节点的运算逻辑都更简单,最终将咱们的响应速度连累到了七八秒上。
优化计划
还是先说这个一万条的列表,原作者除了提出问题外,也提出了解决方案:顶层App
只传id,Item
渲染的数据本人连贯redux store
获取。上面这段代码同样来自这篇文章:
// index.jsfunction items(state = initial_state, action) { switch (action.type) { case 'MARK': const item = state[action.id]; return { ...state, [action.id]: {...item, marked: !item.marked} }; default: return state; }}function ids(state = initial_ids, action) { return state;}function itemsReducer(state = {}, action) { return { // 留神这里,数据多了一个ids ids: ids(state.ids, action), items: items(state.items, action), }}const store = createStore(itemsReducer);export default class NaiveList extends Component { render() { return ( <Provider store={store}> <App /> </Provider> ); }}
// app.jsclass App extends Component { static rerenderViz = true; render() { // App组件只应用ids来渲染列表,不关怀具体的数据 const { ids } = this.props; return ( <div> { ids.map(id => { return <Item key={id} id={id} />; }) } </div> ); }};function mapStateToProps(state) { return {ids: state.ids};}export default connect(mapStateToProps)(App);
// Item.js// Item组件本人去连贯Redux获取数据class Item extends Component { constructor() { super(); this.onClick = this.onClick.bind(this); } onClick() { this.props.markItem(this.props.id); } render() { const {id, marked} = this.props.item; const bgColor = marked ? '#ECF0F1' : '#fff'; return ( <div onClick={this.onClick} > {id} </div> ); }}function mapStateToProps(_, initialProps) { const { id } = initialProps; return (state) => { const { items } = state; return { item: items[id], }; }}const markItem = (id) => ({type: 'MARK', id});export default connect(mapStateToProps, {markItem})(Item);
这段代码的优化次要在这几个中央:
- 将数据从单纯的
items
拆分成了ids
和items
。 - 顶层组件
App
应用ids
来渲染列表,ids
外面只有id
,所以只有不是减少和删除,仅仅单条数据的状态变动,ids
并不需要变动,所以App
不会更新。 Item
组件本人去连贯本人须要的数据,当本人关怀的数据变动时才更新,其余组件的数据变动并不会触发更新。
拆解第三方库源码
下面通过应用调试工具我看到了一个相熟的景象,并猜到了他慢的起因,然而目前仅仅是猜想,具体是不是这个起因还要看看他的源码能力确认。好在我在看他的源码前先去看了下他的文档,而后发现了这个:
react-big-calendar
接管两个参数onSelectEvent
和selected
,selected
示意以后被选中的事件(预约),onSelectEvent
能够用来扭转selected
的值。也就是说当咱们选中某个预约的时候,会扭转selected
的值,因为这个参数是从顶层往下传的,所以他会引起上面所有子节点的更新,在咱们这里就是差不多7000个背景格子 + 1399个其余事件
,这样就导致不须要更新的组件更新了。
顶层selected换成Context?
react-big-calendar
在顶层设计selected
这样一个参数是能够了解的,因为使用者能够通过批改这个值来管制选中的事件。这样选中一个事件就有了两个路径:
- 用户通过点击某个事件来扭转
selected
的值 - 开发者能够在内部间接批改
selected
的值来选中某个事件
有了后面一万条数据列表优化的教训,咱们晓得对于这种问题的解决方法了:应用selected
的组件本人去连贯Redux获取值,而不是从顶部传入。惋惜,react-big-calendar
并没有应用Redux,也没有应用其余任何状态治理库。如果他应用Redux,咱们还能够思考增加一个action
来给内部批改selected
,惋惜他没有。没有Redux就玩不转了吗?当然不是!React其实自带一个全局状态共享的性能,那就是Context
。React Context API
官网有具体介绍,我之前的一篇文章也介绍过他的根本应用办法,这里不再讲述他的根本用法,我这里想提的是他的另一个个性:应用Context Provider
包裹时,如果你传入的value
变了,会运行上面所有节点的render函数,这跟后面提到的一般props
是一样的。然而,如果Provider上面的儿子节点是PureComponent,能够不运行儿子节点的render函数,而间接运行应用这个value的孙子节点。
什么意思呢,上面我将咱们面临的问题简化来阐明下。假如咱们只有三层,第一层是顶层容器Calendar
,第二层是背景的空白格子(儿子),第三层是真正须要应用selected
的事件(孙子):
示例代码如下:
// SelectContext.js// 一个简略的Contextimport React from 'react'const SelectContext = React.createContext()export default SelectContext;
// Calendar.js// 应用Context Provider包裹,接管参数selected,渲染背景Backgroundimport SelectContext from './SelectContext';class Calendar extends Component { constructor(...args) { super(...args) this.state = { selected: null }; this.setSelected = this.setSelected.bind(this); } setSelected(selected) { this.setState({ selected }) } componentDidMount() { const { selected } = this.props; this.setSelected(selected); } render() { const { selected } = this.state; const value = { selected, setSelected: this.setSelected } return ( <SelectContext.Provider value={value}> <Background /> </SelectContext.Provider> ) }}
// Background.js// 继承自PureComponent,渲染背景格子和事件Eventclass Background extends PureComponent { render() { const { events } = this.props; return ( <div> <div>这外面是7000个背景格子</div> 上面是渲染1400个事件 {events.map(event => <Event event={event}/>)} </div> ) }}
// Event.js// 从Context中取selected来决定本人的渲染款式import SelectContext from './SelectContext';class Event extends Component { render() { const { selected, setSelected } = this.context; const { event } = this.props; return ( <div className={ selected === event ? 'class1' : 'class2'} onClick={() => setSelected(event)}> </div> ) }}Event.contextType = SelectContext; // 连贯Context
什么是PureComponent?
咱们晓得如果咱们想阻止一个组件的render
函数运行,咱们能够在shouldComponentUpdate
返回false
,当新的props
绝对于老的props
来说没有变动时,其实就不须要运行render
,shouldComponentUpdate
就能够这样写:
shouldComponentUpdate(nextProps) { const fields = Object.keys(this.props) const fieldsLength = fields.length let flag = false for (let i = 0; i < fieldsLength; i = i + 1) { const field = fields[i] if ( this.props[field] !== nextProps[field] ) { flag = true break } } return flag }
这段代码就是将新的nextProps
与老的props
一一进行比照,如果一样就返回false
,不须要运行render
。而PureComponent
其实就是React官网帮咱们实现了这样一个shouldComponentUpdate
。所以咱们下面的Background
组件继承自PureComponent
,就自带了这么一个优化。如果Background
自身的参数没有变动,他就不会更新,而Event
因为本人连贯了SelectContext
,所以当SelectContext
的值变动的时候,Event
会更新。这就实现了我后面说的如果Provider上面的儿子节点是PureComponent,能够不运行儿子节点的render函数,而间接运行应用这个value的孙子节点。
PureComponent不起作用
现实是美妙的,事实是骨感的。。。实践上来说,如果我将两头儿子这层改成了PureComponent
,背景上7000个格子就不应该更新了,性能应该大幅提高才对。然而我测试后发现并没有什么用,这7000个格子还是更新了,什么鬼?其实这是PureComponent
自身的一个问题:只进行浅比拟。留神this.props[field] !== nextProps[field]
,如果this.props[field]
是个援用对象呢,比方对象,数组之类的?因为他是浅比拟,所以即便前后属性内容没变,然而援用地址变了,这两个就不一样了,就会导致组件的更新!
而在react-big-calendar
外面大量存在这种计算后返回新的对象的操作,比方他在顶层Calendar
外面有这种操作:
代码地址:https://github.com/jquense/react-big-calendar/blob/master/src/Calendar.js#L790
这行代码的意思是每次props
扭转都去从新计算状态state
,而他的计算代码是这样的:
代码地址:https://github.com/jquense/react-big-calendar/blob/master/src/Calendar.js#L794
留神他的返回值是一个新的对象,而且这个对象外面的属性,比方localizer
的计算方法mergeWithDefaults
也是这样,每次都返回新的对象:
代码地址:https://github.com/jquense/react-big-calendar/blob/master/src/localizer.js#L39
这样会导致两头儿子节点每次承受到的props
尽管内容是一样的,然而因为是一个新对象,即便应用了PureComponent
,其运行后果也是须要更新。这种操作在他的源码中大量存在,其实从性能角度来说,这样写是能够了解的,因为我有时候也会这么干。。。有时候某个属性更新了,不太确定要不要更新上面的组件,罗唆间接返回一个新对象触发更新,省事是省事了,然而面对咱们这种近万个组件的时候性能就崩了。。。
旁门左道shouldComponentUpdate
如果只有一两个属性是这样返回新对象,我还能够思考给他重构下,然而调试了一下发现有大量的属性都是这样,咱也不是他作者,也不晓得会不会改坏性能,没敢乱动。然而不动性能也绷不住啊,想来想去,还是在儿子的shouldComponentUpdate
上动点手脚吧。简略的this.props[field] !== nextProps[field]
判断必定是不行的,因为援用地址变啦,然而他内容其实是没变,那咱们就判断他的内容吧。两个对象的深度比拟须要应用递归,也能够参考React diff
算法来进行性能优化,然而无论你怎么优化这个算法,性能最差的时候都是两个对象一样的时候,因为他们是一样的,你须要遍历到最深处能力必定他们是一样的,如果对象很深,这种递归算法不见得会比运行一遍render
快,而咱们面临的大多数状况都是这种性能最差的状况。所以递归比照不太靠谱,其实如果你对这些数据心里有数,没有循环援用什么的,你能够思考间接将两个对象转化为字符串来进行比照,也就是
JSON.stringify(this.props[field]) !== JSON.stringify(nextProps[field])
留神,这种形式只实用于你对props数据理解,没有循环援用,没有变动的Symbol,函数之类的属性,因为JSON.stringify执行时会丢掉Symbol和函数,所以我说他是旁门左道性能优化。
将这个转化为字符串比拟的shouldComponentUpdate
加到背景格子的组件上,性能失去了明显增强,点击相应速度从7.5秒降落到了5.3秒左右。
按需渲染
下面咱们用shouldComponentUpdate
阻止了7000个背景格子的更新,响应工夫降落了两秒多,然而还是须要5秒多工夫,这也很难承受,还须要进一步优化。依照咱们之前说的如果还能阻止另外1399个事件的更新那就更好了,然而通过对他数据结构的剖析,咱们发现他的数据结构跟咱们后面举的列表例子还不一样。咱们列表的例子所有数据都在items
外面,是否选中是item
的一个属性,而react-big-calendar
的数据结构外面event
和selectedEvent
是两个不同的属性,每个事件通过判断本人的event
是否等于selectedEvent
来判断本人是否被选中。这造成的后果就是每次咱们选中一个事件,selectedEvent
的值都会变动,每个事件的属性都会变动,也就是会更新,运行render
函数。如果不改这种数据结构,是阻止不了另外1399个事件更新的。然而改这个数据结构改变太大,对于一个第三方库,咱们又不想动这么多,怎么办呢?
这条路走不通了,咱们齐全能够换一个思路,背景7000个格子,再加上1400个事件,用户屏幕有那么大吗,看得完吗?必定是看不完的,既然看不完,那咱们只渲染他能看到局部不就能够了!依照这个思路,咱们找到了一个库:react-visibility-sensor。这个库应用办法也很简略:
function MyComponent (props) { return ( <VisibilitySensor> {({isVisible}) => <div>I am {isVisible ? 'visible' : 'invisible'}</div> } </VisibilitySensor> );}
联合咱们后面说的,咱们能够将VisibilitySensor
套在Background
下面:
class Background extends PureComponent { render() { return ( <VisibilitySensor> {({isVisible}) => <Event isVisible={isVisible}/> } </VisibilitySensor> ) }}
而后Event
组件如果发现自己处于不可见状态,就不必渲染了,只有当本人可见时才渲染:
class Event extends Component { render() { const { selected } = this.context; const { isVisible, event } = this.props; return ( { isVisible ? ( <div className={ selected === event ? 'class1' : 'class2'}> 简单内容 </div> ) : null} ) }}Event.contextType = SelectContext;
依照这个思路咱们又改了一下,发现性能又晋升了,整体工夫降落到了大略4.1秒:
认真看上图,咱们发现渲染事件Rendering
工夫从1秒左右降落到了43毫秒,快了二十几倍,这得益于渲染内容的缩小,然而Scripting
工夫,也就是脚本执行工夫依然高达4.1秒,还须要进一步优化。
砍掉mousedown事件
渲染这块曾经没有太多方法能够用了,只能看看Scripting
了,咱们发现性能图上鼠标事件有点扎眼:
一次点击同时触发了三个点击事件:mousedown
,mouseup
,click
。如果咱们能干掉mousedown
,mouseup
是不是工夫又能够省一半,先去看看他注册这两个事件时干什么的吧。能够间接在代码外面全局搜mousedown
,最终发现都是在Selection.js,通过对这个类代码的浏览,发现他是个典型的观察者模式,而后再搜new Selection
找到应用的中央,发现mousedown
,mouseup
次要是用来实现事件的拖拽性能的,mousedown
标记拖拽开始,mouseup
标记拖拽完结。如果我把它去掉,拖拽性能就没有了。通过跟产品经理沟通,咱们前面是须要拖拽的,所以这个不能删。
事件进行到这里,我也没有更多方法了,然而响应工夫还是有4秒,真是让人头大
反正没啥好方法了,我就轻易点着玩,忽然,我发现mousedown
的调用栈如同有点问题:
这个调用栈我用数字分成了三块:
- 这外面有很多相熟的函数名啊,像啥
performUnitOfWork
,beginWork
,这不都是我在React Fiber这篇文章中提过的吗?所以这些是React本人外部的函数调用 render
函数,这是某个组件的渲染函数- 这个
render
外面又调用了renderEvents
函数,看起来是用来渲染事件列表的,次要的工夫都耗在这里了
mousedown
监听自身我是干不掉了,然而外面的执行是不是能够优化呢?renderEvents
曾经是库本人写的代码了,所以能够间接全局搜,看看在哪里执行的。最终发现是在TimeGrid.js的render
函数被执行了,其实这个是不须要执行的,咱们间接把后面旁门左道的shouldComponentUpdate
复制过去就能够阻止他的执行。而后再看下性能数据呢:
咱们发现Scripting
降落到了3.2秒左右,比之前缩小约800毫秒,而mousedown
的工夫也从之前的几百毫秒降落到了50毫秒,在图上简直都看不到了,mouseup
事件也不怎么看失去了,又算进了一步吧~
忍痛阉割性能
到目前为止,咱们的性能优化都没有阉割性能,响应速度从7.5秒降落到了3秒多一点,优化差不多一倍。然而,目前这速度还是要三秒多,别说作为一个工程师了,作为一个用户我都忍不了。咋办呢?咱们是真的有点江郎才尽了。。。
看看下面那个性能图,次要耗费工夫的有两个,一个是click
事件,还有个timer
。timer
到当初我还不晓得他哪里来的,然而click
事件咱们是晓得的,就是用户点击某个事件后,更改SelectContext
的selected
属性,而后selected
属性从顶层节点传入触发上面组件的更新,两头儿子节点通过shouldComponentUpdate
跳过更新,孙子节点间接连贯SelectContext
获取selected
属性更新本人的状态。这个流程是咱们后面优化过的,然而,等等,这个貌似还有点问题。
在咱们的场景中,两头儿子节点其实蕴含了高达7000个背景格子,尽管咱们通过shouldComponentUpdate
跳过了render
的执行,然而7000个shouldComponentUpdate
本省执行也是须要工夫的啊!有没有方法连shouldComponentUpdate
的执行也跳过呢?这貌似是个新的思路,然而通过咱们的探讨,发现没方法在放弃性能的状况下做到,然而能够适度阉割一个性能就能够做到,那阉割的性能是哪个呢?那就是裸露给内部的受控selected
属性!
后面咱们提到过选中一个事件有两个路径:
- 用户通过点击某个事件来扭转
selected
的值 - 开发者能够在内部间接批改
selected
的值来选中某个事件
之所以selected
要放在顶层组件上就是为了实现第二个性能,让内部开发者能够通过这个受控的selected
属性来扭转选中的事件。然而通过咱们评估,内部批改selected
这个并不是咱们的需要,咱们的需要都是用户点击来选中,也就是说内部批改selected
这个性能咱们能够不要。
如果不要这个性能那就有得玩了,selected
齐全不必放在顶层了,只须要放在事件外层的容器上就行,这样,扭转selected
值只会触发事件的更新,啥背景格子的更新压根就不会触发,那怎么改呢?在咱们后面的Calendar -- Background -- Event
模型上再加一层EventContainer
,变成Calendar -- Background -- EventContainer -- Event
。SelectContext.Provider
也不必包裹Calendar
了,间接包裹EventContainer
就行。代码大略是这个样子:
// Calendar.js// Calendar简略了,不必承受selected参数,也不必SelectContext.Provider包裹了class Calendar extends Component { render() { return ( <Background /> ) }}
// Background.js// Background要不要应用shouldComponentUpdate阻止更新能够看看还有没有其余参数变动,因为selected曾经从顶层拿掉了// 扭转selected原本就不会触发Background更新// Background不再渲染单个事件,而是渲染EventContainerclass Background extends PureComponent { render() { const { events } = this.props; return ( <div> <div>这外面是7000个背景格子</div> 上面是渲染1400个事件 <EventContainer events={events}/> </div> ) }}
// EventContainer.js// EventContainer须要SelectContext.Provider包裹// 代码相似之前的Calendarimport SelectContext from './SelectContext';class EventContainer extends Component { constructor(...args) { super(...args) this.state = { selected: null }; this.setSelected = this.setSelected.bind(this); } setSelected(selected) { this.setState({ selected }) } render() { const { selected } = this.state; const { events } = this.props; const value = { selected, setSelected: this.setSelected } return ( <SelectContext.Provider value={value}> {events.map(event => <Event event={event}/>)} </SelectContext.Provider> ) }}
// Event.js// Event跟之前是一样的,从Context中取selected来决定本人的渲染款式import SelectContext from './SelectContext';class Event extends Component { render() { const { selected, setSelected } = this.context; const { event } = this.props; return ( <div className={ selected === event ? 'class1' : 'class2'} onClick={() => setSelected(event)}> </div> ) }}Event.contextType = SelectContext; // 连贯Context
这种构造最大的变动就是当selected
变动的时候,更新的节点是EventContainer
,而不是顶层Calendar
,这样就不会触发Calendar
下其余节点的更新。毛病就是Calendar
无奈从内部接管selected
了。
须要留神一点是,如果像咱们这样EventContainer
上面间接渲染Event
列表,selected
不必Context
也能够,能够间接作为EventContainer
的state
。然而如果EventContainer
和Event
两头还有层级,须要穿透传递,依然须要Context
,两头层级和以前的相似,应用shouldComponentUpdate
阻止更新。
还有一点,因为selected
不在顶层了,所以selected
更新也不会触发两头Background
更新了,所以Background
上的shouldComponentUpdate
也能够删掉了。
咱们这样优化后,性能又晋升了:
当初Scripting
工夫间接从3.2秒降到了800毫秒,其中click
事件只有163毫秒,当初从我应用来看,卡顿曾经不显著了,间接录个动图来比照下吧:
下面这个动图曾经根本看不出卡顿了,然而咱们性能图上为啥还有800毫秒呢,而且有一个很长的Timer Fired
。通过咱们的认真排查,发现这其实是个乌龙,Timer Fired
在我一开始录制性能就呈现了,那时候我还在切换页面,还没来得及点击呢,如果咱们点进去会发现他其实是按需渲染引入的react-visibility-sensor
的一个查看元素可见性的定时工作,并不是咱们点击事件的响应工夫。把这块去掉,咱们点击事件的响应工夫其实不到200毫秒。
从7秒多优化到不到200毫秒,三十多倍的性能优化,终于能够交差了,哈哈????
总结
本文分享的是我工作中理论遇到的一个案例,实现的成果是将7秒左右的响应工夫优化到了不到200毫秒,优化了三十几倍,优化的代价是就义了一个不罕用的性能。
原本想着要是优化好了能够给这个库提个PR,造福大家的。然而优化计划的确有点旁门左道:
- 应用了
JSON.stringify
来进行shouldComponentUpdate
的比照优化,对于函数,Symbol
属性的扭转没法监听到,不适宜凋谢应用,只能在数据本人可控的状况下小规模应用。 - 就义了一个裸露给内部的受控属性
selected
,毁坏了性能。
基于这两点,PR咱们就没提了,而是将批改后的代码放到了本人的公有NPM仓库。
上面再来总结下本文面临的问题和优化思路:
遇到的问题
咱们需要是要做一个体育场馆的治理日历,所以咱们应用了react-big-calendar
这个库。咱们需要的数据量是渲染7000个背景格子,而后在这个背景格子上渲染1400个事件。这近万个组件渲染后,咱们发现仅仅一次点击就须要7秒多,齐全不能用。通过粗疏排查,咱们发现慢的起因是点击事件的时候会扭转一个属性selected
。这个属性是从顶层传下来的,扭转后会导致所有组件更新,也就是所有组件都会运行render
函数。
第一步优化
为了阻止不必要的render
运行,咱们引入了Context
,将selected
放到Context
上进行透传。两头层级因为不须要应用selected
属性,所以能够应用shouldComponentUpdate
来阻止render
的运行,底层须要应用selected
的组件自行连贯Context
获取。
第一步优化的成果
响应工夫从7秒多降落到5秒多。
第一步优化的问题
底层事件依然有1400个,获取selected
属性后,1400个组件更新依然要花大量的工夫。
第二步优化
为了缩小点击后更新的事件数量,咱们为事件引入按需渲染,只渲染用户可见的事件组件。同时咱们还对mousedown
和mouseup
进行了优化,也是应用shouldComponentUpdate
阻止了不必要的更新。
第二步优化成果
响应工夫从5秒多降落到3秒多。
第二步优化的问题
响应工夫依然有三秒多,通过剖析发现,背景7000个格子尽管应用shouldComponentUpdate
阻止了render
函数的运行,然而shouldComponentUpdate
自身运行7000次也要费很长时间。
第三步优化
为了让7000背景格子连shouldComponentUpdate
都不运行,咱们忍痛阉割了顶层受控的selected
属性,间接将它放到了事件的容器上,它的更新再也不会触发背景格子的更新了,也就是连shouldComponentUpdate
都不运行了。
第三步优化成果
响应工夫从3秒多降落到不到200毫秒。
第三步优化的问题
性能被阉割了,其余完满!
参考资料:
react-big-calendar仓库
high-performance-redux
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。
欢送关注我的公众号进击的大前端第一工夫获取高质量原创~
“前端进阶常识”系列文章源码地址: https://github.com/dennis-jiang/Front-End-Knowledges