浏览器是如何工作的
2023-07-11
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 个部分。
- 用户界面。包括地址栏,前进/后退按钮,书签等。
- 浏览器引擎。响应用户的操作。
- 渲染引擎。负责将网络请求的内容展示,涉及 HTML 和 CSS 的解析,渲染等。
- 网络。负责网络请求,例如 HTTP。
- UI。窗口绘制。
- JavaScript 解释器。解析和执行 JavaScript 代码。
- 数据存储。例如,IndexDB、WebSQL 等。
5. 渲染引擎如何工作
不同浏览器使用不同的渲染引擎,例如 IE 使用 Trident,Firefox 使用 Gecko,Safari 使用 WebKit,Chrome,Edge 和 Opera 都基于 Chromium,使用 WebKit 的一个 Fork 版本,Blink。
渲染引擎从网络获取到数据后,开始了一场漫长的奇妙之旅。文本格式的数据,经过一步步的处理,将会成为显示在屏幕上的酷炫网页。
基本的流程如下:
- 解析 HTML,构建 DOM 树
- 构建 Render 树
- Render 树布局
- 绘制 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 这么定义:
Render Tree 和 DOM Tree 存在对应关系,但并非所有的 DOM 节点都挂载到 Render Tree 上,只有那些可见的才会被插入。
每个 Render 节点都有一系列丰富的样式,如何计算是个相当复杂的过程,在渲染引擎里有专门的样式计算逻辑。
主要的难点有三个:
- 属性相关的数据很多
- 在诸多样式里找到最匹配的那个,需要复杂的寻找算法,尤其对于结构复杂的选择器而言尤其如此(不要写太复杂的选择器)
- 实现 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. 总结
这篇博客主要解释了渲染引擎的工作原理,这是离用户最近,但同时也是离前端开发者最远的一块。
渲染引擎很重要,但对于现在浏览器而言,也不过只是其中的一个组件。还有多进程架构,进程间通信,网络组件等等。
足可见浏览器的复杂。
在后续文章中会逐步谈及。
(完)
参考
- 本文作者:Plantree
- 本文链接:https://plantree.me/blog/2023/how-browsers-work/
- 版权声明:所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
最后更新于: 2024-11-20T09:44:17+08:00