林秀栋的技术博客

浏览器帧原理

文章来源

在讲帧的概念前,我们得从背景开始看起,也就是渲染页面的这个过程,有哪些关键性的路径呢。

五大关键渲染路径

01

每一个过程具体是怎么样的呢,可以参考下图:

02

所以在这么一个像素的管道里,每部分都有可能造成卡顿,所以我们需要额外的关注这些,毕竟那一部分不当,都会开了不必要的性能开销。

三种输出方式

当时我的疑问是: 难道每一帧都总是会经过管道每个部分的处理嘛,其实不是这样子的,从视觉的角度来说,管道针对指定帧的运行通常有三种方式:

03

如果我们以第三种方式来更新视图,也就是更改一个既不要布局也不要绘制的属性,则浏览器将跳到只执行合成。

跑个demo

为了更加具体的验证上述的过程,可以动手跑一个demo,来验证一下。

04

我们添加多个dom元素进行动画,效果更佳明显,接着我们打开Performance,Record这个过程,我们需要关注的是Main选项卡,也就是主线程,我们在放大里面的Task,就有了下图:

05

经历的过程,也是很清楚看到,Update Layer Tree –» Layout –» Paint –» Composite Layers。

接着,我们按下,Optimize按钮,按照之前的流程走,Record后,发现不对劲,还是这样子步骤,难道是哪里存在问题嘛,好奇的我,打开了Sources面板,然后就发现了:

它的源码优化动画,使用的是rAF,了解过的人一定不会陌生,你可以简单的理解就是:按帧对网页进行重绘。这里就引出了帧的概念,后续会说明。

如何避免回流与重绘

回到前面我们设想的点,我们如何才能保证直接跳到合成过程,避免Layout以及Paint呢,当然有,我们需要对app.js中的uppdate函数进行改造,使用transform: translateX(0px); 做动画,做完update函数的处理逻辑后,我们再次Record一下:

06

从Task子任务中,我们可以发现,Layout –» Paint, 布局和绘制的过程跳过了。这也是为什么我们常说的需要避免回流与重绘。从主线程上来看,能够完全的避免这些过程,避免了很多的运算开销。

这也是为什么经常可以看到这样子的建议:

至于使用will-change和translatez来提升图层,这又是另外知识点了,这里就不张开了。

介绍到这里,我们已经清楚的明白,避免回流和重绘的意义,那么我们提到的帧和rAF 与 渲染路径有啥关系呢。

在视频领域,电影、电视、数字视频等可视为随时间连续变换的许多张画面,其中帧是指每一张画面。

07

这个图,你可以理解成就是像素放到屏幕的完整过程。你肯定对里面的一些关键信息很迷惑,这里作出一些解释。

PROCESSES(进程)

映入眼帘的就是进程:

RENDERER PROCESS THREADS(渲染进程中的线程)

在很多方面,你应该把Compositor线程视为 “大老板”。虽然它不运行JavaScript、Layout、Paint或其他任何东西,但它是完全负责启动主线程工作的线程,然后将帧运送到屏幕上。如果它不需要等待输入事件处理程序,它就可以在等待主线程完成工作的同时运送帧。

你也可以想象Service Workers和Web Workers生活在这个过程中,不过我没有把他们包括在内,因为这让事情变得更加复杂。

THE FLOW OF THINGS(主线程流程)

08

让我们一步步走过这个流程,从vsync到像素,并谈谈在事件的 “全胖 “版本中事情是如何进行的。值得记住的是,浏览器不需要执行所有这些步骤,这取决于什么是必要的。例如,如果没有新的HTML要解析,那么解析HTML就不会启动。事实上,很多时候,提高性能的最好方法就是简单地消除部分流程被启动的必要性!

同样值得注意的是,样式和布局下的红色箭头似乎指向了 requestAnimationFrame。在你的代码中完全有可能意外地触发这两者。这就是所谓的强制同步布局(或样式,视情况而定),它通常对性能不利。

大致上,整个的过程就是上述。

requestIdleCallback

要说这个的话,我们得拿requestAnimationFrame来类比,requestAnimationFrame是在重新渲染屏幕之前执行的,上面提到的rAF,当时做的就是优化动画,所以很适合做动画。

requestIdleCallback你通过主线程里面中的Task去查找的话,会发现它是在渲染屏幕之后执行,通过查阅文章发现,一般会看浏览器是否空闲。