浏览器解析渲染HTML文档的过程

38次阅读

共计 5500 个字符,预计需要花费 14 分钟才能阅读完成。

参考资料

浏览器的工作原理
浏览器加载网页时的过程是什么?
HTML 渲染过程详解

浏览器的工作原理
一、浏览器的高层结构
浏览器的主要组件为:

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

值得注意的是,和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个呈现引擎实例。每个标签页都是一个独立的进程。
二、主流程
呈现引擎一开始会从网络层获取请求文档的内容,内容的大小一般限制在 8000 个块以内。
然后进行如下所示的基本流程:

呈现引擎将开始解析 HTML 文档,并将各标记逐个转化成“内容树”上的 DOM 节点。同时也会解析外部 CSS 文件以及样式元素中的样式数据。HTML 中这些带有视觉指令的样式信息将用于创建另一个树结构:呈现树。
呈现树包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。
呈现树构建完毕之后,进入“布局”处理阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标。下一个阶段是绘制 – 呈现引擎会遍历呈现树,由用户界面后端层将每个节点绘制出来。
需要着重指出的是,这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来。
主流程示例:
三、处理脚本和样式表的顺序

脚本网络的模型是同步的。网页作者希望解析器遇到 <script> 标记时立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。此模型已经使用了多年,也在 HTML4 和 HTML5 规范中进行了指定。作者也可以将脚本标注为“defer”,这样它就不会停止文档解析,而是等到解析结束才执行。HTML5 增加了一个选项,可将脚本标记为异步,以便由其他线程解析和执行。
预解析 WebKit 和 Firefox 都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
样式表另一方面,样式表有着不同的模型。理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。
呈现树构建在 DOM 树构建的同时,浏览器还会构建另一个树结构:呈现树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。

Firefox 将呈现树中的元素称为“框架”。WebKit 使用的术语是呈现器或呈现对象。呈现器知道如何布局并将自身及其子元素绘制出来。
四、布局
呈现器在创建完成并添加到呈现树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。
HTML 采用基于流的布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。但是也有例外情况,比如 HTML 表格的计算就需要不止一次的遍历。
坐标系是相对于根框架而建立的,使用的是上坐标和左坐标。
布局是一个递归的过程。它从根呈现器(对应于 HTML 文档的 <html> 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的呈现器计算几何信息。
根呈现器的位置左边是 0,0,其尺寸为视口(也就是浏览器窗口的可见区域)。所有的呈现器都有一个“layout”或者“reflow”方法,每一个呈现器都会调用其需要进行布局的子代的 layout 方法。
五、绘制
在绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。绘制工作是使用用户界面基础组件完成的。
个人理解总结
一、解析器与预解析机制
呈现引擎从网络层获取请求文档的内容,然后开始解析 HTML 文档,并将各标记逐个转化为 DOM 树(内容树)上的 DOM 节点,同时也会解析外部 CSS 文件以及样式元素中的样式数据。HTML 中这些带有视觉指令的样式信息将用于创建另一个树结构:渲染树(呈现树)。呈现树构建完毕之后,呈现引擎将对呈现树进行布局和绘制。
呈现引擎 的解析包括 HTML 解析和 CSS 解析,HTML 解析器的输出“解析树”是由 DOM 元素和属性节点构成的树结构,DOM 是文档对象模型 (Document Object Model) 的缩写。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。解析树的根节点是“Document”对象。CSS 解析器会将 CSS 样式文件和样式元素中的样式数据解析为 CSS 规则树,浏览器结合 CSS 规则树和 DOM 树生成渲染树。
JavaScript 解释器 用于解析和执行 JavaScript 代码。
一般来讲,我们认为浏览器从网络层接收到 HTML 文档内容,然后开始解析文档生成 DOM 树,遇到 CSS 样式表标签或 JS 脚本标签就起新线程去下载它们,并继续构建 DOM 树,浏览器根据 DOM 树构建渲染树,最后浏览器将渲染书绘制到用户界面。
在上述描述中,需要着重指出的是,HTML 文档的解析和渲染是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来。
浏览器的预解析。WebKit 和 Firefox 都进行了这项优化。在执行脚本时,其他线程会解析 HTML 文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
浏览器的预解析可以减缓渲染被阻塞的情况,例如文档解析过程中预加载器发现了 <script src=”last.js”></script> 标签,会对 last.js 文件进行加载并放在浏览器缓存中,这样当解析器遇到这个 <script> 标记时,由于预加载器已经将 last.js 文件加载下来了,所以 last.js 会被立即执行,不需要等待从网络抓取资源,减缓了对渲染的阻塞。
二、CSS 和 JS 的处理顺序和阻塞分析
HTML 文档的解析和渲染过程中,外部样式表和脚本 顺序执行、并发加载。
JS 脚本会阻塞 HTML 文档的解析,包括 DOM 树的构建和渲染树的构建;CSS 样式表会阻塞渲染树的构建,但 DOM 树依然继续构建(除非遇到 script 标签且 css 文件此时仍未加载完成),但不会渲染绘制到页面上。
在 HTML 文档的解析过程中,解析器遇到 <script> 标记时会立即解析并执行脚本,HTML 文档的解析将被阻塞,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络抓取资源并解析和执行完成后,再继续解析后续内容。
理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。
但无论是哪种情况导致的阻塞,该加载的外部资源还是会加载,例如外部脚本、样式表和图片。HTML 文档的解析可能会被阻塞,但外部资源的加载不会被阻塞。
CSS 外部样式表的加载会阻塞外部脚本的执行,但并不会阻塞外部脚本的加载。这一点可以通过 chrome 调试工具中的 Network – Waterfall 进行验证,但是需要注意 chrome 的并发连接数(同一域名)上限为 6 个。

由上面两张截图可以看到,jquery.min.js 脚本文件与 bootstrap.css 等样式文件并行加载,但是由于 chrome 的并发连接数上限为 6 个,因此 bootstrap.min.js 脚本、xxx.css 样式等文件的加载会等待前面的文件加载完成,有可用连接数的时候才开始加载。

了解以上信息之后,我们可以对该页面进行相应优化,例如对 CSS 文件进行压缩处理、使用 CDN,将资源分布在多个域名下、合并 CSS 文件,减少 HTTP 请求数量等,来提高 CSS 的加载速度,减少 HTML 文档解析和渲染的阻塞时间。
browser only allows six TCP connections per origin on HTTP 1.
浏览器的并发请求数目限制是针对同一域名的。因此可以使用 CDN 加速技术来提高用户访问网站的响应速度,这样使用了 CDN 的资源加载不会占用当前域名下的并发连接数,从而减少阻塞的时间。
网页性能
了解 HTML 文档的解析和渲染的过程对于分析网页性能有着重要意义,它可以帮助我们找到影响网页性能的关键因素。例如,我们知道 JS 外部脚本的执行会阻塞文档的解析,那么重量级的第三方插件则会影响首页加载的速度,如果因此影响到了用户体验,我们就需要考虑这个第三方插件的使用成本是不是太高了,能否使用其他轻量级的插件进行替代,或者只使用其中一部分模块。
以 Datatables 为例:

上图是一个项目页面的 Network 截图,红色框中的部分出现了约 700ms 左右的空白,我们需要知道为什么页面的加载会出现这样的情况,这段空白时间浏览器在干什么?
我们分析 Timeline 图,看看浏览器在这段时间的具体信息,如下:

通过 Timeline 图我们可以看到,在 250ms~900ms 时间区间内,浏览器在执行 datatables.min.js 脚本代码,这个脚本的执行阻塞了文档的解析,耗时约 700ms,对应了 Network 图中的空白。
我们继续查看页面总的耗时时间,评估 700ms 耗时的影响,如下:

可以看到,页面总的完成耗时为 1.66s,由此可知 datatables.min.js 的执行耗时占了很大比重,应当慎重考虑是否一定要使用这个插件,能否使用其他轻量级的插件进行替代,或者能否精简插件的不必要模块,或者舍弃插件的使用。
参考资料 - 1 浏览器接收到 html 代码,可能是一份完整的文档,也可能是一个 chunk,即开始解析。解析过程是先构建 dom 树,再根据 dom 树构建渲染树,最后浏览器将渲染树绘制到页面上。构建 dom 树的过程即根据 html 代码自上而下进行构建,当遇到 script 文件加载/执行会阻塞后面 dom 树的构建(javascript 可能会改变 dom 树),而遇到 css 文件则会阻塞渲染树的构建,即 dom 树依然继续构建(除非遇到 script 标签并且 css 文件依旧未加载完成),但不会渲染绘制到页面上。而无论哪个阻塞,该加载的文件还是会加载,例如 html 文档中的其他 css/js/图片文件。另外 javascript 被加载后就会被执行,执行的过程也阻塞树的构建。是执行完了才解析其他内容,而不是执行完了才加载其他内容。
作者:加冰链接:https://www.zhihu.com/questio…
参考资料 - 2 首先,开源浏览器一般以 8k 每块下载 html 页面。然后解析页面生成 DOM 树,遇到 css 标签或 JS 脚本标签就新起线程去下载他们,并继续构建 DOM。下载完后解析 CSS 为 CSS 规则树,浏览器结合 CSS 规则树和 DOM 树生成 Render Tree。注意:构建 CSS Object Model(CSSOM) 会阻塞 JavaScript 的执行。JavaScript 的执行也会阻塞 DOM 的构建。JavaScript 下载后可以通过 DOM API 修改 DOM,通过 CSSOM API 修改样式作用域 Render Tree。每次修改会造成 Render Tree 的重新布局和重绘。只要修改 DOM 或修改了元素的形状或大小,就会触发 Reflow,单纯修改元素的颜色只需 Repaint 一下(调用操作系统 Native GUI 的 API 绘制)。
作者:陈金链接:https://www.zhihu.com/questio…

正文完
 0