時間分片技術(解決 js 長任務導致的頁面卡頓)


時間分片

旨在把一個運行時間比較長的任務分解成一塊一塊比較小的任務,分塊去執行,因為超過 50ms 的任務就會被認為是 long task,用戶就能感知到渲染卡頓和交互的卡頓,所以我們可以縮短函數的連續執行時間。

起因

同事遇到一個動畫展示的問題,就是下面要執行一個運算量很大的函數,他要加載一個 loading,但他發現把 loading 的元素 display: block; 頁面中也不會立刻出現 loading 動畫,出現動畫的時候是運算函數執行完畢之后。

處理辦法

有兩種方法去處理這種耗時任務,第一種就是 webWorker,但是一些 dom 的操作做不了,於是就想到了通過 generator 函數來解決,下面先簡單了解下事件循環。

事件循環

image.png

微任務:

1. Promise.then
2. Object.observe
3. MutaionObserver

宏任務:

1. script(整體代碼)
2. setTimeout
3. setInterval
4. I/O
5. postMessage
6. MessageChannel

瀏覽器渲染時機

除去特殊情況,頁面的渲染會在微任務隊列清空后,宏任務執行前,所以我們可以讓推入主執行棧的函數執行到一定時間就去休眠,然后在渲染之后的宏任務里面叫醒他,這樣渲染或者用戶交互都不會卡頓了!

原始代碼

我們先模擬一個 js 長任務

代碼

// 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)

效果

如下圖,動畫運行 1s 的時候,js 函數開始運行,動畫會先停止渲染,然后等 js 主執行棧空閑之后動畫才繼續進行。

GIF 2021-9-16 11-43-55.gif

image.png

函數改造

我們把原來的函數改造為 generator 函數

代碼

// generator 處理原來的函數
function * fnc_ () {
    let i = 0
    const start = performance.now()
    while (performance.now() - start <= 5000) {
        yield i++
    }

    return i
}

// 簡易時間分片
function timeSlice (fnc) {
    if(fnc.constructor.name !== 'GeneratorFunction') return fnc()

    return async function (...args) {
        const fnc_ = fnc(...args)
        let data

        do {
            data = fnc_.next()
            // 每執行一步就休眠,注冊一個宏任務 setTimeout 來叫醒他
            await new Promise( resolve => setTimeout(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 一直很穩定,因為我們把耗時任務拆成很多個塊來執行。

GIF 2021-9-16 13-21-25.gif

image.png

優化時間分片

上面的時間分片函數每執行一步,就會休眠,然后通過一個宏任務來喚醒他,但是這樣的執行效率肯定是比較低的,我們再優化一下執行的效率,提升連續執行時間。

代碼

// 精准時間分片
function timeSlice_ (fnc, time = 25) {
    if(fnc.constructor.name !== 'GeneratorFunction') return fnc()

    return function (...args) {
        const fnc_ = fnc(...args)

        function go () {
            const start = performance.now()
            let data

            do {
                data = fnc_.next()
            } while (!data.done && performance.now() - start < time)

            if (data.done) return data.value

            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    try {
                        resolve(go())
                    } catch(e) {
                        reject(e)
                    }
                })
            })
        }

        return go()
    }
}

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 會稍微收到影響,但是在接受范圍內。

image.png

對比優化前后

我們對比一下優化時間分片函數前后的效果

代碼

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 倍,我們做的只是提升了函數連續執行時間。

image.png

最后

generator 函數中 yield 的位置非常關鍵,需要放到耗時的地方,優化后的時間分片函數也提供了 time 變量,你可以根據實際情況來改變你的 time 值。

本文轉自 https://juejin.cn/post/7008416027700789255?share_token=cfd8d999-80f7-4df0-b95b-d8394705e378,如有侵權,請聯系刪除。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM