利用 JavaScript Profiler 分析 Vue 性能问题

孙熠彤
2023-12-01

最近在开发一套组件库,期间在实现InputNumber 组件时候碰到一个诡异卡顿的现象,用了时间来排除这个问题,涉及到一些问题定位的方法,记录下来已备后用。


1. 发现问题

在实现 InputNumber 组件的时候,有一个功能是按住 + 或 - 按钮时,组件的值在不断的自增或者自减,具体如下图

当组件的值自增到一定数量之后,组件会开始卡顿,并且页面上下滚动也会有明显的延迟。

问题体验

相关代码

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 的时间会不断地增长。

接下来要判断具体是哪一句js导致整个页面的 update 时间不断地变长,利用 Chrome 的 JavaScript Profiler 来完成该工作。打开开发者工具

利用这个面板你可以追踪网页程序的内存泄漏问题,进一步提升程序的JavaScript执行性能,点击Start 按钮,然后去复现刚才的操作,得到结果如下

图中标识处有三个模式:

  • Chart 按时间先后顺序显示的火焰图;
  • Heavy(Bottom Up) 根据对性能的消耗影响列出所有的函数,并可以查看该函数的调用路径;
  • Tree(Top Down) 从调用栈的顶端(最初调用的位置)开始,显示调用结构的总体的树状图情况。

选择 Tree(Top Down) 模式,得到结果如下

可以看出 flushCallbacksvue 函数占用了74.66%的 Total Time,所以需要对它进行分析

在它的调用栈中,关键的一步是 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);
        });
    }
});
复制代码

原创声明: 该文章为原创文章,转载请注明出处。

 类似资料: