前段時間公司產品爲了拉新活動,仿照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性能的目標。