利用 JavaScript Profiler 分析 Vue 效能問題

FatGe發表於2019-01-04

最近在開發一套元件庫,期間在實現InputNumber 元件時候碰到一個詭異卡頓的現象,用了時間來排除這個問題,涉及到一些問題定位的方法,記錄下來已備後用。


1. 發現問題

在實現 InputNumber 元件的時候,有一個功能是按住 + 或 - 按鈕時,元件的值在不斷的自增或者自減,具體如下圖

利用 JavaScript Profiler 分析 Vue 效能問題

當元件的值自增到一定數量之後,元件會開始卡頓,並且頁面上下滾動也會有明顯的延遲。

問題體驗

相關程式碼

2. 定位問題

<script>
export default {
  name: "input-number",
  ...
  methods: {
    handleClick(type) {
        const { step } = this;
        const period = 10;
        const timerHandle = () => {
            const { addDisabled, decDisabled } = this;
            if (!addDisabled && type === "add") this.inputNumberValue += step;
            if (!decDisabled && type === "dec") this.inputNumberValue -= step;
        };
        const timer = setInterval(timerHandle, period);
        const startTime = new Date();

        const handler = () => {
            const endTime = new Date();
            if (endTime - startTime < period) timerHandle();

            clearInterval(timer);
            document.removeEventListener("mouseup", handler, false);
        };
        document.addEventListener("mouseup", handler, false);
    }
    ...
};
</script>
複製程式碼

首先定位問題發生的位置,直觀上感受應該是點選之後不無端自增發生的卡頓,對應程式碼中的 handleClick 函式,它將 click 事件分為 mousedown 以及 mouseup,當觸發 mousedown 事件時候,呼叫一個 setInterval 定時執行元件值變化的函式。

初步定位問題應該就發生在 timerHandle 之後,當 inputNumberValue 發生變化之後,它會按照一定的規則來改變 inputValue 的值,從而觸發 $emit(input, this.inputValue) 來完成 v-model

computed: {
    inputNumberValue: {
        get() {
            return this.inputValue;
        },
        set(value) {
            // ...一定規則
            this.inputValue = limits.find(limit => limit.need(value)).value;
        }
    }
},
watch: {
    value: {
        handler(newVal) {
            console.timeEnd()
            this.inputNumberValue = newVal;
        },
        immediate: true
    },
    inputValue(newVal) { 
        this.$emit("input", newVal);
    }
}
複製程式碼

利用 console.time 以及 console.timeEnd 來排查,那一步發生的卡頓,檢測整個 v-model 變化的流程。

也就是在 timerHandle 以及 watch value handler 內新增 console.time 以及 console.timeEnd ,具體如下

const timerHandle = () => {
    const { addDisabled, decDisabled } = this;
    if (!addDisabled && type === "add") this.inputNumberValue += step;
    if (!decDisabled && type === "dec") this.inputNumberValue -= step;
    console.time();
};

watch: {
    value: {
        handler(newVal) {
            console.timeEnd()
            this.inputNumberValue = newVal;
        },
        immediate: true
    }
}
複製程式碼

然後執行,發現執行時間是在不斷地增加的,這時候問題的可以歸類為,inputNumber 元件的值在不斷地變動,導致的 update 的時間會不斷地增長。

利用 JavaScript Profiler 分析 Vue 效能問題

接下來要判斷具體是哪一句js導致整個頁面的 update 時間不斷地變長,利用 Chrome 的 JavaScript Profiler 來完成該工作。開啟開發者工具

利用 JavaScript Profiler 分析 Vue 效能問題

利用這個皮膚你可以追蹤網頁程式的記憶體洩漏問題,進一步提升程式的JavaScript執行效能,點選Start 按鈕,然後去復現剛才的操作,得到結果如下

利用 JavaScript Profiler 分析 Vue 效能問題

圖中標識處有三個模式:

  • Chart 按時間先後順序顯示的火焰圖;
  • Heavy(Bottom Up) 根據對效能的消耗影響列出所有的函式,並可以檢視該函式的呼叫路徑;
  • Tree(Top Down) 從呼叫棧的頂端(最初呼叫的位置)開始,顯示呼叫結構的總體的樹狀圖情況。

選擇 Tree(Top Down) 模式,得到結果如下

利用 JavaScript Profiler 分析 Vue 效能問題

可以看出 flushCallbacksvue 函式佔用了74.66%的 Total Time,所以需要對它進行分析

利用 JavaScript Profiler 分析 Vue 效能問題

在它的呼叫棧中,關鍵的一步是 Vue._update ,它的主要功能是將 Vnode 渲染成真實DOM,所以上述的卡頓問題果然出現在渲染這一步。

繼續分析,發現主要問題在與 updateDirctives 這個函式內,看來問題和指令的更新相關。

最後,發現原來是 highlightBlock 的鍋,因為要完成頁面中程式碼高亮的需求,開發了一個指令

import hljs from 'highlight.js/lib/highlight';

Vue.directive ('highlight', function (el) {
    let blocks = el.querySelectorAll ('code');
    Array.prototype.forEach.call (blocks, block => {
        hljs.highlightBlock (block);
    });
});
複製程式碼

當 InputNumber 元件 v-model 所繫結的父元件 data 變動時候,會導致 v-highlight 指令不斷地更新,使得頁面卡頓。

3. 解決問題

只需要將該指令的高亮程式碼的函式寫在 bind 裡面,這樣就只呼叫一次,指令第一次繫結到元素時呼叫。

Vue.directive ('highlight', {
    bind (el) {
        let blocks = el.querySelectorAll ('code');
        Array.prototype.forEach.call (blocks, block => {
            hljs.highlightBlock (block);
        });
    }
});
複製程式碼

原創宣告: 該文章為原創文章,轉載請註明出處。

相關文章