本文探讨如何利用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时间分片长任务分解的资料请关注本站其它相关文章!