Технология разделения времени (для устранения замятия страницы, вызванного длинными задачами js)

JavaScript оптимизация производительности
Технология разделения времени (для устранения замятия страницы, вызванного длинными задачами js)

разрезание времени

Он направлен на то, чтобы разбить длительную задачу на более мелкие задачи и выполнять их блоками, потому что задачи, превышающие 50 мс, будут считаться длительными задачами, и пользователи могут воспринимать рендеринг и взаимодействие, поэтому мы можем сократить время непрерывного выполнения функции.

причина

Коллега столкнулся с проблемой с отображением анимации, то есть внизу нужно выполнить функцию с большим объемом вычислений.Хочет загрузить загрузку, но обнаружил, что элемент загрузки display:block; анимация загрузки не появится на странице сразу, и анимация появится.Время после выполнения функции операции.

Решение

Есть два способа справиться с этой трудоёмкой задачей.Первый - это webWorker, но некоторые DOM-операции нельзя сделать, поэтому я подумал решить её через функции-генераторы.Давайте вкратце разберёмся с циклом обработки событий.

цикл событий

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)

Эффект

Как показано на рисунке ниже, когда анимация выполняется в течение 1 с, функция js начинает выполняться, анимация сначала останавливает рендеринг, а затем анимация продолжается после того, как основной стек выполнения js простаивает.

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

image.png

преобразование функции

Преобразуем исходную функцию в функцию генератора

код

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

Эффект

На анимацию это никак не повлияло, а частота кадров была стабильной, потому что мы разделили трудоемкие задачи на части для выполнения.

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

наконец

Положение доходности в функции генератора очень критично и должно быть помещено в место, требующее много времени.Оптимизированная функция разделения времени также предоставляет переменную времени, и вы можете изменить свое значение времени в соответствии с реальной ситуацией.