「前端那些事儿」① 浏览器渲染引擎

5,917 阅读13分钟

前言

浏览器基础是前端知识网中的一个小分支,也是前端开发人员必须掌握的基础知识点。他贯穿着前端的整个网络体系,项目优化也是围绕着浏览器进行的。

开发人员在面试的时候或许会被问到:

从你在浏览器输入一个网址到网页内容完全被展示的这段时间内,都发生了什么事情?

确实是个老生常谈的问题,但问题的答案并不是唯一的,或许在三五年前,这个问题还会有一个「相对」标准的答案。

  1. 浏览器在接收到这个指令时,会开启一个单独的线程来处理这个指令,首先要判断用户输入的是否为合法或合理的 URL 地址,是否为 HTTP 协议请求,如果是那就进入下一步
  2. 浏览器的浏览器引擎将对此 URL 进行分析,如果存在缓存「cache-control」且未过期,则会从本地缓存提取文件(From Memory Cache,200返回码),如果缓存「cache-control」不存在或过期,浏览器将发起远程请求
  3. 通过 DNS 解析域名获取该网站地址对应的 IP 地址,连同浏览器的 Cookie、 userAgent 等信息向此 IP 发出 GET 请求。
  4. 接下来就是经典的「三次握手」,HTTP 协议会话,浏览器客户端向 Web 服务器发送报文,进行通讯和数据传输。
  5. 进入网站的后端服务,如 Tomcat、Apache 等,还有近几年流行的 Node.js 服务器,这些服务器上部署着应用代码,语言有很多,如 Java、 PHP、 C++、 C# 和 Javascript 等。
  6. 服务器根据 URL 执行相应的后端应用逻辑,期间会使用到「服务器缓存」或「数据库」。
  7. 服务器处理请求并返回响应报文,如果浏览器访问过该页面,缓存上有对应资源,与服务器最后修改记录对比,一致则返回 304,否则返回 200 和对应的内容。
  8. 浏览器接收到返回信息并开始下载该 HTML文件(无缓存、200返回码)或从本地缓存提取文件(有缓存、304返回码)
  9. 浏览器的渲染引擎在拿到 HTML 文件后,便开始解析构建 DOM 树,并根据 HTML 中的标记请求下载指定的 MIME 类型文件(如 CSS、 JavaScript 脚本等),同时使用&设置缓存等内容。
  10. 渲染引擎根据 CSS 样式规则将 DOM 树扩充为渲染树,然后进行重排、重绘。
  11. 如果含有 JS 文件将会执行,进行 Dom 操作、缓存读存、事件绑定等操作。最终页面将被展示在浏览器上。

此答案精简的概括了「后端为主的 MVC 模式」及早期 Web 应用的浏览器响应的全过程。前端技术发展到现在,「前后端分离」「中间件直出」和「MNV*模式」也已问世,再谈及此问题,答案会有所不同。

就以「前后端分离」为例,在上方答案的第4步后,紧接着就不会直接进入后端服务器了。而会被 HTTP 和反向代理服务器,如 Ngnix,拦截。

  • 前置步骤1、2、3、4
  • Ngnix 在监听到 HTTP(80端口)或 HTTPS(443端口)请求,根据 URL 做服务分发,分发(rewrite)到后端服务器或静态资源服务器,首页请求基本是分发到静态服务器,返回一个 HTML 文件
  • 步骤7、8、9、10
  • 执行 JS 脚本,异步 ajax、 fetch 发起 POST、 GET 请求,重新进入 Ngnix 分发,此次分发到后端服务器,步骤5、6、7,然后返回一个 xml 或 json 格式的信息,一般含有 code(返回码)和 result(依赖信息)
  • js 回调根据返回码执行不同的逻辑,增删改页面元素,此时可能会发生重排或重绘。首页加载结束。

从以上步骤可以发现,浏览器可能会触发两次重绘,极易产生「白屏」或「页面抖动」现象,为了解决这个问题「中间件直出」的模式应运而生。另外为了扩充大前端的阵营,吸纳 IOS 和 Android,Google 设计了「MNV*模式」,典型代表就是 ReactNative,但此模式已经脱离了浏览器的范畴,此处就不再做扩展。

以上讨论的渲染过程中使用到了较多的浏览器功能,如用户地址栏输入框、网络请求、浏览器文档解析、渲染引擎渲染网页、 JavaScript 引擎执行 js 脚本、客户端存储等。 接下来我们介绍下浏览器的基本结构组成。

浏览器的结构组成

浏览器一般由七个模块组成,User Interface(用户界面)、Browser engine(浏览器引擎)、Rendering engine(渲染引擎)、Networking(网络)、JavaScript Interpreter(js解释器)、UI Backend(UI 后端)、Date Persistence(数据持久化存储) 如下图:

浏览器的结构组成
浏览器的结构组成

  • 用户界面 -包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了页面显示窗口之外的其他部分
  • 浏览器引擎 -可以在用户界面和渲染引擎之间传送指令或在客户端本地缓存中读写数据等,是浏览器中各个部分之间相互通信的核心
  • 渲染引擎 -解析DOM文档和CSS规则并将内容排版到浏览器中显示有样式的界面,也有人称之为排版引擎,我们常说的浏览器内核主要指的就是渲染引擎
  • 网络 -用来完成网络调用或资源下载的模块
  • UI 后端 -用来绘制基本的浏览器窗口内控件,如输入框、按钮、单选按钮等,根据浏览器不同绘制的视觉效果也不同,但功能都是一样的。
  • JS解释器 -用来解释执行JS脚本的模块,如 V8 引擎、JavaScriptCore
  • 数据存储 -浏览器在硬盘中保存 cookie、localStorage等各种数据,可通过浏览器引擎提供的API进行调用

作为前端开发人员,我们需要重点理解渲染引擎的工作原理,灵活应用数据存储技术,在实际项目开发中会经常涉及到这两个部分,尤其是在做项目性能优化时,理解浏览器渲染引擎的工作原理尤为重要。而其他部分则是由浏览器自行管理的,开发者能控制的地方较少。今天我们就围绕这两个重点其中的一个部分「浏览器渲染引擎」进行展开

浏览器渲染引擎

浏览器渲染引擎是由各大浏览器厂商依照 W3C 标准自行研发的,也被称之为「浏览器内核」。

目前,市面上使用的主流浏览器内核有5类:Trident、Gecko、Presto、Webkit、Blink。

Trident:俗称 IE 内核,也被叫做 MSHTML 引擎,目前在使用的浏览器有 IE11 -,以及各种国产多核浏览器中的IE兼容模块。另外微软的 Edge 浏览器不再使用 MSHTML 引擎,而是使用类全新的引擎 EdgeHTML。

Gecko:俗称 Firefox 内核,Netscape6 开始采用的内核,后来的 Mozilla FireFox(火狐浏览器)也采用了该内核,Gecko 的特点是代码完全公开,因此,其可开发程度很高,全世界的程序员都可以为其编写代码,增加功能。因为这是个开源内核,因此受到许多人的青睐,Gecko 内核的浏览器也很多,这也是 Gecko 内核虽然年轻但市场占有率能够迅速提高的重要原因。

Presto:Opera 前内核,为啥说是前内核呢?因为 Opera12.17 以后便拥抱了 Google Chrome 的 Blink 内核,此内核就没了寄托

Webkit:Safari 内核,也是 Chrome 内核原型,主要是 Safari 浏览器在使用的内核,也是特性上表现较好的浏览器内核。也被大量使用在移动端浏览器上。

Blink: 由 Google 和 Opera Software 开发,在Chrome(28及往后版本)、Opera(15及往后版本)和Yandex浏览器中使用。Blink 其实是 Webkit 的一个分支,添加了一些优化的新特性,例如跨进程的 iframe,将 DOM 移入 JavaScript 中来提高 JavaScript 对 DOM 的访问速度等,目前较多的移动端应用内嵌的浏览器内核也渐渐开始采用 Blink。

渲染引擎的工作流程

浏览器渲染引擎最重要的工作就是将 HTML 和 CSS 文档解析组合最终渲染到浏览器窗口上。如下图所示,渲染引擎在接受到 HTML 文件后主要进行了以下操作:解析 HTML 构建 DOM 树 -> 构建渲染树 -> 渲染树布局 -> 渲染树绘制。

渲染引擎工作流程
渲染引擎工作流程

解析 HTML 构建 DOM 树时渲染引擎会将 HTML 文件的便签元素解析成多个 DOM 元素对象节点,并且将这些节点根据父子关系组成一个树结构。同时 CSS 文件被解析成 CSS 规则表,然后将每条 CSS 规则按照「从右向左」的方式在 DOM 树上进行逆向匹配,生成一个具有样式规则描述的 DOM 渲染树。接下来就是将渲染树进行布局、绘制的过程。首先根据 DOM 渲染树上的样式规则,对 DOM 元素进行大小和位置的定位,关键属性如position;width;margin;padding;top;border;...,接下来再根据元素样式规则中的color;background;shadow;...规则进行绘制。

另外,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

再者,需要注意的是,在浏览器渲染完首屏页面后,如果对 DOM 进行操作会引起浏览器引擎对 DOM 渲染树的重新布局和重新绘制,我们叫做「重排」和「重绘」,由于重排和重绘是前后依赖的关系,重绘发生时未必会触发渲染引擎的重排,但是如果发生了重排就必然会触发重绘操作,这样带来的性能损害就是巨大的。因此我们在做性能优化的时候应该遵循「避免重排;减少重绘」的原则。

不同浏览器内核间的差异

在不同的浏览器内核下, 浏览器页面渲染的流程略有不同

webkit 内核工作流程
webkit 内核工作流程

Geoko 内核工作流程
Geoko 内核工作流程

上面两幅图分别是 Webkit 和 Geoko 内核渲染 DOM 的工作流程,对比可以看出,两者的区别主要在于 CSS 样式表的解析时机,Webkit 内核下,HTML 和 CSS 文件的解析是同步的,而 Geoko 内核下,CSS 文件需要等到 HTML 文件解析成内容 Sink 后才进行解析。

另外描述术语也有不同,除此之外两者的流程就基本相同了,其中最重要的三个部分就是 「HTML 的解析」「CSS 的解析」「渲染树的生成」。这三个部分的原理比较深,会涉及到「词法分析」「语法分析」「转换」「解释」等数据结构的知识,比较枯燥,一般我们了解到这里就够了,想深入了解的同学可以阅读此篇译文,浏览器的工作原理,里面详细的解释了以上三个部分的流程和原理。此处就不再多做赘述了。

关于 CSS 规则的匹配

上面我们提到过, CSS 规则是按照「从右向左」的方式在 DOM 树上进行逆向匹配的,最终生成一个具有样式规则描述的 DOM 渲染树。

但是你知道为什么要「从右向左」做逆向匹配吗?

我们重新回看【webkit 内核工作流程图】

webkit 内核工作流程
webkit 内核工作流程

CSS 规则匹配是发生在webkit引擎的「Attachment」过程中,浏览器要为每个 DOM Tree 中的元素扩充 CSS 样式规则(匹配 Style Rules)。对于每个 DOM 元素,必须在所有 Style Rules 中找到符合的 selector 并将对应的规则进行合并。选择器的「解析」实际是在这里执行的,在遍历 DOM Tree 时,从 Style Rules 中去寻找对应的 selector。

我们来举一个最简单的栗子:

<template>
<div>
  <div class="t">
    <span>test</span>
    <p>test</p>
  <div>
</div>
</template>

<style>
div{ color: #000; }
div .t span{ color: red; }
div .t p{color: blue; }
</style>

此处我们有一个 html 元素 和一个 style 元素,两者需要做遍历匹配

此处会有 4*3 个匹配项,如果做正向匹配,在遇到 <span> 标签匹配 div .t p{ color: red; } 到匹配项时,计算机首先要找到<span> 标签的父标签和祖父标签,判断他们是否满足div .t的规则,然后再匹配<span>是否为p标签,此处匹配不成功,产生了三次浪费。

如果时逆向匹配,那么第一次对比<span>是否为p标签便可排除此规则,效率更高。

如果将 HTML 结构变复杂,CSS 规则表变庞大,那么,「逆向匹配」的优势就远大于「正向匹配」了,因为匹配的情况远远低于不匹配的情况。另外,如果在选择器结尾加上通配符「*」,那么「逆向匹配」的优势就大打折扣了,这也就是很多优化原则提到的「尽量避免在选择器末尾添加通配符」的原因。

极限了想,如果我们的样式表不存在嵌套关系,如下:

<template>
  <div class="t">
    <span class="div_t_span">test</span>
    <p class="div_t_p">test</p>
  <div>
</template>

<style>
div{ color: #000; }
.div_t_span{ color: red; }
.div_t_p{color: blue; }
</style

那么引擎的「Attachment」过程将得到极大的精简,效率也是可想而知的,这就是为什么「微信小程序」样式表不建议使用关系行写法的原因。

相关的性能优化

我们大致可以在以上案例中看到同浏览器渲染引擎相关的可行优化点。

大致为以下几种

减少 JS 加载对 Dom 渲染的影响

将 JS 文件放在 HTML 文档后加载,或者使用异步的方式加载 JS 代码

避免重排,减少重绘

在做 css 动画的时候减少使用 width、 margin、 padding 等影响 CSS 布局对规则,可以使用 CSS3 的 transform 代替。另外值得注意的是,在加载大量的图片元素时,尽量预先限定图片的尺寸大小,否则在图片加载过程中会更新图片的排版信息,产生大量的重排。

减少使用关系型样式表的写法

直接使用唯一的类名即可最大限度的提升渲染效率,另外尽量避免在选择器末尾添加通配符

减少 DOM 的层级

减少无意义的 dom 层级可以减少 渲染引擎 Attachment 过程中的匹配计算量

预告

「前端那些事儿系列」二 关于前端优化策略