7分钟教你用时间切片让页面看起来更流畅

一、前置知识

首先,在开始介绍时间切片前,很有必要先把浏览器的渲染流程梳理一下,这里面涉及的知识有event loop渲染帧等知识,下面会简单地介绍。

event loop

事件循环,这个严格来说其实并不是js语言本身的特性,而是在浏览器这个宿主环境下提供的机制,(因为在node环境下又是另一种事件循环机制了),浏览器虽然是多进程,但是本身每个Tag页就是一个子进程,而每个子进程的js都是以单线程执行的,按道理来说就是代码从上往下执行,中间如果有东西卡住了,那么整个js的执行就会阻塞掉。那么诸如setTimeoutsetInterval之类的异步又是怎么回事呢?其实它们并不在js的线程上,而是浏览器交给额外的线程去执行的,执行完触发条件后才会推进任务队列中,供js引擎拿来运行.

如上所示,当js开始运行时,把任务压入执行栈,然后一个个按顺序执行,当遇到异步任务时,其实会先注册到event table中,(可以理解为一个用来记录异步任务的队列),然后继续执行下一个同步任务。与此同时(因为异步任务是独立于js线程外的线程执行的,不存在阻塞关系),记录在event table里的异步任务会先执行它的前置条件,比如setTimeout设置了多少秒,那么就会开始计时多少秒,达成条件后才会推进去任务队列中排队。当整个js执行栈执行完并清空后,就会去遍历任务队列,拿出里面的宏任务推入到js执行栈中重复上面的步骤。

渲染帧

页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时(每秒60帧画面),页面是流畅的,小于这个值时,用户会感觉到卡顿,所以每一帧分到的时间是 1000/60 ≈ 16 ms,那么一帧里浏览器到底干了哪些事呢?请看下图:

这里解释一下每个阶段的含义:

  • events: 点击事件、键盘事件、滚动事件等
  • macro: 宏任务,如 setTimeout
  • micro: 微任务,如 Promise
  • rAF: requestAnimationFrame, 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
  • Layout: CSS 计算,页面布局
  • Paint: 页面绘制
  • rIC: requestIdleCallback,将在浏览器的空闲时段内调用的函数排队调用。

也就是说,event loop也只是一帧里的一小部分,浏览器除了执行js的任务,还会经历rAFlayoutpaint等阶段,当js的执行时间过长,加上绘制等阶段超过了16ms,那么用户就会感觉到明显卡顿。

二、时间切片核心

在理解了event loop 和浏览器每一帧发生的事情之后,我们基本可以知道,页面交互卡顿大多数是因为js线程执行过久,也就是出现了长任务。

而时间切片的核心原理,就是把一段原本要执行100ms的任务,分成10个10ms的任务,分散到每一帧中执行,留足时间给layoutpaint阶段,这样就可以保证肉眼可见的流畅了。需要注意的是,把一个长任务分成多个小任务执行的前提,必须结合event loop的机制来操作,很明显的是,如果多个小的同步任务依然在同一次事件循环中执行,那么依然会阻塞页面的渲染,因此必须合理地把它们一个个安排到下一轮事件循环中。

下面先给出对比效果:

假设不使用时间切片,同步地渲染10万条数据。

可以看到,直接暴力渲染10万条以上数据时,会卡顿2、3秒,并且直到渲染完成之前,页面上所有元素包括input框都无法交互。

而使用时间切片后,流畅度会有显著提升:

加载的速度明显提升,并且交互没有阻塞

需要说明的是,在使用了时间切片的例子中,其实列表并没有加载完的,而是动态地一帧加载一点这样子,留足了时间给浏览器渲染,所以才会看起来前1000条渲染完,但是滚动一下页面会发现,其实后面依然在加载,只是没有阻塞而已。

三、如何使用时间切片

看了以上的对比之后,相信各位都知道时间切片的优势了。但是具体怎么用呢?其实上面已经把思路说得非常直白了,就是把本来需要执行很长时间的js逻辑,分割成一段段小的逻辑,然后一个接一个地推到下一帧里。

手动切片

//假设有个任务要插入10W条数据,大概耗时10s
var listDom = document.getElementById("list");

function bigInsert(){
    let i = 0;
    while(i<100000){
        let item = document.createElement("li");
        item.innerText = `第${i++}条数据`;
        listDom.appendChild(item)
    }
}

// 如果使用时间切片,那么应该这样分割
function bigInsert(){
       let i = 0;
        return setTimeout(()=>{
            while(i<25000){
                let item = document.createElement("li");
                item.innerText = `第${i++}条数据`;
                listDom.appendChild(item)
            };
            return setTimeout(()=>{
                while(i<50000){
                    let item = document.createElement("li");
                    item.innerText = `第${i++}条数据`;
                    listDom.appendChild(item)
                };
                return setTimeout(()=>{
                    while(i<75000){
                        let item = document.createElement("li");
                        item.innerText = `第${i++}条数据`;
                        listDom.appendChild(item)
                    };
                    return setTimeout(()=>{
                        while(i<100000){
                            let item = document.createElement("li");
                            item.innerText = `第${i++}条数据`;
                            listDom.appendChild(item)
                        };
                    },16)
                },16)
            },16)
    },16)
}

可以看到,基本就是把任务尽可能分割,然后插入到每一帧中执行(这里使用了setTimeout只是为了体现推入下一轮渲染的行为而已,千万不要以为把不同的函数放到setTimeout就等于分散到每一帧中了,实际上浏览器会智能地合并一些宏任务,并不一定每一个宏任务都伴随着重绘,详情可以参考晨曦大佬的深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示),实际上我更推荐用requestIdleCallback

自动切片

可以发现,如果切片的粒度不大,那么手动自己改造函数其实也能接受,但是如果需要切割成粒度非常小的逻辑,那么使用generator函数特性,会更加方便。(不熟悉generator的,请细看es6之generator

//首先我们封装一个时间切片执行器
function timeSlice(gen) {
    if (typeof gen !== "function")
        throw new Error("TypeError: the param expect a generator function");
    var g = gen();
    if (!g || typeof g.next !== "function")
        return;
    return function next() {
        var start = performance.now();
        var res = null;
        do {
            res = g.next();
        } while (res.done !== true && performance.now() - start < 25);
        if (res.done)
            return;
        window.requestIdleCallback(next);
    };
}

//然后把长任务变成generator函数,交由时间切片执行器来控制执行
const add = function(i){
            let item = document.createElement("li");
                item.innerText = `第${i++}条`;
                listDom.appendChild(item);
        }
function* gen(){
    let i=0;
    while(i<100000){
        yield add(i);
        i++
    }
}
//使用时间切片来插入10W条数据
function bigInsert(){
    timeSlice(gen)()
}

利用generator的特性。把每一次yield都放在requestIdleCallback里执行,直到全部执行完毕,就可以轻松达到时间切片的效果了。

四、总结

时间切片不是什么高级的api,而是一种根据浏览器渲染特性衍生出的优化方案,是一种优化思想,把计算量过大,容易阻塞渲染的逻辑切割成一个个小的任务来执行,留给浏览器渲染的时间来达到肉眼可见的流畅,本质上并没有优化什么js的计算性能,有些算法的逻辑该优化还是需要从算法的思想上去优化。

以上如果有任何表述不对,烦请各位大佬一一指出。

原文链接:juejin.im

上一篇:[GitHub] JavaScript 趋势榜项目(第40周)
下一篇:基于HTML + CSS + 原生JavaScript制作中国象棋(更新中)

相关推荐

  • 🌓vue页面换肤实践

    前言 最近要做一个换肤的功能,不过只是对一个页面换肤,换一下背景图呀,背景、边框、字体颜色呀之类的,并非整个项目换肤,相对比较简单,所以以下介绍的换肤方法仅适用于页面换肤而非整个项目换肤。

    3 个月前
  • (详解)从浏览器输入 URL 到页面展示过程

    引言 对于面试常问的从浏览器输入 URL 到页面渲染过程发生了什么?,我想大家都或多或少能说出一二。但是,其实这个问题很有深度,而你是否回答的有深度,在很大程度上会影响到面试官对你的印象。

    8 个月前
  • 高德地图+vue实现页面点击绘制多边形及多边形切割拆分

    最终效果 技术栈 项目中使用到的技术 高德基于vue的vue-amap,组件使用的element,算法是用的turf.js 配置步骤划分 1.创建vue文件,项目中引入vue-amap 官方有...

    6 个月前
  • 项目中的导航栏搜索及搜索页面的思路

    项目背景 今天项目里的实习生在做搜索功能时,导航栏的搜索框与搜索页面的搜索框,在内容上竟然是各自独立的。。。。。 无论他怎么鼓捣,在搜索栏上进行搜索,搜索页面都没有任何反应。

    7 个月前
  • 页面间通信--使用storage事件保持多tab页共享轮询请求的数据

    业务场景 最近接到一个优化需求,某页面会轮询请求两个接口获取未读消息的数量,但是当用户同时在多个tab打开了页面的话,这些页面都会进行轮询请求,有用户打开tab页太多,1分钟请求了几千次,触发了风控,...

    6 个月前
  • 页面进入后台如何关闭背景音乐

    之前在做小程序的时候遇到一个问题:小程序中有一个webview页面,这个页面中用iframe嵌套了一个H5页面,这个H5页面中有背景音乐,背景音乐播放的情况下,点击小程序右上角退出的小圆圈背景音乐仍然...

    1 年前
  • 页面跳转的归纳

    前端工作有很多地方需要页面跳转,有很多种实现方法,这里做一下归纳。 HTML标签 一般形式上可以用&lt;a&gt;&lt;/a&gt;、&lt;button&gt;&lt;/button&gt;、&...

    3 年前
  • 页面跳转时,点击上报丢失问题解析

    背景 最近在工作中,遇到了页面跳转时点击上报丢失的问题,突出表现在微信ios的webview上,上报后直接跳转的失败率达到了惊人的93%。喝口水压压惊,开始逐步分析问题的发生。

    3 年前
  • 页面跳转中的安全问题

    最近参与了一小部分安全排查,涉及到一些前端编码安全问题,记个小tip。 1.Http请求头中的Referer 上图是一个http请求的header,其中referer字段就是当前的页面地址,那么这个...

    1 年前
  • 页面负载启动自举模型

    zanzuBrandon提出了一个问题:Launch Bootstrap Modal on page load,或许与您遇到的问题类似。 回答者Julian SchmuckliAndres Ilich...

    3 年前

官方社区

扫码加入 JavaScript 社区