浏览器主要组件

浏览器主要组件

用户界面、浏览器引擎、呈现引擎、网络 、用户界面后端、JavaScript解释器、数据存储。
这些组件都人如其名,就不详细介绍了,其中浏览器引擎,提供了对呈现引擎的更高级别的封装。

下面就介绍渲染引擎的相关内容。

渲染引擎

Rendering Engine, 或称为 呈现引擎。

负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。

各个呈现引擎对应的主要浏览器:

不同的呈现引擎在实现上都有不同,但是大致流程都是一致的。

主流程

呈现引擎一开始会从网络层获取请求文档的内容,内容的大小一般限制在 8000 个块以内。

然后进行如下所示的基本流程:

主流程

note:
1.这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来。
2.解析html,生成内容树,该树不完全是DOM树,该树包含DOM节点。

主流程示例

Webkit:

主流程-webkit

Gecko:

主流程-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 方法。

绘制

把渲染树中的每个节点转换为屏幕上的实际像素 - 称为「绘制」或者「栅格化」。

绘制顺序:https://www.w3.org/TR/CSS21/zindex.html (背景颜色、背景图片、边框、子代、轮廓)

加载javascript

内联脚本: HTML 解析器遇到一个 script 标签,它会暂停构建 DOM,并移交控制权给 JavaScript 引擎;等 JavaScript 引擎执行完毕,浏览器从中断的地方恢复 DOM 构建。

脚本文件: 浏览器必须暂停,然后等待脚本从磁盘、缓存或远程服务器中取回.

script标签添加async属性,异步执行和加载js。

CSSOM 准备就绪前,JavaScript 执行被延后。

note:

优化呈现路径

我们来定义将用于描述关键呈现路径的词汇:

所以,优化关键呈现路径常规步骤:

优化首次呈现的规则和建议:

60fps和设备刷新率

当今大多数设备的屏幕刷新率都是 60次/秒 。因此,浏览器渲染动画或页面的每一帧的速率,也需要跟设备屏幕的刷新率保持一致。才会显得动画流畅。

也就是说,浏览器对每一帧画面的渲染工作需要在16毫秒(1秒 / 60 = 16.66毫秒)之内完成。但实际上,在渲染某一帧画面的同时,浏览器还有一些额外的工作要做(比如渲染队列的管理,渲染线程与其他线程之间的切换等等)。因此单纯的渲染工作,一般需要控制在10毫秒之内完成,才能达到流畅的视觉效果。

使用requestAnimationFrame方法执行逐帧动画。回调函数会传入一个时间戳作为参数。

requestAnimationFrame返回一个id,cancelAnimationFrame使用该ID,取消注册的任务。

raf的回调在下一帧重绘前调用,调用频率为每秒60次或者和浏览器频率同步。

如果当前页面是个后台页面没有展示时,会减少raf执行频率或者不执行。

重绘渲染流水线

一般渲染流程包含以下5个关键步骤:

重绘

note: 绘制过程本身包含两步:

  1. 创建一系列draw调用;
  2. 填充像素。 第二步的过程被称作 "rasterization” 。

因此当你在DevTools中查看页面的paint记录时,你可以认为它已经包含了 rasterization。

在实际渲染中,根据变化的样式不同或访问元素样式属性的不同,对视觉变化效果的一个帧的渲染,有以下三种情况:

  1. JS / CSS > 计算样式 > 布局 > 绘制 > 渲染层合并
    重绘-1
    如果你修改一个DOM元素的”layout”属性,也就是改变了元素的样式(比如宽度、高度或者位置等),
    那么浏览器会检查哪些元素需要重新布局,然后对页面激发一个reflow过程完成重新布局。
  2. JS / CSS > 计算样式 > 绘制 > 渲染层合并
    重绘-2
    如果你修改一个DOM元素的“paint only”属性,比如背景图片、文字颜色或阴影等,这些属性不会影响页面的布局,因此浏览器会在完成样式计算之后,跳过布局过程,只做绘制和渲染层合并过程。
  3. JS / CSS > 计算样式 > 渲染层合并
    重绘-3
    如果你修改一个非样式且非绘制的CSS属性,那么浏览器会在完成样式计算之后,跳过布局和绘制的过程,直接做渲染层合并。
    目前只有transforms和opacity两个属性。

优化渲染性能

优化JavaScript的执行效率

降低样式计算的范围和复杂度

样式计算: 添加或移除一个DOM元素、修改元素属性和样式类、应用动画效果等操作,都会引起DOM结构的改变,从而导致浏览器需要重新计算每个元素的样式、对页面或其一部分重新布局(多数情况下)。

  1. 降低样式选择器的复杂。
    例如.box:nth-last-child(-n+1) .title 这样的选择器,
    样式是从右向左匹配的, 从而确定一个dom元素是否匹配该样式,要1)检查是否有title类,2)是否有一个父元素,且具有.box类,3)父元素是第(-n+1)个子元素。 这个计算就很复杂,换成.final-box-title效率就快很多。
  2. 减少需要执行样式计算的元素的个数。
  3. 使用timeline面板评估样式计算成本。
    如果有低于60fps的长帧或者很高的紫色柱状图,则可能是样式计算有问题。

避免大规模、复杂的布局

布局:就是浏览器计算DOM元素的几何信息的过程:元素大小和在页面中的位置。每个元素都有一个显式或隐式的大小信息。

Blink/WebKit和IE中,称为layout。在Gecko中,称为Reflow.

需要布局的DOM元素的数量直接影响到性能;应该尽可能避免触发布局。

  1. 尽可能避免触发布局。
    对于DOM元素的“几何属性”的修改,比如width/height/left/top等,都需要重新计算布局。
    几乎所有的布局都是在整个文档范围内发生的。
  2. 使用flexbox替代老的布局模型。
    新的Flexbox比旧的Flexbox和基于浮动的布局模型更高效。
  3. 避免强制同步布局事件的发生。
    将一帧画面渲染到屏幕上的处理顺序为: 执行js—>样式计算—>布局—>Paint—>Composite,
    在执行js时,获取到的样式属性值,都是上一帧画面的。
    但是如果对样式属性,先赋值,再读取,浏览器为了返回正确的属性值,要执行一次布局。
    对样式操作应当遵循先读后写的原则,避免同步布局。
  4. 避免快速连续的布局。
    有可能触发连续多次的同步布局。

简化绘制的复杂度、减小绘制区域

绘制: 填充像素的过程。这些像素将最终显示在用户的屏幕上。

一般情况下,绘制是整个渲染流水线中代价最高的环节,要尽可能避免它。

除了transform和opacity之外,修改任何属性都会触发绘制。

  1. 提升移动或渐变元素的绘制层。
    必要时,绘制会分为多层,最终再合成一层显示在屏幕上。
    例如: 应用transforms属性的元素,会被单层上单独绘制,从而不触发对其他元素的绘制。
    使用will-change,结合transform,可以创建一个新的组合层: .moving-element { will-change: transform; }
    使用transform: translateZ(0);也可以强制浏览器创建一个新的渲染层。
    适当的创建渲染层。因为每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。
  2. 减少绘制区域。
    浏览器会把两个相邻区域的渲染任务合并在一起进行,这将导致整个屏幕区域都会被绘制。
  3. 简化绘制的复杂度。
    比如blur效果比其他绘制效果更费时。
  4. 使用Chrome DevTools来定位绘制过程中的性能瓶颈。
    在setting-rendering选中Show paint rectangles,查看绘制区域。
    timeline的recoding选中Paint,在Detail信息处,有更多关于绘制的信息。

优先使用渲染层合并属性、控制层数量

  1. 使用transform/opacity实现动画效果。
    transforms/opacity属性的元素必须独占一个渲染层。为了对这个元素创建一个自有的渲染层,必须提升该元素。
  2. 提升动画效果中的元素。
    使用 will-change: transform; 或者 transform: translateZ(0); 把动画效果中的元素提升到其自有的渲染层中(但不要滥用)。
  3. 管理渲染层、避免过多数量的层。
    当且仅当需要的时候才为元素创建渲染层。
  4. 使用Chrome DevTools来了解页面中的渲染层的情况。
    recording选中Paint,
    在FPS横条上点击每一个单独的帧,看到每个帧的渲染细节:
    在Detail部分,会有layers选项卡,在该选项卡中,可以对这一帧中的所有渲染层进行扫描、缩放等操作,同时还能看到每个渲染层被创建的原因。

对频繁触发的事件回调去抖动(Debounce)

  1. 避免运行时间过长的事件处理函数。
  2. 避免在事件处理函数中修改样式属性。
    输入事件处理函数,比如scroll/touch,都会在requestAnimationFrame之前被调用执行。
    所以如果在事件处理函数中,修改了样式,则修改会被标记起来(未渲染),然后再调用raf,如果在raf中又读取了样式属性,则会触发同步布局过程。
  3. 对事件处理去抖动处理。

window.performance.timing

可以借助该全局对象,对当前页面进行简单的性能评估。以下是一些常用的属性。