关于javascript:歪门邪道性能优化魔改三方库源码性能提高几十倍

6次阅读

共计 17013 个字符,预计需要花费 43 分钟才能阅读完成。

本文会分享一个 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.js
function 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.js
class 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
// 一个简略的 Context
import React from 'react'

const SelectContext = React.createContext()

export default SelectContext;
// Calendar.js
// 应用 Context Provider 包裹,接管参数 selected,渲染背景 Background
import 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,渲染背景格子和事件 Event
class 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 不再渲染单个事件,而是渲染 EventContainer
class Background extends PureComponent {render() {const { events} = this.props;
    return  (
        <div>
          <div> 这外面是 7000 个背景格子 </div>
          上面是渲染 1400 个事件
          <EventContainer events={events}/>
      </div>
    )
  }
}
// EventContainer.js
// EventContainer 须要 SelectContext.Provider 包裹
// 代码相似之前的 Calendar
import 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

正文完
 0