浏览器内部机制

Posted by Rimin on 2019-03-04

浏览器解释并显示 HTML 文件的方式是在 HTML 和 CSS 规范中指定的。这些规范由网络标准化组织 W3C(万维网联盟)进行维护。

浏览器的用户界面并没有任何正式的规范,这是多年来的最佳实践自然发展以及彼此之间相互模仿的结果。

浏览器的组成

  1. 用户界面 : 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
  2. 浏览器引擎: 在用户界面和呈现引擎之间传送指令。
  3. 呈现引擎: 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  4. 网络: 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
  5. 用户界面后端: 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
  6. JavaScript 解释器: 用于解析和执行 JavaScript 代码。
  7. 数据存储: 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。

本篇重点讲述 呈现引擎 的一些规则

呈现流程(呈现引擎)

1
2
3
4
5
6
1. 处理 HTML 标记,构建 DOM 树。(字节数据-字符串-Token-Node-Dom)
2. 处理 CSS 标记,构建 CSSOM 树。(与上诉构建DOM树并行)(字节数据-字符串-Token-Node-CSSOM)
3. 将 DOM 树和 CSSOM 树融合成渲染树 render tree。
4. 根据渲染树来布局layout(回流reflow),计算每个节点的几何信息。
5. 在屏幕上绘制painting(重绘)各个节点。
6. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

image

⚠️ css文件不会阻塞html的解析,但是会阻塞html的渲染, 而js会阻塞解析。

虽然浏览器内部十分复杂,对于呈现来说, HTML 和 CSS 解析器相对于一般的语法解析还是不一样的。

  • HTML 解析器

HTML 和 XML 非常相似。有很多 XML 解析器可以使用。HTML 存在一个 XML 变体 (XHTML),那么有什么大的区别呢?区别在于 HTML 的处理更为“宽容”,它允许您省略某些隐式添加的标记,有时还能省略一些起始或者结束标记等等。和 XML 严格的语法不同,HTML 整体来看是一种“软性”的语法。
简单看一下HTML的解析算法:

比如:

1
2
3
4
5
<html>
<body>
Hello world
</body>
</html>

初始状态是数据状态。遇到字符 < 时,状态更改为“标记打开状态”。接收一个 a-z 字符会创建“起始标记”,状态更改为“标记名称状态”。这个状态会一直保持到接收 > 字符。在此期间接收的每个字符都会附加到新的标记名称上。在本例中,我们创建的标记是 html 标记。

遇到 > 标记时,会发送当前的标记,状态改回“数据状态”。 标记也会进行同样的处理。目前 html 和 body 标记均已发出。现在我们回到“数据状态”。接收到 Hello world 中的 H 字符时,将创建并发送字符标记,直到接收 中的 <。我们将为 Hello world 中的每个字符都发送一个字符标记。

现在我们回到“标记打开状态”。接收下一个输入字符 / 时,会创建 end tag token 并改为“标记名称状态”。我们会再次保持这个状态,直到接收 >。然后将发送新的标记,并回到“数据状态”。 输入也会进行同样的处理。

  • CSS树的构建

和 HTML 不同,CSS 是上下文无关的语法,可以使用简介中描述的各种解析器进行解析。

  • 呈现树的构建

WebKits RenderObject 类是所有呈现器的基类

1
2
3
4
5
6
7
8
class RenderObject {
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}

下面这段 WebKit 代码描述了根据 display 属性的不同,针对同一个 DOM 节点应创建什么类型的呈现器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;

switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}

return o;
}

所以这就解释了为什么当 display: none;时, 元素是不占物理空间的。

image

reflow AND repaint

reflow(回流)

浏览器为了重新渲染部分或整个页面,重新计算页面元素位置和几何结构的进程叫做reflow

  • 什么时候会导致reflow发生呢?
1
2
3
4
5
6
7
8
9
- 改变窗口大小
- 改变文字大小
- 添加/删除样式表
- 内容的改变,(用户在输入框中写入内容也会)
- 激活伪类,如:hover
- 操作class属性
- 脚本操作DOM
- 计算offsetWidth和offsetHeight
- 设置style属性
1
2
3
🌝🌝插播一个问题 :如何插入几万个 DOM,如何实现页面不卡顿?
1. requestAnimationFrame 的方式去循环的插入 DOM
2. 虚拟滚动(virtualized scroller)只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。
  • reflow优化建议
1
2
3
4
5
6
7
8
9
- 不要一条一条地修改 DOM 的样式,预先定义好 class,然后修改 DOM 的 className
- 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改100次,然后再把它显示出来
- 尽可能不要修改影响范围比较大的 DOM
- 为动画的元素使用绝对定位 absolute / fixed
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 避免设置大量的style属性,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow,所以最好是使用class属性
- 如果CSS里面有计算表达式,每次都会重新计算一遍,出发一次reflow
- CSS 选择符从**右往左**匹配查找,避免节点层级过多
- 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
repaint

屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变。是在一个元素的外观被改变,但没有改变布局的情况下发生的当repaint发生时,浏览器会验证DOM树上所有其他节点的visibility 属性。

常见的重绘元素:
color border-style visibility background text-decoration background-image background-position background-repeat outline-color border-radius box-shadow

CSS高消耗属性

1
2
3
4
5
- box-shadows
- border-radius
- transparency
- transforms
- CSS filters(性能杀手)

CSS2 规范定义了绘制流程的顺序。绘制的顺序其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。块呈现器的堆栈顺序如下:

1
2
3
4
5
1. 背景颜色
2. 背景图片
3. 边框
4. 子代
5. 轮廓

事件执行机制相关

  • 当 Event loop 执行完 Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
    然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
  • 判断是否触发了 media query
  • 更新动画并且发送事件
  • 判断是否有全屏操作事件
  • 执行 requestAnimationFrame回调
  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
  • 更新界面
    以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。

最后: 还是抛出一个常见的问题:

Q: 当浏览器输入URL后发生了什么?

A:
image