generator在前端优化场景中的应用

148 阅读3分钟

前言

generator函数对于大多数前端来说可能熟悉又陌生。作为实现async/await的底层方法,在实际开发过程中,可能完全被async/await方法替代了。本文便记录一种generator函数在实际优化场景中的使用案例。

代码分析

测试场景搭建

// html(部分代码)
<div class="container">
    <div>
        <span>总循环次数</span>
        <input type="number" id="loopCount" value="10000000" step="100000" />
    </div>
    <div class="topBox">
        <div class="part">
            <button onclick="directExecute('d1', 'simple')">一帧搞定</button>
        </div>
    </div>
    <div class="resultBox">
        <div id="d1"></div>
    </div>
    <div class="aniBox">
        <div class="ani-obj"></div>
    </div>
</div>

// css
@keyframes slidein {
    from {
        margin-left: 0;
    }

    to {
        margin-left: 90%;
    }
}
.aniBox {
    width: 100%;
    background-color: rgb(169, 169, 127);
    position: relative;
}
.ani-obj {
    width: 60px;
    height: 60px;
    position: relative;
    background-color: coral;

    animation-duration: 1s;
    animation-name: slidein;
    animation-iteration-count: infinite;
    animation-direction: alternate;
}

// js
const getLoopCount = function () {
    const el = document.querySelector("#loopCount");
    let loopCount = 10000000;
    if (!isNaN(Number(el.value)) && Number(el.value) > 0) {
        loopCount = Number(el.value);
    }
    return loopCount;
}
const resetDom = function (id) {
    const element = document.getElementById(id);
    element.innerText = "";
    element.style.backgroundColor = "rgb(224, 224, 224)";
}
// 简单的计算
const simple = function (i) {
    return i * 2 + Math.abs(i - 600) - Math.cos(i) + Math.sin(i);
}
// 复杂一些的计算
const complex = function (i) {
    return i * 2 + Math.abs(i - 600) - (Math.cos(i) + Math.sin(i)) * (1 + Math.sqrt(i)) + Math.sin(i + 1) + Math.cos(i + 3); 
}
// 计算函数获取对象
const taskObj = {
    simple,
    complex
}
// 定义一个循环函数,计算结果并展示,计算过程在一帧之内完成
const directExecute = function (textID, type) {
    console.time('directExecute');
    resetDom(textID); // 此项应当不成功,因为进程卡住了,test2和test3的可以
    const loopCount = getLoopCount();
    const directExecute = function () {
        let result = 0;
        for (let i = 0; i < loopCount; i++) {
            result += taskObj[type](i);
        }
        const element = document.getElementById(textID);
        element.innerText = result;
        element.style.backgroundColor = "cornflowerblue";
        console.timeEnd('directExecute');
    };
    directExecute();
};

展示效果如下:

20.gif

其实就是同步的大量计算,可以在gif中看到,每一次点击都会使得页面卡顿,这也是常见的前端性能问题。

初步优化

增加下列逻辑

// html
<div class="part">
    <input type="number" id="t2" value="500000" step="100000" />
    <button onclick="firstGenerator('t2', 'd2', 'simple')">分帧</button>
</div>

// js
const firstGenerator = function (inputID, textID, type) {
    console.time('firstGenerator');
    resetDom(textID);
    const loopCount = getLoopCount();
    const frameExecute = function* (maxCount) {
        // 执行计数
        let count = 0;
        let result = 0;
        for (let i = 0; i < loopCount; i++) {
            result += taskObj[type](i);

            // 以下为分帧相关逻辑
            count++; // 每次循环计数+1
            if (count >= maxCount) {
                count = 0; // 达到最大次数后,yield
                yield;
            }
        }
        const element = document.getElementById(textID);
        element.innerText = result;
        element.style.backgroundColor = "cornflowerblue";
        console.timeEnd('firstGenerator');
    };
    // 每frame执行多少次
    const el = document.querySelector(`#${inputID}`);
    let maxCountPerFrame = 3;
    if (!isNaN(Number(el.value)) && Number(el.value) > 0) {
        maxCountPerFrame = Number(el.value);
    }
    const gen = frameExecute(maxCountPerFrame);
    const e = function () {
        const iter = gen.next();
        if (iter.done) {
            console.log('结束!!!');
            return;
        }
        requestAnimationFrame(e);
    };
    requestAnimationFrame(e);
};

运行结果如下:

21.gif

分析: 使用generator结合requestAnimationFrame方法,将大量的运算逻辑分在各个帧的空闲时段处理。如图所示,在使用分帧逻辑进行运算时,下面的动画完全没有卡顿。

问题:

  1. 有人可能会说,这种大量计算的逻辑,用worker也可以实现,可能还更为标准便利。
  2. 这只是纯数据计算的场景,那么在那些浏览器的功能场景下能否使用这种方式呢?

在浏览器功能场景下的案例

下面在代码中添加功能交互: element.innerText = result,动态展示计算数据

const secondGenerator = function (inputID, textID, type) {
    resetDom(textID);
    const loopCount = getLoopCount();
    const frameExecute = function* (maxCount) {
        // 执行计数
        let count = 0;
        let result = 0;
        const element = document.getElementById(textID);
        for (let i = 0; i < loopCount; i++) {
            count++; // 每次循环计数+1
            result += taskObj[type](i);
            element.innerText = result; // 非常卡!!!
            if (count >= maxCount) {
                count = 0; // 达到最大次数后,yield
                yield;
            }
        }
        element.style.backgroundColor = "cornflowerblue";
        console.timeEnd('secondGenerator');
    };
    // 每frame执行多少次
    const el = document.querySelector(`#${inputID}`);
    let maxCountPerFrame = 3;
    if (!isNaN(Number(el.value)) && Number(el.value) > 0) {
        maxCountPerFrame = Number(el.value);
    }
    const gen = frameExecute(maxCountPerFrame);
    const e = function () {
        const iter = gen.next();
        if (iter.done) {
            console.log('结束!!!');
            return;
        }
        requestAnimationFrame(e);
    };
    requestAnimationFrame(e);
};

执行结果如下:

22.gif

可以看出,加入dom操作后,整个流程又变的非常卡顿。毕竟重绘的消耗和纯运算不是一个数量级。

但是如果把每帧计算次数调低,还是比较流畅的:

23.gif

但是强制将执行次数缩小到一定范围,又会大大增加完成整个逻辑流程的时间,有没有什么更合适的方式呢?

动态调整执行次数

增加动态调整执行次数的逻辑,根据当前帧率,如果保持在正常范围(16-17)或者更大则适当增加执行次数,如果小于正常情况(暂定16)则减小当前次数。

const thirdGenerator = function (inputID, textID, type) {
    resetDom(textID);
    const loopCount = getLoopCount();
    const frameExecute = function* (maxCount) {
        // 执行计数
        let count = 0;
        let result = 0;
        let currentMaxCount = maxCount
        const element = document.getElementById(textID);
        for (let i = 0; i < loopCount; i++) {
            result += taskObj[type](i);

            // 以下为分帧相关逻辑
            count++; // 每次循环计数+1
            element.innerText = result;
            if (count >= currentMaxCount) {
                count = 0; // 达到最大次数后,yield
                currentMaxCount = Math.round(currentMaxCount * (yield));
            }
        }
        element.style.backgroundColor = "cornflowerblue";
        console.timeEnd('thirdGenerator');
    };
    const createAdjustMaxCount = function() {
        const normalFPS = 60;
        const standard = 1000 / normalFPS;
        let frequentTimeList = [];
        const frequentNum = 5;
        let lastTime = performance.now();
        return function() {
            const currentTime = performance.now();
            frequentTimeList.push(currentTime - lastTime);
            lastTime = currentTime;
            if(frequentTimeList.length >= frequentNum) {
                const currentFrequentTime = frequentTimeList.reduce((sum, next) => sum + next) / frequentTimeList.length;
                frequentTimeList = [];
                if(currentFrequentTime > standard + 0.5) {
                    return 0.9
                } else {
                    return 1.1
                }
            }
            return 1;
        }
    }
    // 每frame执行多少次
    const el = document.querySelector(`#${inputID}`);
    let maxCountPerFrame = 3;
    if (!isNaN(Number(el.value)) && Number(el.value) > 0) {
        maxCountPerFrame = Number(el.value);
    }
    const gen = frameExecute(maxCountPerFrame);
    const adjustMaxCount = createAdjustMaxCount();
    const e = function () {
        const iter = gen.next(adjustMaxCount());
        if (iter.done) {
            console.log('结束!!!');
            return;
        }
        requestAnimationFrame(e);
    };
    requestAnimationFrame(e);
};

结果如下:

24.gif 可以看到,在初始以500次/帧的起始情况下,跨度到达8000-10000的处理频率,既不会导致卡顿,时间也有所保证,还能兼容用户在操作情况下的性能变化。至此整个应用场景完成。