浏览器的内核中主要分为渲染引擎和 javascript 引擎,本篇主要围绕渲染引擎介绍一下浏览器的工作原理。

渲染引擎简介

本文所讨论的浏览器——Firefox、Chrome和Safari是基于两种渲染引擎构建的,Firefox使用Geoko——Mozilla自主研发的渲染引擎,Safari和Chrome都使用webkit。

渲染主流程

渲染引擎首先通过网络获得所请求文档的内容,通常以8K分块的方式完成。下面是渲染引擎在取得内容之后的基本流程:

解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树

渲染主流程.png

这里先解释一下几个概念,方便大家理解:

DOM Tree:浏览器将HTML解析成树形的数据结构。

CSS Rule Tree:浏览器将CSS解析成树形的数据结构。

Render Tree: DOM和CSSOM合并后生成Render Tree。

layout: 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系,从而去计算出每个节点在屏幕中的位置。

painting: 按照算出来的规则,通过显卡,把内容画到屏幕上。

reflow(回流):当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染,内行称这个回退的过程叫 reflow。reflow 会从 这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显 示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲 染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。

repaint(重绘):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。

注意:

display:none 的节点不会被加入Render Tree,而visibility: hidden 则会,所以,如果某个节点最开始是不显示的,设为display:none是更优的。

display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发现位置变化。

有些情况下,比如修改了元素的样式,浏览器并不会立刻reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。但是在有些情况下,比如resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行 reflow。

浏览器如何渲染网页

要了解浏览器渲染页面的过程,首先得知道一个名词——关键渲染路径。关键渲染路径是指浏览器从最初接收请求来的HTML、CSS、javascript等资源,然后解析、构建树、渲染布局、绘制,最后呈现给用户能看到的界面这整个过程。

用户看到页面实际上可以分为两个阶段:页面内容加载完成和页面资源加载完成,分别对应于DOMContentLoaded和Load。

DOMContentLoaded事件触发时,仅当DOM加载完成,不包括样式表,图片等

load事件触发时,页面上所有的DOM,样式表,脚本,图片都已加载完成

浏览器渲染的过程主要包括以下五步:

浏览器将获取的HTML文档解析成DOM树。

处理CSS标记,构成层叠样式表模型CSSOM(CSS Object Model)。

将DOM和CSSOM合并为渲染树(rendering tree),代表一系列将被渲染的对象。

渲染树的每个元素包含的内容都是计算过的,它被称之为布局layout。浏览器使用一种流式处理的方法,只需要一次绘制操作就可以布局所有的元素。

将渲染树的各个节点绘制到屏幕上,这一步被称为绘制painting。

需要注意的是,以上五个步骤并不一定一次性顺序完成,比如DOM或CSSOM被修改时,亦或是哪个过程会重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。而在实际情况中,JavaScript和CSS的某些操作往往会多次修改DOM或者CSSOM。

来看看webkit的主要流程:

webkit.png

再来看看Geoko的主要流程:

Geoko.png

Gecko 里把格式化好的可视元素称做“帧树”(Frame tree)。每个元素就是一个帧(frame)。 webkit 则使用”渲染树”这个术语,渲染树由”渲染对象”组成。webkit 里使用”layout”表示元素的布局,Gecko则称为”reflow”。Webkit使用”Attachment”来连接DOM节点与可视化信息以构建渲染树。一个非语义上的小差别是Gecko在HTML与DOM树之间有一个附加的层 ,称作”content sink”,是创建DOM对象的工厂。

浏览器渲染网页的具体流程

构建DOM树

当浏览器接收到服务器响应来的HTML文档后,会遍历文档节点,生成DOM树。

需要注意以下几点:

DOM树在构建的过程中可能会被CSS和JS的加载而执行阻塞

display:none的元素也会在DOM树中

注释也会在DOM树中

script标签会在DOM树中

无论是DOM还是CSSOM,都是要经过Bytes→characters→tokens→nodes→object model这个过程。

image.png

当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

构建CSSOM规则树

浏览器解析CSS文件并生成CSSOM,每个CSS文件都被分析成一个StyleSheet对象,每个对象都包含CSS规则。CSS规则对象包含对应于CSS语法的选择器和声明对象以及其他对象。

在这个过程需要注意的是:

CSS解析可以与DOM解析同时进行。

CSS解析与script的执行互斥 。

在Webkit内核中进行了script执行优化,只有在JS访问CSS时才会发生互斥。

构建渲染树(Render Tree)

通过DOM树和CSS规则树,浏览器就可以通过它两构建渲染树了。浏览器会先从DOM树的根节点开始遍历每个可见节点,然后对每个可见节点找到适配的CSS样式规则并应用。

有以下几点需要注意:

Render Tree和DOM Tree不完全对应

display: none的元素不在Render Tree中

visibility: hidden的元素在Render Tree中

image.png

渲染树生成后,还是没有办法渲染到屏幕上,渲染到屏幕需要得到各个节点的位置信息,这就需要布局(Layout)的处理了。

渲染树布局(layout of the render tree)

布局阶段会从渲染树的根节点开始遍历,由于渲染树的每个节点都是一个Render Object对象,包含宽高,位置,背景色等样式信息。所以浏览器就可以通过这些样式信息来确定每个节点对象在页面上的确切大小和位置,布局阶段的输出就是我们常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。需要注意的是:

float元素,absoulte元素,fixed元素会发生位置偏移。

我们常说的脱离文档流,其实就是脱离Render Tree。

渲染树绘制(Painting the render tree)

在绘制阶段,浏览器会遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的UI后端组件完成的。

以下是在网上找到的几张图,简单解释了 DOM 树到 RenderLayer 树最终的过程。

image.png

这一段简单的 HTML 代码,其中包含的 body, div canvas script 等元素,通过 HTML,CSS 解析器进行解析,最终会生成一个 RenderLayer 树, 此时不再将所有层绘制到一起,而是进行分层渲染,合成之后再显示到屏幕上。

image.png

以下 RenderLayer 树的结构,从图中可以看出整个树分了 3 个 Layer,在 Layer 下面包含了 RenderBlock,RenderText 等 Render 节点,每个节点上都包含的坐标,大小,以及背景颜色等渲染依赖的信息。

image.png

RenderBlock 其实就是我们 HTML 中的块级元素,我们都知道一个元素是否为块级元素是可以通过 CSS 改变的,所以,一个 RenderLayer 树的结构也会根据CSS的变化变化,如果影响到元素的位置发生变化会都在整个树重新计算,也是就是我们说的回流。

浏览器渲染网页的那些事儿

浏览器主要组件结构

image.png

渲染引擎主要有两个:webkit和Gecko

Firefox使用Geoko,Mozilla自主研发的渲染引擎。Safari和Chrome都使用webkit。Webkit是一款开源渲染引擎,它本来是为linux平台研发的,后来由Apple移植到Mac及Windows上。

虽然主流浏览器渲染过程叫法有区别,但是主要流程还是相同的。

渲染阻塞

JS可以操作DOM来修改DOM结构,可以操作CSSOM来修改节点样式,这就导致了浏览器在遇到

每次去执行JavaScript脚本都会严重地阻塞DOM树的构建,如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建。所以,script标签的位置很重要。

JS阻塞了构建DOM树,也阻塞了其后的构建CSSOM规则树,整个解析进程必须等待JS的执行完成才能够继续,这就是所谓的JS阻塞页面。

由于CSSOM负责存储渲染信息,浏览器就必须保证在合成渲染树之前,CSSOM是完备的,这种完备是指所有的CSS(内联、内部和外部)都已经下载完,并解析完,只有CSSOM和DOM的解析完全结束,浏览器才会进入下一步的渲染,这就是CSS阻塞渲染。

CSS阻塞渲染意味着,在CSSOM完备前,页面将一直处理白屏状态,这就是为什么样式放在head中,仅仅是为了更快的解析CSS,保证更快的首次渲染。

需要注意的是,即便你没有给页面任何的样式声明,CSSOM依然会生成,默认生成的CSSOM自带浏览器默认样式。

当解析HTML的时候,会把新来的元素插入DOM树里面,同时去查找CSS,然后把对应的样式规则应用到元素上,查找样式表是按照从右到左的顺序去匹配的。

例如:div p {font-size: 16px},会先寻找所有p标签并判断它的父标签是否为div之后才会决定要不要采用这个样式进行渲染)。

所以,我们平时写CSS时,尽量用id和class,千万不要过渡层叠。

回流和重绘(reflow和repaint)

我们都知道HTML默认是流式布局的,但CSS和JS会打破这种布局,改变DOM的外观样式以及大小和位置。因此我们就需要知道两个概念:replaint和reflow。

reflow(回流)

当浏览器发现布局发生了变化,这个时候就需要倒回去重新渲染,这个回退的过程叫reflow。reflow会从html这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,以确认是渲染树的一部分发生变化还是整个渲染树。reflow几乎是无法避免的,因为只要用户进行交互操作,就势必会发生页面的一部分的重新渲染,且通常我们也无法预估浏览器到底会reflow哪一部分的代码,因为他们会相互影响。

repaint(重绘)

repaint则是当我们改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸和位置没有发生改变。

需要注意的是,display:none会触发reflow,而visibility: hidden属性则并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,它会被渲染成一个空框。所以visibility:hidden只会触发repaint,因为没有发生位置变化。

另外有些情况下,比如修改了元素的样式,浏览器并不会立刻reflow或repaint一次,而是会把这样的操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是在有些情况下,比如resize窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行reflow。

引起reflow

现代浏览器会对回流做优化,它会等到足够数量的变化发生,再做一次批处理回流。

页面第一次渲染(初始化)

DOM树变化(如:增删节点)

Render树变化(如:padding改变)

浏览器窗口resize

获取元素的某些属性

浏览器为了获得正确的值也会提前触发回流,这样就使得浏览器的优化失效了,这些属性包括offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、调用了getComputedStyle()。

引起repaint

reflow回流必定引起repaint重绘,重绘可以单独触发。

背景色、颜色、字体改变(注意:字体大小发生变化时,会触发回流)

减少reflow、repaint触发次数

用transform做形变和位移可以减少reflow

避免逐个修改节点样式,尽量一次性修改

使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性append到真实DOM中渲染

可以将需要多次修改的DOM元素设置display:none,操作完再显示。(因为隐藏元素不在render树内,因此修改隐藏元素不会触发回流重绘)

避免多次读取某些属性

通过绝对位移将复杂的节点元素脱离文档流,形成新的Render Layer,降低回流成本

几条关于优化渲染效率的建议

结合上文有以下几点可以优化渲染效率。

合法地去书写HTML和CSS ,且不要忘了文档编码类型。

样式文件应当在head标签中,而脚本文件在body结束前,这样可以防止阻塞的方式。

简化并优化CSS选择器,尽量将嵌套层减少到最小。

DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。

如果某个样式是通过重排得到的,那么最好缓存结果。避免下一次用到的时候,浏览器又要重排。

不要一条条地改变样式,而要通过改变class,或者csstext属性,一次性地改变样式。

尽量用transform来做形变和位移

尽量使用离线DOM,而不是真实的网页DOM,来改变元素样式。比如,操作Document Fragment对象,完成后再把这个对象加入DOM。再比如,使用cloneNode()方法,在克隆的节点上进行操作,然后再用克隆的节点替换原始节点。

先将元素设为display: none(需要1次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,你就用两次重新渲染,取代了可能高达100次的重新渲染。

position属性为absolute或fixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响。

只在必要的时候,才将元素的display属性为可见,因为不可见的元素不影响重排和重绘。另外,visibility : hidden的元素只对重绘有影响,不影响重排。

使用window.requestAnimationFrame()、window.requestIdleCallback()这两个方法调节重新渲染。