浏览器渲染相关知识

2019 / 09 / 22

从输入url按下回车到渲染出整个页面的过程一直都是考查一个前端开发人员基础知识最好的问题之一。说实话我目前也很难很完备的答出来这个问题,我在一段时间前曾经立下一个flag,半年后当别人再次问到我这个问题的时候,我可以滔滔不绝的回答半个小时以上。

其实要回答问题,首先需要拆分一下,首先可能是按下回车之后。

  1. 浏览器该如何工作,有几个进程,又是否多线程,如何和操作系统交互,这可能是一些操作系统和浏览器内核的知识。
  2. 不同的url浏览器如何处理,如何发起网络请求。网络中的DNS解析,http到ssl,到TCP三次握手,到IP的路由查找,以及链路层的工作,以及这是关于计算机网络的知识。
  3. 浏览器收到http响应结果时,如何把资源展示到网页上的,http对资源的缓存机制,浏览器的缓存机制,从html到像素被分毫不差的渲染到页面上发生的事情。这个主要就是渲染的原理。

三个阶段分别侧重三种不同方面的只是 1. 操作系统 2. 网络 3. 浏览器渲染原理。而本文着重讲述第三个阶段。

博主是通过youtube里的一个视频学习的,可以搜索 life of a pixel。ppt可以点击这里

目标

要理解浏览器渲染的机制,首先需要知道浏览器为什么要这么去设定它的机制。

  1. 浏览器的渲染并不是它自己就能完成的,它需要借助操作系统的渲染接口,也就是openGL。windows中也提供了另外一种接口direct3D。所以浏览器需要把html和css的字符串转换为计算机可以识别的图形语言。
  2. 浏览器也需要给JS脚本和一些用户代理提供接口,用来查询和更新当前的页面状态。保证页面的可更新,可检测等。

所以在渲染的时候需要提供给js可读取和操作的数据结构。同时也要给openGL提供可识别的渲染数据。为了保持尽量高的性能,保持更高的封装性和便捷性,大多数开发者接触到的浏览器开发的接口只是非常简单的DOM API。所以多数前端开发者可感知的那部分只是DOM树和附着在DOM上的样式树。

渲染的生命周期

parse 解析

首先是html和css的解析,这个过程对于大多数前端都是非常熟悉的。

html 被转化成具有父节点,子节点和兄弟节点的文档对象模型DOM,解析css为样式文档CSSOM可以用来查询和修改。

style computed 计算样式

根据选择器和DOM匹配为DOM进行最终的值,被放在一个叫做computedStyle的巨大对象里,作为样式到值的映射。

layout 排版

构建了DOM并计算了所有样式之后,下一步就是计算视觉几何形状。与内容所占的几何区域相对应的矩形坐标,可以称做 边界矩形 或者 LayoutObject布局对象。最简单的情况就是块级元素垂直向下排布,内联元素水平排布并且可以换行。当然还有很多非常复杂的布局比如float,table,flex,grid等等。而文字会有行高,字间距,不同样式的文字排版方式也又差异。

边界矩形也会有border box rect和overflow rect等区别。而overflow rect也如果会有scrollable的特点。document本身就是一个overflow rect。

每一个需要渲染的node节点会根据layout算法生成对应的layoutObject。这些layoutObject和DOM不是一一对应的,和需要显示的node节点对应。

paint 绘制

根据layout object可以实现绘制。这一个过程会生成一个类似于的todo list绘制列表。例如从(x, y)开始绘制一个矩形,宽度和高度分别多少,颜色和阴影之类的,对于绘制来说会无差别的对待各种不同类型的布局,全部当作样式说明来看待。

raster 栅格化

绘制的结果是用来进行栅格化的,如果使用过ps的化会非常熟悉这个词,栅格化意思是说把图像以像素为单位,一个格子一个格子的绘制出来。日常所见到的网页其实是位图,只不过在放大缩小的时候重新进行了栅格化而已。栅格化的结果是包含颜色和透明度的像素位。

栅格化阶段也会解码图片信息,调用适当的解码器解析图片,获取图片的像素信息,类似于canvas的getImageData获取像素信息。

这个时候像素依然没有展示在屏幕上,而是保存在内存中,通常是openGL纹理对象引用的GPU内存,或者可以称为显存。GPU也可以运算paint为raster,俗称GPU加速渲染。

栅格化调用一个叫做skia的开源库用来发起openGL调用,skia作为和硬件沟通的抽象层,可以识别更为复杂的一些东西,比如贝塞尔曲线和路径算法等。

栅格化的结果是对写在GPU命令区的缓存。

draw GPU 图形处理器

图形处理对于开发者来说是一个sandbox,几乎无从知晓里边发生了什么,这个对于开发者或者用户来说其实更多的是非常友好的一面。显然它很好的帮助我们远离了不必要的图形驱动程序的困扰。

GPU从命令去缓存中读取数据,并且通过一组函数指针发起实际的GL调用。这里通过skia写入GPU命令区缓存的方法,而传统的方式是IPC进行进程间通信的性能相对要差一些。

在windows系统中google通过ANGLE来把openGL转化为directX,directX是windows上的加速图形的API。GL调用之后我们就能看到html和css表达的图形被渲染到了页面上了。

更新

change 变动

从文档到渲染到屏幕上的像素是非常昂贵的一个过程。而渲染也并不是静态的而是可以动态改变的。比如load, zooming, scroll, animation, transition或者javascript对文档进行改变都会引起变动。

如果需要改变很流畅的化,从排版到栅格化的过程需要在16.6ms内完成,也就是每秒钟60帧,否则就会出现卡顿。一般来说layout相对更昂贵,但对于大面积的变动,paint + raster也很昂贵。同时js脚本的执行也会阻塞主渲染进程,导致页面出现卡顿janky。

layers 分层

compositor合成器线程可以执行页面中非主进程的渲染任务,比如js脚本执行的时候仍然可以滚动页面或者执行动画,它可以把页面分离成独立栅格的不同图层。然后再合并图层进行渲染。

页面中是从在和ps中图层一样的多图层概念的,比如scrollable的容器会创建一个滚动视图,其实是一种新的图层,transform等一些特定的属性也会创建新的图层来更新页面。

其实在paint阶段之前还会有一个build layer tree的阶段,可以用来进行图层的分离,图层树可以保证不同图层的相互依赖的同时互相的影响也会更小。

而build layer发生在paint之前其实也会导致不同图层分别paint和raster,其实paint放在build layer之前可能会更好一些。日后google也会去修复这个问题。

tilling 切片

图层很大的时候,比如连续很多屏的一个图层,其实渲染当前视口区域的图层优先级会更高,所以图层paint完成后会由合成器把图层分块进行栅格化。切片是栅格化的单位。所以其实网页是一块一块的渲染的,不光网页中的图片是一块一块的。

draw quads 绘制块

切片完成之后,就会输出一些给GPU识别的compisitor frames,每个合成器帧由多个draw quads组成,就是一个一个的绘制矩形的命令。

compisitor frames可能来自于多个浏览器的渲染进程(renderer process),其实可能又多个渲染进程完成,界面其实还可以嵌入在其他的界面中,最典型的就是iframe,不同渲染进程的统筹协调由browser process完成。

最后我们调用GL来完成compisitor中的draw quads的绘制,一般来讲GPU命令缓存区和compisitor的任务执行区是双缓存的,所以新产生的compistitor frames也会在GPU读取。

关于重排和重绘

上边的一系列关于渲染的描述可以感受到其实简单的页面渲染其实也会进行大量的计算和渲染的时候如果改变样式或者文档结构(包括文字变化)都又可能触发重排或者重绘,而读取文档属性的时候也有可能触发。

github有一个整理了一系列的性能杀手的文章,具体参考 What forces layout / reflow,可见触发重排的API非常多,在使用的时候需要谨慎处理这些,当然在不复杂的页面可以不担心这些问题。但如果页面复杂度比较高的时候,他们很容易带来性能问题。

优化方法

  1. 减少重排,position: absolute 可以让元素脱离文档流,减少不必要的重排。改变color,background-color等属性也不会产生大规模的重排。
  2. GPU加速,transform, opacity, filter, will-change 等属性可以产生分层,不会产生重排重绘,硬件加速还有canvas2D canvas3D transition和video。
  3. 减少性能杀手中的DOM操作和查询,尽量把数据提前缓存起来。
  4. 整体操作,就是js操作DOM的时候可以clone DOM进行一系列的操作之后然后替换原有的DOM。包括document.createFragment和cssText等方法也有这种功效。

参考文章

openGL百度百科

blink维基百科

嗨,请先 登录

加载中...
(๑>ω<๑) 又是元气满满的一天哟