浏览器是如何工作的

2023-07-11 pv

1. 背景

对于前端开发者来说,浏览器的底层原理通常不需要有太深入的了解。因为现代浏览器已经将所有的底层细节实现了很好的封装,前端开发通常只需要关注HTML+CSS+JavaScript这三样东西。

在软件工程领域,封装(encapsulation)是降低系统复杂度的一种常用手段。这对于开发者而言是件好事:只需要关心接口的使用方式,无需了解底层细节。这样开发的门槛就降低了。

但是伴随开发水平提升,逐渐就会遇到瓶颈。因为浏览器构建出的沙盒环境(sandbox)固然美好,但真实世界毕竟不是如此简单,如果能深入进去,了解浏览器的运行逻辑,就会更加清楚地知道为什么会出现这样的结果,为什么这样写会导致性能问题等。当懂得了这一切后,写起代码来,自然胸有成竹,韬略尽在眼中。

浏览器是个复杂的领域,像 Chromium,源码达到千万行级别,和一个操作系统差不多。不过好在绝大多数开发者并不需要亲自开发浏览器,因此不需要深入了解每一行的代码运行,只要从宏观上把握大致原理即可。

这也是程序员学习的技巧之一:技术上了解到合适的深度即可。否则卷帙浩繁,一辈子都学不完。

How browsers work🔗 How browsers work Mozilla 官网上一篇很经典的文章,文字不多,核心的几块,主要是渲染都涉及到了。美中不足的是缺少网络,以及浏览器整体架构的介绍,这个在后面的文章中会补充。

2. 开始

整个过程,将从在地址栏输入google.com开始,然后以浏览器上渲染出 Google 网页结束。看似简单的过程,背后却发生了非常多工作。

3. 主要功能

浏览器的主要功能,就是将用户请求的资源,从服务器获取后,展示到屏幕上。

这里的资源可以是 HTML,也可以是 PDF,每个资源都有一个独一无二的标识符,URI(Uniform Resource Identifier)。

浏览器所能理解得资源,例如 HTML 和 CSS,遵循一个规范,这个规范主要由 W3C(World Wide Web Consortium)制定。

规范的好处在于,相同一份资源,在不同的浏览器上会以相同的方式处理,这样就减少了很多兼容性的问题。

4. 组织结构

一个浏览器,大致可分为 7 个部分。

  1. 用户界面。包括地址栏,前进/后退按钮,书签等。
  2. 浏览器引擎。响应用户的操作。
  3. 渲染引擎。负责将网络请求的内容展示,涉及 HTML 和 CSS 的解析,渲染等。
  4. 网络。负责网络请求,例如 HTTP。
  5. UI。窗口绘制。
  6. JavaScript 解释器。解析和执行 JavaScript 代码。
  7. 数据存储。例如,IndexDB、WebSQL 等。

5. 渲染引擎如何工作

不同浏览器使用不同的渲染引擎,例如 IE 使用 Trident,Firefox 使用 Gecko,Safari 使用 WebKit,Chrome,Edge 和 Opera 都基于 Chromium,使用 WebKit 的一个 Fork 版本,Blink。

渲染引擎从网络获取到数据后,开始了一场漫长的奇妙之旅。文本格式的数据,经过一步步的处理,将会成为显示在屏幕上的酷炫网页。

基本的流程如下:

  1. 解析 HTML,构建 DOM 树
  2. 构建 Render 树
  3. Render 树布局
  4. 绘制 Render 树
5.1 Parser

HTML 的 parser,并不符合上下文无关语法(Context Free Grammar),因为用户的输入很有可能不符合规范,因此渲染引擎做了很多容错。因此无论 HTML 写成什么样,渲染引擎都不会 crash。

浏览器总是会尽可能的将内容呈现。

解析的过程,有完整的规范:Parsing HTML documents🔗

与 HTML 不同,CSS 符合上下文无关语法,也有定义的规范:Grammar of CSS 2.1🔗 。语法用 BNF 描述。这就有一系列工具可以使用,例如 WebKit 使用 Flex 和 Bison 从语法文件中自动创建解析器。

另外,在 parse 过程中也存在一些值得优化的地方。

比如,在解析过程中,遇到<script>标签,解析器会暂停,直到 script 被执行。如果引用的是外部脚本,那么必须先从网络中获,这个过程,默认是同步的。这会大大降低解析器的效率。

这也是为什么,<script>通常被建议使用在<body>尾部,否则会影响解析效率。

不过现在有了更好的方案,在<script>里增加defer属性,这样 script 的执行就是异步的,不会阻塞解析,同时会在 DOM 准备好后执行,并且保证在DOMContentLoaded前。

不仅如此,现代浏览器还会做更多的优化,例如推测性解析(Speculative parsing),利用多线程,加速解析的过程,但只针对外部资源。

5.2 Render

Render 的过程,也是通过构建一棵树实现。

WebKit 中的主要数据结构 RenderObject 这么定义:

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
}

Render Tree 和 DOM Tree 存在对应关系,但并非所有的 DOM 节点都挂载到 Render Tree 上,只有那些可见的才会被插入。

每个 Render 节点都有一系列丰富的样式,如何计算是个相当复杂的过程,在渲染引擎里有专门的样式计算逻辑。

主要的难点有三个:

  1. 属性相关的数据很多
  2. 在诸多样式里找到最匹配的那个,需要复杂的寻找算法,尤其对于结构复杂的选择器而言尤其如此(不要写太复杂的选择器)
  3. 实现 CSS 定义的复杂级联规则
5.3 Layout

当 Render Tree 被创建好后,还并没有位置和大小信息。计算这些值的过程,叫做布局(Layout)。

HTML 使用了一种基于流(Flow)的布局模型,这意味着多数情况下,几何形状可以一次性(Single Pass)计算出。流里后出现的元素会影响之前出现的,因此布局处理的过程,自左向右,自顶向下。

为了避免微小的改动造成整个布局的重新计算,浏览器使用了一种“脏位(dirty bit)”的思想:布局存在改变的节点被标记为脏,脏存在两种类型:节点自身为脏,子节点为脏。因此在后续计算中,只有标记为脏的节点会被重新计算。这种影响布局的方式就是增量布局。

还有一种全量布局,例如 font size 改变,在窗口 resize 的时候发生。

对于增量布局而言,这个过程是异步的,Firefox 会把增量布局累积到队列里然后批量执行,WebKit 也存在类似的计时器。

5.4 Paint

在绘制阶段,整个 Render Tree 被遍历,并触发*paint()*方法,借助 UI 组件实现绘制。

至此,页面的解析,构建,布局,绘制的过程就完成了。

6. 总结

这篇博客主要解释了渲染引擎的工作原理,这是离用户最近,但同时也是离前端开发者最远的一块。

渲染引擎很重要,但对于现在浏览器而言,也不过只是其中的一个组件。还有多进程架构,进程间通信,网络组件等等。

足可见浏览器的复杂。

在后续文章中会逐步谈及。

(完)

参考

在 GitHub 上编辑本页面

最后更新于: 2024-11-20T09:44:17+08:00