JS时间分片技术解决长任务导致的页面卡顿

作者:异步漫步

时间:2026-10-26

浏览:638

本文探讨如何利用JavaScript的时间分片技术解决长任务导致的页面卡顿问题。通过解析事件循环机制,结合Generator函数将耗时操作拆分为多个小块,并在渲染间隙执行,有效保持动画流畅度。文章提供了从基础到优化后的代码实现,对比展示了执行效率的提升,帮助开发者在保持UI响应的同时处理复杂计算。

文章目录

起因

同事在处理一个动画展示需求时遇到了问题:需要执行一个计算量巨大的函数,并为此添加loading提示。然而,即使将loading元素的display属性设置为block,动画也不会立即出现,而是等到耗时函数执行完毕后才显示。

处理办法

针对此类耗时任务,通常有两种解决方案:一是使用Web Worker,但由于其无法直接操作DOM,因此被排除;二是利用Generator函数进行处理,为此我们需要先了解JavaScript的事件循环机制。

事件循环

起因

微任务主要包括:

1. Promise.then

2. Object.observe

3. MutaionObserver

宏任务包括:

1. script(整体代码)

2. setTimeout

3. setInterval

4. I/O

5. postMessage

6. MessageChannel

浏览器渲染时机

在一般情况下,页面渲染会在微任务队列清空后、宏任务执行前进行。因此,我们可以让推入主执行栈的函数在运行一段时间后暂停,并在渲染完成后的宏任务中唤醒它,从而避免渲染或用户交互时的卡顿。

原始代码

首先,我们模拟一个JavaScript长任务。

代码

// style
@keyframes move {
    from {
        left: 0;
    }
    to {
        left: 100%;
    }
}
.move {
    position: absolute;
    animation: move 5s linear infinite;
}
// dom
<div class="move">123123123</div>
// script
function fnc () {
    let i = 0
    const start = performance.now()
    while (performance.now() - start <= 5000) {
        i++
    }
    return i
}
setTimeout(() => {
    fnc()
}, 1000)

效果

如图所示,当动画运行至1秒时,JS函数开始执行。此时动画会停止渲染,直到JS主执行栈空闲后,动画才会继续播放。

起因

起因

函数改造

我们将原函数改造为Generator函数。

代码

// generator 处理原来的函数
function * fnc_ () {
    let i = 0
    const start = performance.now()
    while (performance.now() - start <= 5000) {
        yield i++
    }
    return i
}
// 简易时间分片
function timeSlice (fnc, cb = setTimeout) {
    if(fnc.constructor.name !== 'GeneratorFunction') return fnc()
    return async function (...args) {
        const fnc_ = fnc(...args)
        let data
        do {
            data = fnc_.next(await data?.value)
            // 每执行一步就休眠,注册一个宏任务 setTimeout 来叫醒他
            await new Promise( resolve => cb(resolve))
        } while (!data.done)
        return data.value
    }
}
setTimeout(async () => {
    const fnc = timeSlice(fnc_)
    const start = performance.now()
    console.log('开始')
    const num = await fnc()
    console.log('结束', `${(performance.now() - start)/ 1000}s`)
    console.log(num)
}, 1000)

效果

动画完全不受影响,FPS保持平稳,因为我们将耗时任务拆分成了多个小块执行。

起因

起因

优化时间分片

上述时间分片函数每执行一步就会休眠,并通过宏任务唤醒,这导致执行效率较低。为了提升连续执行时间并优化效率,我们进行了进一步改进。

代码

// 精准时间分片
function timeSlice_ (fnc, time = 25, cb = setTimeout) {
    if(fnc.constructor.name !== 'GeneratorFunction') return fnc()
    return function (...args) {
        const fnc_ = fnc(...args)
        let data
        return new Promise(async function go (resolve, reject) {
            try {
                const start = performance.now()
                do {
                    data = fnc_.next(await data?.value)
                } while (!data.done && performance.now() - start < time)
                if (data.done) return resolve(data.value)
                cb(() => go(resolve, reject))
            } catch(e) {
                reject(e)
            }
        })
    }
}
setTimeout(async () => {
    const fnc1 = timeSlice_(fnc_)
    let start = performance.now()
    console.log('开始')
    const num = await fnc1()
    console.log('结束', `${(performance.now() - start)/ 1000}s`)
    console.log(num)
}, 1000);

效果

我们将函数分成了较大的执行块,从而提高了执行效率。虽然FPS略有波动,但仍在可接受范围内。

起因

对比优化前后

我们对比了优化时间分片函数前后的效果。

代码

setTimeout(async () => {
    const fnc = timeSlice(fnc_)
    const fnc1 = timeSlice_(fnc_)
    let start = performance.now()
    console.log('开始')
    const a = await fnc()
    console.log('结束', `${(performance.now() - start)/ 1000}s`)
    console.log('开始')
    start = performance.now()
    const b = await fnc1()
    console.log('结束', `${(performance.now() - start)/ 1000}s`)
    console.log(a, b)
}, 1000);

效果

优化后的时间分片函数效率是之前的4452倍,这主要得益于我们提升了函数的连续执行时间。

起因

最后

Generator函数中yield的位置至关重要,应放置在耗时操作处。优化后的时间分片函数提供了time变量,可根据实际情况调整该值。

以上就是JS时间分片技术解决长任务导致的页面卡顿的详细内容,更多关于js时间分片长任务分解的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章