手记

优化DOM渲染——documentFragment,requestAnimationFrame使用

documentFragment

接口表示的是没有父节点的最小的文档对象.。它被当做一个轻量版本的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。

requestAnimationFrame

方法告诉浏览器您希望执行动画,并请求浏览器调用指定的函数在下一次重绘之前更新动画。该方法将在重绘之前调用的回调作为参数。

场景
页面上有个空的无序列表节点,用 <ul></ul> 表示,要往列表中插入 30000<li>,每个列表项的文本内容是列表项的插入顺序,取值 1, 2, 3, ... ,30000,怎么用原生的 JS 实现这个需求?

解题思路:
出现卡顿感的主要原因是每次循环都会修改 DOM 结构,外加大循环执行时间过长,浏览器的渲染帧率(FPS)过低。而实际上,包含 30000 个 <li> 的长列表,用户不会立即看到全部,大部分甚至根本都不会看,那部分都没有渲染的必要,好在现代浏览器提供了 requestAnimationFrame API 来解决非常耗时的代码段对渲染的阻塞问题,不知道 requestAnimationFrame 用法和原理的请研究下这篇文章,该技术在 React 和 Angular 里面都有使用,如果你理解了 requestAnimationFrame 的原理,就很容易理解最新的 React Fiber 算法。

综合上面的分析,可以从减少 DOM 操作次数、缩短循环时间两个方面减少主线程阻塞的时间。减少 DOM 操作次数的良方是 DocumentFragment;而缩短循环时间则需要考虑使用分治的思想把 30000 个 <li> 分批次插入到页面中,每次插入的时机是在页面重新渲染之前。由于 requestAnimationFrame 并不是所有的浏览器都支持,Paul Irish 给出了对应的 polyfill,这个 Gist 也非常值得你学习。

下面是完整的代码示例:

(() => {
    const ndContainer = document.getElementById('js-list');
    if (!ndContainer) {
        return;
    }

    const total = 30000;
    const batchSize = 4; // 每批插入的节点次数,越大越卡
    const batchCount = total / batchSize; // 需要批量处理多少次
    let batchDone = 0;  // 已经完成的批处理个数

    function appendItems() {
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < batchSize; i++) {
            const ndItem = document.createElement('li');
            ndItem.innerText = (batchDone * batchSize) + i + 1;
            fragment.appendChild(ndItem);
        }

        // 每次批处理只修改 1 次 DOM
        ndContainer.appendChild(fragment);

        batchDone += 1;
        doBatchAppend();
    }

    function doBatchAppend() {
        if (batchDone < batchCount) {
            window.requestAnimationFrame(appendItems);
        }
    }

    // kickoff
    doBatchAppend();

    ndContainer.addEventListener('click', function (e) {
        const target = e.target;
        if (target.tagName === 'LI') {
            alert(target.innerHTML);
        }
    });
})();
0人推荐
随时随地看视频
慕课网APP