
壹 ❀ 引
公司產品一直在做企業項目研發工具,所以我們自己當然也會用自己的產品去管理公司大小項目,但在此之前,項目管理體驗上一直存在一個卡頓問題。比如我剛登錄上賬號,在項目里隨便到處點點到處跳轉頁面,然后點擊項目頭部的搜索功能進行任意搜索,並成功跳轉到搜索結果頁后,再點擊chrome
的回退按鈕回到上個頁面,就會遇到長達10S的頁面卡頓,我的電腦是16G M1芯片都要卡這么久,像測試同學配置相對差一點的MAC,chrome
甚至會卡到直接失去響應,總而言之,如果客戶碰巧也這么操作了,使用體驗自然非常很好。
貳 ❀ Performance性能分析
好在chrome
已經提供了Performance
幫助我們分析頁面加載性能瓶頸問題。F12
打開控制台,點擊Performance
按鈕就能看到如下界面,考慮到我的電腦配置較好,為了模擬低配置,更好的復現問題,所以我將CPU
選項選擇為降低6倍性能6x slowdown
,另外,上面還有個Network
用於降網速,這個可用來模擬網絡慢的情況,因為我這問題跟網絡沒啥關系,這里就不管了。

然后我還是按照上面說的操作,在項目里亂點一通,到處跳轉,之后搜索,進入搜索頁面,這時候就可以開啟Performance
的錄制功能,也就是上圖那個黑色的圓形按鈕,然后點擊chrome
的回退按鈕,卡頓幾十秒后頁面終於恢復正常,我們再點擊錄制按鈕結束錄制,少許片刻,於是我們得到了如下信息:

讓我們把目光看向CPU的火焰圖,從5000ms到60000ms這么長長的一段,接近55秒的時間內CPU使用都占了一半(黃色區域),那么性能問題自然出現在這55s之間。直接看一眼下面的統計報表,也能發現總共1.1min中,有62446ms在執行JS,而等待,渲染,重繪都在百ms,因此跟這些沒太大關系。

我們拖拽鼠標,將分析范圍選中為有問題的55S之間,可以看到是一個Task任務總共耗時是54秒,注意,這是一個長任務,也就是單一跑完這個任務就用了這么久。順帶解釋下這段黃綠藍區域的含義,橫向表示這個任務耗時的長度,長度越長說明用時越久,縱向表示這個任務的調用棧,比如總任務名為Task,task下又分別包含了哪些任務呢?於是就有了縱向這樣一列。

由於任務過長,我們不得不繼續滾動鼠標,將選中范圍精確到更小的執行粒子上,神奇的事情發生了,隨着我選中范圍越來越小,下面的調用棧名稱居然就沒變過,也就是說這么長的時間里,執行的JS過程就是如下這么一段,我們前面說了,縱向表示調用棧,而在最下的anonymous
下,存在無數個黃色的小段落,粗略來看,這個方法估計被執行了上千或者上萬遍。

既然問題出在這,我們通過鼠標選中這個匿名函數,在Sunmary表報處我們就能看到這個函數所在的文件了,一個名為selector.js
的文件,點擊進入,成功找到了可能有問題的代碼:

看樣子寫這塊代碼的同學估計也猜到這里特別耗性能,所以才使用了memoize
做了緩存,只是沒想到第一次緩存准備數據還是要等待幾十秒。出於好奇,我在這段代碼前后加了console.time
與console.timeEnd
,刷新,重新走一遍復現流程,看了眼控制台,人傻了...接近46秒,好奇看了眼這里的permissionRuleListMap
,居然有十幾萬的數據。

叄 ❀ 優化思路
怎么優化呢?其實文章標題已經給了答案了,就是一個小小的concat
引發的問題,這段代碼第一獲取了名為permissionRuleListMap
的所有key,然后遍歷key,依次去取key對應的內容,再利用concat
將內容加入到新數組resultList
取,事實上這里都不需要做Object.keys
這部操作,畢竟Object.keys
也是一次遍歷,十幾萬的數據跑一遍也需要時間,我們完完全全可以一遍遍歷搞定,改為如下代碼:
const resultList = [];
// 一次遍歷,不用單獨獲取key
for (const key in permissionRuleListMap) {
// 用push取代concat
resultList.push(...permissionRuleListMap[key]);
}
return resultList;
重走上述流程,然后繼續點回退,神奇的事情發生啦,看一眼控制台時間輸出,現在只要幾百毫秒了。

肆 ❀ 謹慎使用concat
為了更好的驗證這兩個方法的快慢,我們可以控制變量,聲明一個包含10W個[1,2]
的數組,並將其復制到一個新數組中去,讓我們來對比時間:
const arr = new Array(100000).fill([1, 2]);
let a1 = [];
let a2 = [];
// 使用concat
console.time('1');
for (let i = 0; i < arr.length; i++) {
a1 = a1.concat(arr[i]);
};
console.timeEnd('1');
// 使用push
console.time('2');
for (let i = 0; i < arr.length; i++) {
a2.push(...arr[i]);
}
console.timeEnd('2');

我們都知道,concat
是返回一個新數組,所以每次遍歷,JS都需要新開一個內存,創建一個新數組,用於保存合並后的數組內容,所以遍歷10W次,那么就需要重復開10W個內存,再將數組元素依次放進去,只要數組夠大,它的時間只會要的更久。
相對而言push
就簡單了,即便執行10W次,我們操作的始終是一個數組,每次我們都只是在這個數組上新加幾個空位,用於依次存放新的數組元素而已,腦補一下這個過程,性能誰更優一目了然。
在Javascript Array.push is 945x faster than Array.concat 🤯🤔一文中,也有闡述為啥push
比concat
快了接近945倍,這里我們只需要得知這個結論就好,總而言之,請謹慎使用concat
方法,如果你的數據量較大,就一定得留意這一點,那么本文就記錄到這里啦。