React.js 一次动画性能的简单优化

背景

前段时间公司产品为了拉新活动,仿照Facebook的Creators页面决定制作一套自己的HelpCenter页面。

Facebook-Creators

Facebook的Creators页面其实大体上只有两个功能点:

  • 卡片移入和移出时的动画效果
  • 左侧菜单栏跟随当前卡片变换的效果

这个页面的重点在于每张卡片都需要独立计算位置以及独立进行动画,因此对性能要求很高。

初版

第一个版本是在实习的时候写的,因为要赶时间配合其他组同时上线,因此在整体上没有考虑性能问题。

整体的思路就是将transform属性和transform-origin属性存于每一张卡片的state中,并给每一张卡片绑定一个scroll事件监听,每次卡片移动都对在视口中的卡片进行位置检测,如果该卡片的当前位置需要进行动画的话,则直接修改state中的属性重新给dom赋值即可。

左侧菜单栏的变化则是通过在每张卡片移动到屏幕对应位置的时候,根据卡片自带的hash值修改菜单栏state中的hash实现的。

初版完成了这个页面最基础的功能,使之可用,但是由于多个scroll事件的监听(多达40+),以及每次计算所有卡片的位置,都造成了严重的性能问题,导致页面滚动时的帧数经常掉到30帧左右,一旦遇到大一些的卡片甚至会产生卡帧的情况,整个页面的运行时间有70%左右的时间在运行js,而facebook的页面仅仅只有不到20%的时间在运行js。

Facebook性能

因此优化的工作迫在眉睫。

减少计算工作量

第一次优化的目标是不影响正常用户体验的情况下,减少监听的事件数量,以及减少对卡片位置的计算。

事件部分的优化思路是:删除每一个卡片自身的scroll事件监听,在父级组件用一个scroll事件监听所有卡片位置,每次滚动的时候都遍历计算所有卡片的位置信息,不在动画区域内的卡片直接跳过,在动画区域内的卡片才进行动画计算。但是优化之后的方法在效率上提升不大,因此猜测大部分时间都消耗在了计算上面。

因此对卡片位置的计算进行了优化。以前计算卡片位置的时候,都是重新根据getBoundingClientRect方法获取到的位置信息,再根据当前视口的宽高信息来计算结果,本身getBoundingClientRect方法就比较耗时间,并且每次滚动都要遍历每一张卡片,效率极低,虽然尝试过通过截流了方式进行滚动,但是一旦经过截流处理,卡片的动画就变的极为不流畅,因此并没有采用截流的方式。

后来我将每一张卡片的初始位置储存起来,每次滚动的时候不再需要重新计算每一张卡片的位置信息,只需要根据滚动的距离更新储存的数据,然后遍历一个最大只有40+的数组进行位置判断即可,这样就节省了大量的在获取位置部分浪费的时间。

经过两步优化之后的卡片动画比初版好了很多,帧数最多能达到50帧,运行时间也减少到40-50%左右,但是依然和facebook有很大的差距。

不使用setState

减少计算之后,下一个目标就是setState了,setState本身就有一个计算过程,并且会导致重新渲染,scroll时候大量的setState是造成性能瓶颈的最大元凶。

react官方推荐的修改style的方式是:

  • 修改className来修改style
  • 通过setState修改内联style

react本身不推荐直接修改dom的特性,以及常年写通过state修改视图的react写法习惯让我第一时间并没有想到修改减少setState的使用的思路,后来看了一下调用栈消耗时间发现经过了计算优化之后,剩下的最多的部分就是setState了,于是决定做一下实验,不使用setState,直接修改卡片的style试试。

我直接去掉了卡片组件中的state,直接在卡片初始化完成之后将实例传给父级组件保存起来,每次父级组件滚动的时候,经过计算后,直接修改dom实例的style,不再经过state修改。

import produce from 'immer';
/**
   * @description 初始化所有卡片的高度,并将cardBody{DOM}元素挂载到cardList上
   * @param {Object} item
   * @param {Number} height
   */
  initCardContainer(item, height, cardBody) {
    this.setState(
      produce(draft => {
        const index = draft.cardList.findIndex(i => i.hash === item.hash);
        if (index > -1) {
          draft.cardList[index] = Object.assign({}, draft.cardList[index], {
            height,
            cardBody
          });
        }
      })
    );
  }
/**
   * @description 改变卡片的style
   * @param {Object} item
   * @returns {Object}: style
   */
  changeCardState(item) {
    // 卡片在视口之外的样式
    let style = defaultStyle;

    const { bodyTop, bodyBottom } = item;
    const scrollY = window.scrollY + document.body.scrollTop;
    const scrollHeight = document.body.scrollHeight;
    if (bodyBottom < scrollY + OFFSET && bodyBottom >= scrollY - OFFSET) {
      // 卡片从上方进入/退出
      const cal = 1 - (bodyBottom - scrollY) / OFFSET;
      style = {
        transformOrigin: '50% 100% 0',
        rotate: ROTATE * cal,
        translate: -TRANSLATEZ * cal
      };
    } else if (
      bodyTop > scrollY + innerHeight - OFFSET &&
      bodyTop <= scrollY + innerHeight + OFFSET
    ) {
      // 卡片从下方进入/退出
      const cal = 1 - (scrollY + innerHeight - bodyTop) / OFFSET;
      style = {
        transformOrigin: '50% 0% 0',
        rotate: -ROTATE * cal,
        translate: -TRANSLATEZ * cal
      };
    } else {
      // 卡片在视口之外或屏幕中央,不需要动画
      style = {
        transformOrigin: '50% 50% 0',
        rotate: 0,
        translate: 0
      };
    }
    return style;
  }
复制代码

果然,去掉setState之后,整体性能提升了一倍之多,js运行时间直接从50%降低到了15%-20%,页面稳定50-70帧,滚动起来十分流畅,基本达到了facebook对应页面的性能标准。

去掉setState后的性能

其他优化

  1. 配置优化

将卡片的所有信息都配置成了config.json文件的形式,页面中直接读取配置文件进行渲染。

{
        "title": "Header Demo",
        "type": "header",
        "children": [
            {
                "title": "Title One",
                "content": "#### Content One"
            },
            {
                "title": "Title Two",
                "content": "<p>Hello World</p>"
            }
        ]
    },
复制代码

这样以后修改的时候可以不需要修改组件代码,直接修改配置文件的配置即可。

以前的卡片部分是通过读取config配置文件,然后经过递归进行渲染,优化之后在渲染卡片之前,先将整个配置文件打平,然后直接对卡片数组进行一次遍历渲染即可,这样在render时可以减少大量的递归计算时间。

import produce from 'immer';
/**
 * @description 展平配置文件的树形结构
 * @param {Array} list
 * @returns {Array} arr
 */
const expandConfig = list => {
  if (!(list instanceof Array)) {
    return [];
  }
  const arr = [];
  const recursion = list => {
    if (list instanceof Array) {
      list.forEach(i => {
        // 防止修改cardList污染menuList
        arr.push(
          produce(i, draft => {
            draft.hash = draft.title
              ? util.getHash(draft.title)
              : util.genRandomId();
            return draft;
          })
        );
        if (i.children && i.children instanceof Array) {
          recursion(i.children);
        }
      });
    }
  };
  recursion(list);
  return arr;
};
复制代码

至此本次优化结束,基本达成了追平Facebook性能的目标。

猜你喜欢

转载自juejin.im/post/5bc3f605e51d450e6749a7bc