浏览器一个渲染帧执行卡顿问题详解
渲染帧
我们通常看的视频其实都是一张张图片播放的,图片播放的频率越快(帧数越高)就会看起来越流畅。浏览器的渲染其实也是一样的。现代浏览器通常以 60HZ刷新率(即每秒60帧,每帧约16.67ms)为目标进行渲染。也就是每隔 16.67ms 渲染一张图片,那如果浏览器发生卡顿(主线程被 long task 长时间占据)就会导致浏览器没法每隔 16.67ms 渲染一张图片。就会导致视觉上看起来是卡卡的。如下所示
从上面能看出刚开始还是比较流程,后面点击了 longtask 之后,开始卡顿。整个的代码如下
1 | const box = document.getElementById('box'); |
从上面的代码可以看出,点击 longtask 之后,进行了 5 次 200ms 的死循环,导致主线程被占用 200ms。
划重点 那么为什么主线程占用会导致动画卡顿呢
首先我们先要知道一个渲染帧里面浏览器做了什么事情?
- execute js(执行js)
- calculate style(样式计算)
- Layout(布局)
- Paint(绘制)
- Layerize(分层)
- Commit(提交绘制)
我们众所周知的 requestAnimationFrame的回调函数就发生在 1 和 2 的中间。
在 longtask 介入之前我们看一下Chrome Performance 的表现
从上图可以看出在 16.6ms 的渲染帧里面在前面极短的时间就完成了一个渲染帧第 1 步到 6 步的所有事情。因为此时第一步并没有js代码执行,主线程是空闲的。所以此时动画就会特别流畅。
倘若在一个 16.6ms 的渲染帧里面,执行 js 需要花费的时间就不止 16.6ms怎么办?
那么很遗憾当前这个渲染帧里面的绘制就会被错过,也就是通常说的掉帧,看起来动画就不连贯。
当 longtask 介入之后,主线程开启了一个时间长达 200ms 的长任务,导致了 200ms 内 requestAnimateFrame 得不到执行,所以动画就会一直卡在那里。
所以想要保证高质量且流畅的用户体验,主线程不要执行长任务,可以使用 requestIdleCallback 在当前渲染帧,执行完成第6步之后,如果还有剩余时间则该方法注入的回调函数将被执行。我们比较耗时的任务可以在里面拆分成子任务执行。
顺便提一下,React 18 中的可中断渲染就是为了解决React对比和渲染组件树时间过长(JS 在一个渲染帧里面执行太久)从而导致页面无响应或卡顿的问题,后续文章再详细展开讲一下。