本文会分享一个React性能优化的故事,这也是我在工作中实在遇到的故事,最终咱们是通过魔改第三方库源码将它性能进步了几十倍。这个第三方库也是很有名的,在GitHub上有4.5k star,这就是:react-big-calendar。

这个工作不是我一个人做的,而是咱们团队几个月前共同完成的,我感觉挺有意思,就将它复盘总结了一下,分享给大家

在本文中你能够看到:

  1. React罕用性能剖析工具的应用介绍
  2. 性能问题的定位思路
  3. 常见性能优化的形式和成果:PureComponent, shouldComponentUpdate, Context, 按需渲染等等
  4. 对于第三方库的问题的解决思路

对于我工作中遇到的故事,我后面其实也分享过两篇文章了:

  1. 速度进步几百倍,记一次数据结构在理论工作中的使用
  2. 应用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自带的性能工具,根本步骤是:

  1. 关上Chrome调试工具,点到Performance一栏
  2. 点击左上角的小圆点,开始录制
  3. 执行你想要的操作,我这里就是点击一个预约
  4. 等你想要的后果进去,我这里就是点击的预约色彩加深
  5. 再点击左上角的小圆点,完结录制就能够看到了

为了让大家看得更分明,我这里录制了一个操作的动图,这个图能够看到,点击操作的响应花了很长时间,Chrome加载这个性能数据也花了很长时间:

测试数据量

下面仅仅一个点击耗时就七八秒,是因为我成心用了很大数据量吗?不是!我的测试数据量是齐全依照用户实在场景计算的:同时显示10个场馆,每个场馆每天20个预约,下面应用的是周视图,也就是能够同时看到7天的数据,那总共显示的预约就是:

10 * 20 * 7 = 1400,总共1400个预约显示在页面上。

为了跟下面这个龟速点击做个比照,我再放下优化后的动图,让大家对前面这个简明扼要实现的成果先有个预期:

定位问题

咱们个别印象中,React不至于这么慢啊,如果慢了,大概率是写代码的人没写好!咱们都晓得React有个虚构树,当一个状态扭转了,咱们只须要更新与这个状态相干的节点就行了,呈现这种状况,是不是他干了其余不必要的更新与渲染呢?为了解决这个纳闷,咱们装置了React专用调试工具:React Developer Tools。这是一个Chrome的插件,Chrome插件市场能够下载,装置胜利后,Chrome的调试工具上面会多两个Tab页:

Components这个Tab下有个设置,关上这个设置能够看到你每次操作触发哪些组件更新,咱们就是从这外面发现了一点惊喜:

为了看清楚点击事件触发哪些更新,咱们先缩小数据量,只保留一两个预约,而后关上这个设置看看:

哼,这有点意思。。。我只是点击一个预约,你把整个日历的所有组件都给我更新了!那整个日历有多少组件呢?下面这个图能够看出10:00 AM10: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);

这段代码的优化次要在这几个中央:

  1. 将数据从单纯的items拆分成了idsitems
  2. 顶层组件App应用ids来渲染列表,ids外面只有id,所以只有不是减少和删除,仅仅单条数据的状态变动,ids并不需要变动,所以App不会更新。
  3. Item组件本人去连贯本人须要的数据,当本人关怀的数据变动时才更新,其余组件的数据变动并不会触发更新。

拆解第三方库源码

下面通过应用调试工具我看到了一个相熟的景象,并猜到了他慢的起因,然而目前仅仅是猜想,具体是不是这个起因还要看看他的源码能力确认。好在我在看他的源码前先去看了下他的文档,而后发现了这个:

react-big-calendar接管两个参数onSelectEventselectedselected示意以后被选中的事件(预约),onSelectEvent能够用来扭转selected的值。也就是说当咱们选中某个预约的时候,会扭转selected的值,因为这个参数是从顶层往下传的,所以他会引起上面所有子节点的更新,在咱们这里就是差不多7000个背景格子 + 1399个其余事件,这样就导致不须要更新的组件更新了。

顶层selected换成Context?

react-big-calendar在顶层设计selected这样一个参数是能够了解的,因为使用者能够通过批改这个值来管制选中的事件。这样选中一个事件就有了两个路径:

  1. 用户通过点击某个事件来扭转selected的值
  2. 开发者能够在内部间接批改selected的值来选中某个事件

有了后面一万条数据列表优化的教训,咱们晓得对于这种问题的解决方法了:应用selected的组件本人去连贯Redux获取值,而不是从顶部传入。惋惜,react-big-calendar并没有应用Redux,也没有应用其余任何状态治理库。如果他应用Redux,咱们还能够思考增加一个action来给内部批改selected,惋惜他没有。没有Redux就玩不转了吗?当然不是!React其实自带一个全局状态共享的性能,那就是ContextReact 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来说没有变动时,其实就不须要运行rendershouldComponentUpdate就能够这样写:

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的数据结构外面eventselectedEvent是两个不同的属性,每个事件通过判断本人的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了,咱们发现性能图上鼠标事件有点扎眼:

一次点击同时触发了三个点击事件:mousedownmouseupclick。如果咱们能干掉mousedownmouseup是不是工夫又能够省一半,先去看看他注册这两个事件时干什么的吧。能够间接在代码外面全局搜mousedown,最终发现都是在Selection.js,通过对这个类代码的浏览,发现他是个典型的观察者模式,而后再搜new Selection找到应用的中央,发现mousedownmouseup次要是用来实现事件的拖拽性能的,mousedown标记拖拽开始,mouseup标记拖拽完结。如果我把它去掉,拖拽性能就没有了。通过跟产品经理沟通,咱们前面是须要拖拽的,所以这个不能删。

事件进行到这里,我也没有更多方法了,然而响应工夫还是有4秒,真是让人头大

反正没啥好方法了,我就轻易点着玩,忽然,我发现mousedown的调用栈如同有点问题:

这个调用栈我用数字分成了三块:

  1. 这外面有很多相熟的函数名啊,像啥performUnitOfWorkbeginWork,这不都是我在React Fiber这篇文章中提过的吗?所以这些是React本人外部的函数调用
  2. render函数,这是某个组件的渲染函数
  3. 这个render外面又调用了renderEvents函数,看起来是用来渲染事件列表的,次要的工夫都耗在这里了

mousedown监听自身我是干不掉了,然而外面的执行是不是能够优化呢?renderEvents曾经是库本人写的代码了,所以能够间接全局搜,看看在哪里执行的。最终发现是在TimeGrid.js的render函数被执行了,其实这个是不须要执行的,咱们间接把后面旁门左道的shouldComponentUpdate复制过去就能够阻止他的执行。而后再看下性能数据呢:

咱们发现Scripting降落到了3.2秒左右,比之前缩小约800毫秒,而mousedown的工夫也从之前的几百毫秒降落到了50毫秒,在图上简直都看不到了,mouseup事件也不怎么看失去了,又算进了一步吧~

忍痛阉割性能

到目前为止,咱们的性能优化都没有阉割性能,响应速度从7.5秒降落到了3秒多一点,优化差不多一倍。然而,目前这速度还是要三秒多,别说作为一个工程师了,作为一个用户我都忍不了。咋办呢?咱们是真的有点江郎才尽了。。。

看看下面那个性能图,次要耗费工夫的有两个,一个是click事件,还有个timertimer到当初我还不晓得他哪里来的,然而click事件咱们是晓得的,就是用户点击某个事件后,更改SelectContextselected属性,而后selected属性从顶层节点传入触发上面组件的更新,两头儿子节点通过shouldComponentUpdate跳过更新,孙子节点间接连贯SelectContext获取selected属性更新本人的状态。这个流程是咱们后面优化过的,然而,等等,这个貌似还有点问题。

在咱们的场景中,两头儿子节点其实蕴含了高达7000个背景格子,尽管咱们通过shouldComponentUpdate跳过了render的执行,然而7000个shouldComponentUpdate本省执行也是须要工夫的啊!有没有方法连shouldComponentUpdate的执行也跳过呢?这貌似是个新的思路,然而通过咱们的探讨,发现没方法在放弃性能的状况下做到,然而能够适度阉割一个性能就能够做到,那阉割的性能是哪个呢?那就是裸露给内部的受控selected属性!

后面咱们提到过选中一个事件有两个路径:

  1. 用户通过点击某个事件来扭转selected的值
  2. 开发者能够在内部间接批改selected的值来选中某个事件

之所以selected要放在顶层组件上就是为了实现第二个性能,让内部开发者能够通过这个受控的selected属性来扭转选中的事件。然而通过咱们评估,内部批改selected这个并不是咱们的需要,咱们的需要都是用户点击来选中,也就是说内部批改selected这个性能咱们能够不要。

如果不要这个性能那就有得玩了,selected齐全不必放在顶层了,只须要放在事件外层的容器上就行,这样,扭转selected值只会触发事件的更新,啥背景格子的更新压根就不会触发,那怎么改呢?在咱们后面的Calendar -- Background -- Event模型上再加一层EventContainer,变成Calendar -- Background -- EventContainer -- EventSelectContext.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也能够,能够间接作为EventContainerstate然而如果EventContainerEvent两头还有层级,须要穿透传递,依然须要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,造福大家的。然而优化计划的确有点旁门左道:

  1. 应用了JSON.stringify来进行shouldComponentUpdate的比照优化,对于函数,Symbol属性的扭转没法监听到,不适宜凋谢应用,只能在数据本人可控的状况下小规模应用。
  2. 就义了一个裸露给内部的受控属性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个组件更新依然要花大量的工夫。

第二步优化

为了缩小点击后更新的事件数量,咱们为事件引入按需渲染,只渲染用户可见的事件组件。同时咱们还对mousedownmouseup进行了优化,也是应用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