当前位置: 首页 > Web前端 > CSS

WebRender:让网页渲染流畅

时间:2023-03-30 22:16:17 CSS

WebRender:让网页渲染流畅https://hacks.mozilla.org/2017/10/the-whole-web-at-maximum-fps-how-webrender-gets-rid-of-jankFirefoxQuantum即将推出。它带来了许多性能改进,包括来自Servo的超快CSS引擎。但是Servo中的很大一部分技术还没有被引入FirefoxQuantum,尽管相距不远。那就是WebRender,它作为QuantumRender项目的一部分被添加到Firefox中。WebRender以极速着称,但它所做的并不是加速渲染,而是让渲染结果更流畅。使用WebRender,我们希望应用程序以每秒60帧(FPS)或更高的速度运行:无论显示器有多大,页面每帧变化多少。可以办到。在Chrome和当前版本的Firefox中,一些页面卡在只有15FPS,而使用WebRender它们能够达到60FPS。WebRender如何做到这一点?它从根本上改变了渲染引擎的工作方式,使其更像一个3D游戏引擎。一起来看看怎么说吧。渲染器的工作在我关于Stylo的文章中,我讨论了浏览器如何将HTML和CSS转换为屏幕上的像素,并提到大多数浏览器分五个步骤完成此操作。这五个步骤可以分为两部分。前一部分基本上是构建计划:渲染器将HTML和CSS与视口大小等信息结合起来,以确定每个元素的外观(宽度、高度、颜色等)。最后的结果就是帧树(frametree),也称为渲染树(rendertree)。另一部分是绘画和合成,这是渲染器所做的。渲染器将??上一节的结果转换为显示在屏幕上的像素。对于同一个网页,这项工作只做一次是不够的,必须重复进行。一旦网页发生变化(比如div切换),浏览器就需要重新经历很多这样的步骤。即使页面没有变化(比如页面滚动,或者一些文本被高亮显示),浏览器仍然需要做第二部分的一些步骤,然后在屏幕上绘制新的内容。为了使滚动、动画等看起来流畅,它们必须以每秒60帧的速度呈现。您以前可能听说过每秒帧数(FPS)这个术语,但您可能不确定它的含义。想象一下,你手里拿着一本翻书(FlipBook)。满是静态图画的书,用手指快速翻动,画面仿佛在动。为了让这个翻书动画看起来流畅,它需要每秒翻60页。这本书是用图画制作的。纸上有很多小方块,每个方块只能填一种颜色。渲染器的工作是为绘图中的方块着色。将图中的所有方块填满,一帧渲染完成。当然,电脑里是没有实物图的。相反,它是一块称为帧缓冲区的内存。帧缓冲区中的每个内存地址就像绘图中的一个正方形……它对应于屏幕上的一个像素。浏览器将用代表RGBA(红、绿、蓝和alpha通道)颜色值的数字填充每个位置。当需要刷新显示时查询此内存。大多数计算机显示器每秒刷新60次。这就是浏览器尝试以每秒60帧的速度呈现页面的原因。这意味着浏览器有16.67毫秒的时间来完成所有工作(CSS样式、布局、绘画)并用像素颜色填充帧缓冲区内存。两帧之间的时间(16.67ms)称为帧预算。有时您可能会听到人们谈论掉帧。丢帧是指系统无法在帧预算内完成工作。在缓冲区颜色填充完成之前,监视器会尝试读取新帧。在这种情况下,显示屏将再次显示旧版本的帧信息。丢帧就像从活页簿中撕下一页。结果,动画看起来像消失或跳跃,因为前一页和下一页之间的过渡页面丢失了。因此,请确保在再次检查显示之前将所有像素放入帧缓冲区。看看浏览器过去是如何做的,以及从那时起发生了什么变化。从这里,你可以找到提速的空间。渲染和合成简史注:渲染和合成是不同渲染引擎之间最不同的地方。_单平台浏览器(Edge和Safari)与跨平台浏览器(Firefox和Chrome)的工作方式不同。_即使是最早的浏览器也进行了一些优化,使页面呈现速度更快。例如,当滚动页面时,浏览器会保留仍然可见的部分并移动它。然后在空白处绘制新的像素。弄清楚发生了什么变化并仅更新发生变化的元素或像素。此过程称为失效。后来,浏览器开始应用更多的失效技术,例如矩形失效。矩形失效技术找到最小的矩形来包围屏幕的每个变化部分。然后重新绘制那些矩形内的内容。这在页面变化不大的情况下确实可以省去很多工作。例如,光标闪烁。但如果页面的大部分内容发生变化,这还不够。因此,再次出现了处理这些情况的新技术。引入图层和合成当页面的大部分发生变化时,使用图层要容易得多……至少在某些情况下是这样。浏览器中的图层很像Photoshop中的图层,或者手绘动画中使用的洋葱皮。一般来说,不同的元素绘制在不同的图层上。然后可以调整这些层的相对层次结构。这些一直是浏览器的一部分,但并不总是用于加速。起初,它们只是用来确保页面正确呈现d。它们对应于堆叠上下文。例如,半透明元素将处于其自己的堆叠上下文中。这意味着它有自己的图层,因此您可以将其颜色与其下方的图层混合。帧完成后,层将被丢弃。在下一帧中,所有图层将再次重绘。但是,这些层中的内容通常不会因帧而异。想想那种传统动画。背景没有改变,只有前景中的人物改变了。保留和重复使用背景层效率更高。这就是浏览器所做的。它保留了这些层。然后浏览器可以仅重绘已更改的图层。在某些情况下,图层甚至没有改变。它们只需要重新排列:例如动画在屏幕上移动,或者滚动。组织图层的过程称为合成。合成器从两部分开始:源位图:背景(包括可滚动内容占据的空框)和可滚动内容本身目标位图:屏幕上显示的位图首先,合成器将背景复制到目标位图。然后找到应该显示的可滚动部分。将节复制到目标位图。这减少了主线程上的绘制量。但这意味着主线程会花费大量时间进行合成。并且有很多工作在主线程上争夺时间。这个我之前讲过,主线程有点像全栈开发。它负责DOM、布局和JavaScript。并且还负责绘图和合成。对于主线程用于绘制和合成的每一毫秒,都有不能用于JavaScript和布局的毫秒数。硬件的另一部分闲置,没有太多工作要做。该硬件专用于图形。它是一个GPU。自90年代末以来,游戏一直使用GPU加速来渲染帧。从那时起,GPU变得越来越强大。GPU加速合成因此浏览器开发人员开始将任务卸载到GPU。有两个任务可以卸载到GPU:1.图层绘制2.图层合成卸载到GPU的绘制可能很棘手。所以在大多数情况下,跨平台浏览器还是通过CPU来绘制。但是GPU可以很快的完成合成工作,搬过来也比较简单。一些浏览器在这种并行方法上更进了一步,直接在CPU上添加了一个合成器线程。它管理GPU中发生的合成工作。这意味着如果主线程正在做一些事情,比如运行JavaScript,合成器线程仍然可以处理其他工作,比如在用户滚动时滚动内容。这会将所有合成工作移出主线程。尽管如此,它仍然在主线程上留下了很多工作。当需要重绘图层时,主线程需要进行绘制工作,然后将图层卸载到GPU。一些浏览器将绘图工作移至另一个线程(目前Firefox正在处理此问题)。但是将绘图工作卸载到GPU会快得多。GPU加速绘图因此,浏览器也开始将绘图工作卸载到GPU。这种转变仍在进行中。一些浏览器一直通过GPU进行绘制,而另一些浏览器只能在特定平台(例如Windows或移动设备)上进行绘制。GPU渲染可以解决一些问题。CPU可以腾出时间专注于JavaScript和布局g'z。此外,GPU绘制像素的速度比CPU快得多,因此它可以绘制得更快。这也意味着从CPU复制到GPU的数据更少。然而,保持绘图和合成工作之间的这种分离仍然需要付出代价,即使这一切都发生在GPU上。做出这种区分还限制了可以采用的优化类型,这些优化可以使GPU运行得更快。这就是WebRender旨在解决的问题。它从根本上改变了渲染的方式,消除了绘图和合成之间的区别。这种解决渲染器性能的方法可以在当前网络中提供最好的用户体验,为未来的网络提供最好的支持。这意味着我们不只是想让帧渲染得更快……我们想让渲染更加一致且无闪烁。即使需要绘制很多像素,比如4k显示器或WebVR设备,我们仍然希望体验流畅。现在的浏览器闪烁是什么时候发生的?在某些情况下,上述优化可以加快页面渲染。当页面上没有太多变化时(就像一个闪烁的光标),浏览器将做尽可能少的工作。将页面拆分为多个层可以扩展最佳案例的数量。“paint+composite”架构通过绘制多个图层并让它们相对移动而非常有效。然而,层的使用也需要权衡。这将占用相当多的内存,实际上可能会减慢速度。浏览器需要组合有意义的层。但很难说什么是有道理的。这意味着如果页面中有很多不同的东西在移动,那么图层可能太多了。这些层占用内存并需要很长时间才能传输到合成器。其他时候,当您需要多层时,您可能只会得到一层。该层将不断重绘并传输到合成器进行合成工作,而无需更改任何内容。这意味着您将绘图量加倍,每个像素都绘制两次,但没有任何好处。跳过合成步骤并直接渲染页面会更快。还有很多情况下层不是很有用。如果您对背景颜色进行动画处理,则必须重绘整个图层。这些层只能帮助处理少量的CSS属性。即使大多数帧都是最佳情况(即,它们只占帧预算的一小部分),运动仍然会不稳定。只要有三两帧落入最坏情况,就会出现可察觉的闪烁。这些情况称为性能悬崖。应用程序运行平稳,直到遇到这些最坏的情况(例如背景颜色动画)并且帧率突然处于边缘。然而,这些性能悬崖是可以避免的。这个怎么做?跟随3D游戏引擎的脚步。像游戏引擎一样使用GPU如果您不再尝试猜测需要哪些层会怎么样?如果您去除绘图和合成之间的边界并且只考虑每帧绘制像素会怎么样?这听起来可能很荒谬,但有先例。现代视频游戏重绘每个像素并保持每秒60帧的速度比浏览器更可靠。他们以意想不到的方式做到了这一点……他们只是重新绘制了整个屏幕,而没有创建那些最小化绘制内容的无效矩形和层。这样渲染网页不会更慢吗?如果在CPU上绘制,确实会慢一些。但这就是GPU的用途。GPU专为极端并行处理而设计。我在上一篇关于Stylo的文章中谈到了并行性。通过并行性,一台机器可以同时执行多个操作。它一次可以完成的任务数量取决于核心数量。CPU通常有2到8个内核。GPU往往至少有几百个内核,通常超过1,000个内核。尽管这些内核的工作方式不同。它们不能像CPU内核那样完全独立运行。相反,它们通常一起工作,对数据的不同部分执行相同的指令。这正是我们填充像素时所需要的。每个像素都可以由不同的内核填充。GPU能够一次处理数百个像素,在像素处理方面比CPU快得多……当所有内核都在工作时。由于内核需要同时做同样的事情,GPU的步骤非常严格,API也非常有限。让我们看看这是如何工作的。首先,您需要告诉GPU要绘制什么。这意味着将形状传递给它,并告诉它如何填充它。为此,首先将绘图分解成简单的形状(通常是三角形)。这些形状位于3D空间中,因此某些形状可以位于其他形状的后面。然后组成三角形所有角顶点的x,y,z坐标数组。然后发出绘制调用-告诉GPU绘制这些形状。接下来由GPU接管。所有核心将同时处理同一件事。他们将:找到形状的所有角顶点位置。这称为顶点着色。找到连接这些角的顶点的线。从这里你可以得到形状覆盖了哪些像素。这称为光栅化。现在我们知道形状覆盖的像素,我们可以遍历每个像素并确定该像素的颜色。这称为像素着色。最后一步可以用不同的方式完成。为了告诉GPU做什么,它将一个称为像素着色器的程序传递给GPU。像素着色器是GPU的几个可编程部分之一。一些像素着色器很简单。例如,如果形状是单一颜色,着色器程序只需要为形状中的每个像素返回相同的颜色。其他情况更复杂。例如,当有一张背景图片时,需要找出图片中每个像素点对应的部分。您可以像艺术家一样缩放图像……在图像上放置一个网格,对应于每个像素。这样只需要知道某个像素点对应的区域,然后对该区域进行颜色采样即可。这称为纹理映射,因为它将图像(称为纹理)映射到像素。对于每个像素,GPU调用一个像素着色器程序。不同的核心可以同时在不同的像素上并行工作,但它们都需要使用相同的像素着色器程序。当您告诉GPU绘制形状时,您就是在告诉它要使用哪个像素着色器。与几乎所有网页一样,页面的不同部分将需要不同的像素着色器。着色器适用于一次绘制中的所有形状,因此通常需要将绘制工作分成几组。这些称为批处理。为了尽可能利用所有核心,创建了一定数量的批处理作业,每个批处理包含大量形状。这就是GPU将工作分割成数百或数千个内核的方式。正是由于这种极端的并行性,我们才能想到每一帧渲染所有内容。即使有这种极端的并行性,仍然需要大量工作。需要动脑筋才能解决。WebRender来了...WebRender如何利用GPU让我们回顾一下浏览器渲染网页的步骤。这里会有两个变化。1.不再区分绘图和合成。它们都是同一步骤的一部分。GPU根据传递给它的图形API命令同时执行它们。2.布局步骤会产生不同的数据结构。在此之前是框架树(或Chrome中的渲染树)。现在将生成一个显示列表。显示列表是一组高级绘图指令。它告诉我们要绘制什么并且不指定任何图形API。每当有新的东西要绘制时,主线程将显示列表提供给RenderBackend,这是在CPU上运行的WebRender代码。RenderBackend的工作就是将这个高级绘图指令列表转换成GPU需要的绘图调用,并将这些绘图调用分成同一批来加速运行。RenderBackend然后将这些批次传递给合成器线程,合成器线程又将它们传递给GPU。RenderBackend传递给GPU的绘制调用需要尽可能快地运行。它为此使用了几种不同的技术。节省时间的最佳方法是从列表中删除任何不必要的形状(及早剔除),什么也不做。首先,RenderBackend可以减少显示列表项。它标识哪些项目将实际出现在屏幕上。为此,它会查看每个滚动框的滚动距离等内容。如果形状的某些部分在框内,则该形状将包含在需要绘制的列表中。否则将被删除。这个过程称为早期剔除。最小化中间纹理的数量(渲染任务树)现在有一个只包含将要使用的形状的树结构。这棵树被组织成前面提到的堆叠上下文。链接文本CSS过滤器和堆叠上下文等效果使事情变得复杂。假设有一个不透明度为0.5的元素包含子元素。您可能认为每个子元素都是透明的……但实际上整个组都是透明的。所以组需要首先渲染为纹理,每个孩子都是不透明的。然后,当您将子元素连接到父元素时,您可以更改整个纹理的透明度。这些堆叠上下文可以嵌套......父元素可能是另一个堆叠上下文的一部分。这意味着它必须被渲染成另一个中间纹理......为这些纹理创造空间是昂贵的。我们希望尽可能地将事物分组到相同的中间纹理中。为了帮助GPU做到这一点,需要创建渲染任务树。有了它,就可以知道哪些纹理需要在其他纹理之前创建。任何不依赖于其他纹理的纹理都可以在第一次创建,这意味着它们可以在那些中间纹理中组合。所以在上面的例子中,我们先输出盒子阴影的一个角。(实际上比那要复杂一点,但这就是要点)。在第二遍中,这个角可以镜像到盒子的各个部分。然后可以将该组呈现为完全不透明。接下来,我们需要做的就是改变这个纹理的不透明度,并将它放在需要提供给屏幕的最终纹理中。通过构建这个渲染任务树,可以找到需要使用的最小数量的离屏渲染目标。这很好,如前所述,为这些渲染目标纹理创建空间是昂贵的。这也便于批处理。Drawcall分组(batching)如前所述,需要创建一定数量的批次,每个批次包含大量的形状。请注意,您创建批次的方式确实会影响速度。同一批次的形状数量应尽可能多。这是由几个原因决定的。首先,当CPU告诉GPU进行绘图调用时,CPU必须做很多工作。它需要做很多工作,比如启动GPU、上传shader程序和测试硬件bug等。而在CPU做这些工作的时候,GPU可能是空闲的。其次,改变状态是有代价的。假设您需要在批次之间更改着色器程序。在典型的GPU上,您需要等到所有内核都完成了当前着色器的工作。这称为排空管道。在流水线清空之前,其他核心不会闲置。因此,批次包含尽可能多的东西。对于典型的PC,每帧需要100次或更少的绘制调用,每次调用中有数千个顶点。这允许充分利用并行性。从渲染任务树中,您可以找出可以批处理的内容。目前,每种类型的图元都需要一个着色器。例如边框着色器、文本着色器、图像着色器。我们认为可以将许多着色器组合起来,从而提高批处理能力。但目前来说已经很不错了。它们已准备好发送到GPU。但实际上,还是有一些排除是可以做到的。减少像素阴影(Z剔除)大多数网页都有很多重叠的形状。例如,具有背景的div内的文本框位于具有另一个背景的主体内。当GPU计算每个像素的颜色时,它可以计算每个形状中像素的颜色。但是只会显示顶层。这称为透支,它会浪费GPU时间。所以我们可以先渲染顶部的形状。绘制下一个形状的时候,遇到相同的像素点,先检查是否已经有值。如果有值则跳过。但这有一点问题。当形状是半透明时,需要混合两种形状的颜色。为了使它看起来正确,需要从内向外绘制。因此,这项工作需要分为两部分。首先一起做不透明的工作。从外向内渲染所有不透明形状。跳过其他像素后面的像素。然后处理半透明形状。工作是由内而外完成的。如果半透明像素落在不透明像素之上,它将混合到不透明像素中。如果它落后于不透明形状,则计算将被忽略。将工作拆分为不透明度和alpha通道部分,跳过不必要的像素计算,这个过程称为Z剔除。这看起来像是一个简单的优化,但对我们来说却是一个巨大的成功。在一个典型的网页上,这项工作大大减少了我们需要处理的像素数量,我们目前正在研究如何将更多的工作转移到不透明度步骤。至此,我们已经准备好了内容框架。我们已经尽可能地减少了工作量。准备绘制我们准备启动GPU并渲染批次。警告:并非一切都取决于GPUCPU仍然需要做一些绘图工作。例如,我们仍然使用CPU来渲染文本块中的字符(称为字形)。也可以在GPU上执行此操作,但很难获得与计算机在其他应用程序中的字形渲染相匹配的像素完美外观。所以GPU渲染出来的字体看起来会很乱。我们正在尝试通过Pathfinder项目将字形之类的东西卸载到GPU。这些当前由CPU绘制为位图。然后将它们上传到GPU的纹理缓存。此缓存保留在帧之间,因为它们通常不会更改。虽然这个绘图工作是由CPU完成的,但是在速度上还是有提升空间的。比如在使用某种字体绘制字符时,我们会将不同的字符分开,使用不同的内核分别渲染。这与Stylo用于并行计算样式的技术相同......请参见此处。WebRender的后续步骤在FirefoxQuantum发布后的几个版本中,WebRender有望在2018年作为QuantumRender项目的一部分出现在Firefox中。这将使今天的网页运行更加流畅。随着屏幕上像素数量的增加,渲染性能变得越来越重要,因此WebRender还让Firefox为高分辨率4K显示器的新浪潮做好准备。但WebRender不仅仅适用于Firefox。这对于正在进行的WebVR工作也很重要,因为需要在4K显示器上以90FPS的速度为每只眼睛渲染不同的帧。当前可以通过Firefox标志启用早期版本的WebRender。集成仍在进行中,因此目前的性能不如工作完成时。如果你想跟上WebRender的发展,你可以关注GitHub存储库,或者在Twitter上关注FirefoxNightly,每周更新QuantumRender项目。关于作者LinClarkLin是Mozilla开发者关系团队的一名工程师。她正在修补JavaScript、WebAssembly、Rust和Servo,以及绘制代码漫画。