前段时间公司产品为了拉新活动,仿照Facebook的Creators页面决定制做一套本身的HelpCenter页面。react
Facebook的Creators页面其实大致上只有两个功能点:json
这个页面的重点在于每张卡片都须要独立计算位置以及独立进行动画,所以对性能要求很高。数组
第一个版本是在实习的时候写的,由于要赶时间配合其余组同时上线,所以在总体上没有考虑性能问题。dom
总体的思路就是将transform属性和transform-origin属性存于每一张卡片的state中,并给每一张卡片绑定一个scroll事件监听,每次卡片移动都对在视口中的卡片进行位置检测,若是该卡片的当前位置须要进行动画的话,则直接修改state中的属性从新给dom赋值便可。性能
左侧菜单栏的变化则是经过在每张卡片移动到屏幕对应位置的时候,根据卡片自带的hash值修改菜单栏state中的hash实现的。优化
第一版完成了这个页面最基础的功能,使之可用,可是因为多个scroll事件的监听(多达40+),以及每次计算全部卡片的位置,都形成了严重的性能问题,致使页面滚动时的帧数常常掉到30帧左右,一旦遇到大一些的卡片甚至会产生卡帧的状况,整个页面的运行时间有70%左右的时间在运行js,而facebook的页面仅仅只有不到20%的时间在运行js。动画
所以优化的工做迫在眉睫。ui
第一次优化的目标是不影响正经常使用户体验的状况下,减小监听的事件数量,以及减小对卡片位置的计算。this
事件部分的优化思路是:删除每个卡片自身的scroll事件监听,在父级组件用一个scroll事件监听全部卡片位置,每次滚动的时候都遍历计算全部卡片的位置信息,不在动画区域内的卡片直接跳过,在动画区域内的卡片才进行动画计算。可是优化以后的方法在效率上提高不大,所以猜想大部分时间都消耗在了计算上面。spa
所以对卡片位置的计算进行了优化。之前计算卡片位置的时候,都是从新根据getBoundingClientRect
方法获取到的位置信息,再根据当前视口的宽高信息来计算结果,自己getBoundingClientRect
方法就比较耗时间,而且每次滚动都要遍历每一张卡片,效率极低,虽然尝试过经过截流了方式进行滚动,可是一旦通过截流处理,卡片的动画就变的极为不流畅,所以并无采用截流的方式。
后来我将每一张卡片的初始位置储存起来,每次滚动的时候再也不须要从新计算每一张卡片的位置信息,只须要根据滚动的距离更新储存的数据,而后遍历一个最大只有40+的数组进行位置判断便可,这样就节省了大量的在获取位置部分浪费的时间。
通过两步优化以后的卡片动画比第一版好了不少,帧数最多能达到50帧,运行时间也减小到40-50%左右,可是依然和facebook有很大的差距。
减小计算以后,下一个目标就是setState
了,setState
自己就有一个计算过程,而且会致使从新渲染,scroll时候大量的setState
是形成性能瓶颈的最大元凶。
react官方推荐的修改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对应页面的性能标准。
将卡片的全部信息都配置成了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性能的目标。