浏览器主要组件
用户界面、浏览器引擎、呈现引擎、网络 、用户界面后端、JavaScript解释器、数据存储。
这些组件都人如其名,就不详细介绍了,其中浏览器引擎,提供了对呈现引擎的更高级别的封装。
下面就介绍渲染引擎的相关内容。
渲染引擎
Rendering Engine, 或称为 呈现引擎。
负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
各个呈现引擎对应的主要浏览器:
- Trident:又叫MSHTML, IE4~IE11。
- EdgeHTML:IE Edge.
- Gecko:Firefox.
- Webkit:Safari.
- Blink:Chromium.
- Presto:原Opera,后改用Blink.
不同的呈现引擎在实现上都有不同,但是大致流程都是一致的。
主流程
呈现引擎一开始会从网络层获取请求文档的内容,内容的大小一般限制在 8000 个块以内。
然后进行如下所示的基本流程:
- 首先解析html和css,构建DOM和CSSOM。
- 通过DOM和CSSOM构建呈现树(render tree)。呈现树包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。
- 进入“布局”阶段,为每个节点分配一个应出现在屏幕上的确切坐标。
- “绘制” 阶段:呈现引擎会遍历呈现树,由用户界面后端层将每个节点绘制出来。
note:
1.这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来。
2.解析html,生成内容树,该树不完全是DOM树,该树包含DOM节点。
主流程示例
Webkit:
Gecko:
Gecko 将视觉格式化元素组成的树称为“框架树”。每个元素都是一个框架。
WebKit 使用的术语是“呈现树”,它由“呈现对象”组成。
对于元素的放置,WebKit 使用的术语是“布局”,而 Gecko 称之为“重排”。
对于连接 DOM 节点和可视化信息从而创建呈现树的过程,WebKit 使用的术语是“附加”。
有一个细微的非语义差别,就是 Gecko 在 HTML 与 DOM 树之间还有一个称为“内容槽”的层,用于生成 DOM 元素。
关键呈现路径
创建对象模型
在解析html和css时,会创建文档对象模型(DOM)和CSS对象模型(CSSOM),
相关的Timeline面板的事件为:Parse HTML 和 Recalculate Style。
默认情况下, HTML 和 CSS 都是阻塞渲染的资源。所以CSSOM 构建完成前,浏览器会暂停渲染任何已处理的内容。
媒体类型与媒体查询会把 CSS 资源标记为不阻塞渲染。声明默认的媒体类型和查询也会阻塞。例如:media=“all"。
所有的 CSS 资源,不论阻塞或不阻塞,浏览器都会下载。
「阻塞渲染」仅是指该资源是否会暂停浏览器的首次页面渲染,直至该资源准备就绪。
构建呈现树
根据CSSOM 树与 DOM 树生成一棵渲染树,渲染树既包含网页可见的DOM内容,又包含CSSDOM样式信息。
渲染树只包括渲染页面需要的节点。非可视化的 DOM 元素不会插入呈现树中。例如:header元素,display为none的元素。
有一些 DOM 元素对应多个可视化对象。它们往往是具有复杂结构的元素,无法用单一的矩形来描述。例如 select。
有一些呈现对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不同,如float,absoulte,它们处于正常的流程之外,放置在树中的其他地方,并映射到真正的框架,而放在原位的是占位框架。
有一些呈现器没有对应的DOM节点,例如不规范的html可能会导致创建匿名的呈现器。
布局
呈现树不包含位置和大小信息。计算这些值的过程称为布局(layout)或重排(reflow)。
布局过程输出一个「盒模型」,它精确捕获每个元素在视口中的准确位置及尺寸:所有相对度量单位都被转换为屏幕上的绝对像素位置,等等。
布局是一个递归的过程。它从根节点开始,遍历部分或所有的框架层次结构,为每一个需要计算的呈现器计算几何信息。
所有的呈现器都有一个“layout”或者“reflow”方法,每一个呈现器都会调用其需要进行布局的子代的 layout 方法。
- 全量布局:屏幕大小调整、全局样式更改等会触发全量布局。
- 增量布局: 使用Dirty 位系统。主要有两种标记:“dirty”和“children are dirty”。对标记为 dirty 的呈现器进行布局。
- 异步布局:增量布局一般是异步布局。
- 同步布局:请求样式信息(例如“offsetHeight”)的脚本可同步触发增量布局(使用raf避免同步布局)。 全局布局也往往是同步触发的。
绘制
把渲染树中的每个节点转换为屏幕上的实际像素 - 称为「绘制」或者「栅格化」。
绘制顺序:https://www.w3.org/TR/CSS21/zindex.html (背景颜色、背景图片、边框、子代、轮廓)
加载javascript
内联脚本: HTML 解析器遇到一个 script 标签,它会暂停构建 DOM,并移交控制权给 JavaScript 引擎;等 JavaScript 引擎执行完毕,浏览器从中断的地方恢复 DOM 构建。
脚本文件: 浏览器必须暂停,然后等待脚本从磁盘、缓存或远程服务器中取回.
script标签添加async属性,异步执行和加载js。
CSSOM 准备就绪前,JavaScript 执行被延后。
note:
- 实际目前浏览器会优化这一过程,只有在js中遇到访问样式信息时,才会等待CSSOM就绪。
- 同时,在解析和处理js,css同时,其他线程会预解析文档的其余部分,找出并加载需要的外部资源。
优化呈现路径
我们来定义将用于描述关键呈现路径的词汇:
- 关键资源:可能阻止网页首次呈现的资源。
- 关键路径长度:即往返过程数量,或提取所有关键资源所需的总时间。
- 关键字节:实现网页首次呈现所需的总字节数,是所有关键资源的传输文件大小总和。
所以,优化关键呈现路径常规步骤:
- 尽量减少关键资源数量:删除相应资源、延迟下载、标记为异步资源等等。
- 分析和描述关键路径:资源数量、字节数、长度。
- 优化剩余关键资源的加载顺序:需要尽早下载所有关键资源,以缩短关键路径长度。
- 尽量减少关键字节数,以缩短下载时间(往返次数)。
优化首次呈现的规则和建议:
- 删除阻止呈现的 JS 和 CSS: 尽量减少关键资源的数量、尽量减少下载的关键字节数以及尽量缩短关键路径的长度。
- 优化 JavaScript 的使用: 1.异步加载JS, 2.延迟解析 JavaScript, 3.避免运行时间长的 JavaScript
- 优化 CSS 的使用:
- 将 CSS 放入文档的 head 标签内,使浏览器可以尽早发现<link\>标签后发出css请求。
- 避免使用 CSS import
- 把阻止呈现的 CSS 内联到html中,关键路径中减少额外的往返次数。
60fps和设备刷新率
当今大多数设备的屏幕刷新率都是 60次/秒 。因此,浏览器渲染动画或页面的每一帧的速率,也需要跟设备屏幕的刷新率保持一致。才会显得动画流畅。
也就是说,浏览器对每一帧画面的渲染工作需要在16毫秒(1秒 / 60 = 16.66毫秒)之内完成。但实际上,在渲染某一帧画面的同时,浏览器还有一些额外的工作要做(比如渲染队列的管理,渲染线程与其他线程之间的切换等等)。因此单纯的渲染工作,一般需要控制在10毫秒之内完成,才能达到流畅的视觉效果。
使用requestAnimationFrame方法执行逐帧动画。回调函数会传入一个时间戳作为参数。
requestAnimationFrame返回一个id,cancelAnimationFrame使用该ID,取消注册的任务。
raf的回调在下一帧重绘前调用,调用频率为每秒60次或者和浏览器频率同步。
如果当前页面是个后台页面没有展示时,会减少raf执行频率或者不执行。
重绘渲染流水线
一般渲染流程包含以下5个关键步骤:
- javascript
使用JavaScript来实现一些视觉变化的效果。例如JQuery的animate函数。 - 计算样式
根据CSS选择器,对每个DOM元素匹配对应的CSS样式。这一步结束之后,就确定了每个DOM元素上的CSS样式规则。 - 布局
上一步确定了每个DOM元素的样式规则,这一步就是具体计算每个DOM元素最终在屏幕上显示的大小和位置。
web页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。 - 绘制
绘制,本质上就是填充像素的过程。一般来说,这个绘制过程是在多个层上完成的。 - 渲染层合并
在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。
note: 绘制过程本身包含两步:
- 创建一系列draw调用;
- 填充像素。 第二步的过程被称作 "rasterization” 。
因此当你在DevTools中查看页面的paint记录时,你可以认为它已经包含了 rasterization。
在实际渲染中,根据变化的样式不同或访问元素样式属性的不同,对视觉变化效果的一个帧的渲染,有以下三种情况:
- JS / CSS > 计算样式 > 布局 > 绘制 > 渲染层合并
如果你修改一个DOM元素的”layout”属性,也就是改变了元素的样式(比如宽度、高度或者位置等),
那么浏览器会检查哪些元素需要重新布局,然后对页面激发一个reflow过程完成重新布局。 - JS / CSS > 计算样式 > 绘制 > 渲染层合并
如果你修改一个DOM元素的“paint only”属性,比如背景图片、文字颜色或阴影等,这些属性不会影响页面的布局,因此浏览器会在完成样式计算之后,跳过布局过程,只做绘制和渲染层合并过程。 - JS / CSS > 计算样式 > 渲染层合并
如果你修改一个非样式且非绘制的CSS属性,那么浏览器会在完成样式计算之后,跳过布局和绘制的过程,直接做渲染层合并。
目前只有transforms和opacity两个属性。
优化渲染性能
优化JavaScript的执行效率
- 使用requestAnimationFrame方法。
- 降低代码复杂度或者使用Web Workers。
- 避免对JavaScript代码进行微优化。
例如虽然offsetTop属性比getBoundingClientRect()要快,但一般情况下,在每一帧中运行的代码中调用这些函数的次数是有限的。因此,在这些微优化上花再大的精力,整体性能可能也就获得若干毫秒的提升。而如果换一种实现方式,浏览器的执行速度可以会有上百倍的提升(例如在动画中的同步渲染改为异步)。
降低样式计算的范围和复杂度
样式计算: 添加或移除一个DOM元素、修改元素属性和样式类、应用动画效果等操作,都会引起DOM结构的改变,从而导致浏览器需要重新计算每个元素的样式、对页面或其一部分重新布局(多数情况下)。
- 降低样式选择器的复杂。
例如.box:nth-last-child(-n+1) .title 这样的选择器,
样式是从右向左匹配的, 从而确定一个dom元素是否匹配该样式,要1)检查是否有title类,2)是否有一个父元素,且具有.box类,3)父元素是第(-n+1)个子元素。 这个计算就很复杂,换成.final-box-title效率就快很多。 - 减少需要执行样式计算的元素的个数。
- 使用timeline面板评估样式计算成本。
如果有低于60fps的长帧或者很高的紫色柱状图,则可能是样式计算有问题。
避免大规模、复杂的布局
布局:就是浏览器计算DOM元素的几何信息的过程:元素大小和在页面中的位置。每个元素都有一个显式或隐式的大小信息。
Blink/WebKit和IE中,称为layout。在Gecko中,称为Reflow.
需要布局的DOM元素的数量直接影响到性能;应该尽可能避免触发布局。
- 尽可能避免触发布局。
对于DOM元素的“几何属性”的修改,比如width/height/left/top等,都需要重新计算布局。
几乎所有的布局都是在整个文档范围内发生的。 - 使用flexbox替代老的布局模型。
新的Flexbox比旧的Flexbox和基于浮动的布局模型更高效。 - 避免强制同步布局事件的发生。
将一帧画面渲染到屏幕上的处理顺序为: 执行js—>样式计算—>布局—>Paint—>Composite,
在执行js时,获取到的样式属性值,都是上一帧画面的。
但是如果对样式属性,先赋值,再读取,浏览器为了返回正确的属性值,要执行一次布局。
对样式操作应当遵循先读后写的原则,避免同步布局。 - 避免快速连续的布局。
有可能触发连续多次的同步布局。
简化绘制的复杂度、减小绘制区域
绘制: 填充像素的过程。这些像素将最终显示在用户的屏幕上。
一般情况下,绘制是整个渲染流水线中代价最高的环节,要尽可能避免它。
除了transform和opacity之外,修改任何属性都会触发绘制。
- 提升移动或渐变元素的绘制层。
必要时,绘制会分为多层,最终再合成一层显示在屏幕上。
例如: 应用transforms属性的元素,会被单层上单独绘制,从而不触发对其他元素的绘制。
使用will-change,结合transform,可以创建一个新的组合层: .moving-element { will-change: transform; }
使用transform: translateZ(0);也可以强制浏览器创建一个新的渲染层。
适当的创建渲染层。因为每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。 - 减少绘制区域。
浏览器会把两个相邻区域的渲染任务合并在一起进行,这将导致整个屏幕区域都会被绘制。 - 简化绘制的复杂度。
比如blur效果比其他绘制效果更费时。 - 使用Chrome DevTools来定位绘制过程中的性能瓶颈。
在setting-rendering选中Show paint rectangles,查看绘制区域。
timeline的recoding选中Paint,在Detail信息处,有更多关于绘制的信息。
优先使用渲染层合并属性、控制层数量
- 使用transform/opacity实现动画效果。
transforms/opacity属性的元素必须独占一个渲染层。为了对这个元素创建一个自有的渲染层,必须提升该元素。 - 提升动画效果中的元素。
使用 will-change: transform; 或者 transform: translateZ(0); 把动画效果中的元素提升到其自有的渲染层中(但不要滥用)。 - 管理渲染层、避免过多数量的层。
当且仅当需要的时候才为元素创建渲染层。 - 使用Chrome DevTools来了解页面中的渲染层的情况。
recording选中Paint,
在FPS横条上点击每一个单独的帧,看到每个帧的渲染细节:
在Detail部分,会有layers选项卡,在该选项卡中,可以对这一帧中的所有渲染层进行扫描、缩放等操作,同时还能看到每个渲染层被创建的原因。
对频繁触发的事件回调去抖动(Debounce)
- 避免运行时间过长的事件处理函数。
- 避免在事件处理函数中修改样式属性。
输入事件处理函数,比如scroll/touch,都会在requestAnimationFrame之前被调用执行。
所以如果在事件处理函数中,修改了样式,则修改会被标记起来(未渲染),然后再调用raf,如果在raf中又读取了样式属性,则会触发同步布局过程。 - 对事件处理去抖动处理。
window.performance.timing
可以借助该全局对象,对当前页面进行简单的性能评估。以下是一些常用的属性。
- domLoading:这是整个过程开始的时间戳,浏览器开始解析 HTML 文档第一批收到的字节 document.
- domInteractive:标记 DOM 准备就绪。
- domContentLoaded:通常标记 [DOM 和 CSSOM 都准备就绪] 的时间
标记 DOM 准备就绪并且没有样式表阻碍 JavaScript 执行的时间点 - 意味着我们可以开始构建呈现树了。
很多 JavaScript 框架等待此事件发生后,才开始执行它们自己的逻辑。因此,浏览器会通过捕获 EventStart 和 EventEnd 时间戳,跟踪执行逻辑所需的时间。 - domComplete: 标记网页及其所有附属资源都已经准备就绪的时间。包括网页上所有资源(图片等) 下载完成 - 即加载旋转图标停止旋转。
- loadEvent:作为每个网页加载的最后一步,浏览器会触发onLoad事件,以便触发附加的应用逻辑。