浅析渲染引擎与前端优化
本文主要是两方面内容:
- 浅析浏览器内核的工作原理(以 WebKit 2 为例)。
-
浅析由浏览器内核想到的前端优化,或者说前端优化规则是从哪儿来的。
大家知道,大部分的 WEB 页面依托浏览器呈现,而浏览器能够将页面展示出来,基本依赖于浏览器的内核,即渲染引擎。今天以 Chrome 浏览器的内核 WebKit(更确切是 WebKit 分支 Blink,以下统称为 WebKit )为例,对渲染引擎如何展示页面做个简单、全面的了解。
浏览器的渲染引擎及其依赖模块
渲染引擎主要是将 WEB 资源如 HTML、CSS、图片、JavaScript等经过一系列加工,最终呈现出展示的图像。渲染引擎主要包含了对这些资源解析的处理器,如 HTML 解释器、CSS 解释器、布局计算+绘图工具、JavaScript 引擎等。为了更好地呈现渲染效果,渲染引擎还会依赖网络栈、缓存机制、绘图工具、硬件加速机制等。
浏览器的渲染过程
浏览器的渲染过程,主要包括两大部分:网页 资源加载过程和 渲染过程。
上图将整个网页渲染的过程做了大致的剖析。以下我们按照 数据流向,逐一详细剖析每个过程。
一、域名解析 DNS
当我们在浏览器中输入 URL 后,浏览器首先会进行域名解析。一般情况下,一次 DNS 域名解析大概需要 60-120 ms,一次 TCP 的三次握手需要 1.5 个 RTT(round-trip time)。WebKit 的方案是 采用 DNS 预取技术和 TCP 预连接技术。
DNS 预取技术利用现有 DNS 机制,提前解析网页中可能的网络连接。即对用户浏览网页中存在的链接,用较少的 CPU 和网络带宽来解析这些链接的域名或 IP 地址;等用户单击链接时,就会节省时间~ 特别是域名解析慢的时候~
同样,在地址栏输入链接时,候选项也会被默默地执行 DNS 预取~。在 DNS 预取后,会预先建立 TCP 连接。
对此 前端优化建议:
- 在页面中指定预取域名:<link rel=”dns-prefetch” href=”http://this-is-a.com”>
-
大数据分析,推测用户可能点击的链接,提前预取。
-
减少页面中的域名数量,可以直接减少DNS的请求。
二、SPDY 和 HTTP2
因为请求带来的 TCP 三次握手的 1.5 RTT 延迟,Google 引入 SPDY,尝试解决HTTP的延迟和安全性(HTTP 明文方式)问题。不过,SPDY 促使了 HTTP2.0 的诞生后,自己也不再更新,逐步退出。
SPDY 基于 SSL 之上,轻松兼容 HTTP 新老版本。其优势如下:
- 多路复用。一个 TCP 连接传输多个资源。减少 TCP 连接成本。
-
不同资源,不同优先级。比如优先加载首屏。
-
Header 头压缩。减少传送的字节数。SPDY 对 Header 压缩率可高达 80%。
SPDY 开拓了 HTTP 新局面,秒杀我们太多的前端优化工作,从本质上提升了页面加载速度。但我们 前端优化的工作还是不能偏废。向着继续 减少请求, 减少 TCP 连接建立的路上,让我们继续。
- 合并资源,如 combo 合并 JavaScript 文件、CSS 文件,利用 sprite 合并图片,图片地图等;
-
当页面资源较小时,可直接放页面中,如小图可使用 Base64 编码格式引入。甚至一些基础样式,或首屏依赖样式,都可以放在页面中;
-
资源压缩技术。如 Gzip 等。主要是对响应数据的压缩~
-
精简 JavaScript 和 CSS 代码。减少无用的空格。压缩混淆~
-
避免链接重定向、避免错误的链接请求。建立多次链接、多次 DNS 解析,阻碍 DNS 预取技术。及时更新掉你页面中没有价值的链接吧。
三、资源加载
域名解析完,TCP 连接也建立起来后,资源加载器就开始工作了。
资源及资源加载器
资源包括:HTML、JavaScript、CSS 样式表、图片、SVG、字体文件、视频音频等。资源加载器有三种:
- 特定加载器,只加载某一种。如ImageLoader类。
-
缓存机制的资源加载器。特定加载器通过它查找是否有缓存资源,属于 HTML 的文档对象。
-
通用的资源加载器。在WebKit需要从网络或文件系统获取资源时使用。只负责获取资源的数据,被所有特定资源加载器共享。
资源加载的过程
在 WebKit 中,资源都以 CachedResource 为基类,以 Cached 为前缀,体现了浏览器的缓存机制。即请求资源时,浏览器会先看缓存中有没有这个资源,然后再决定是否向服务器发出请求。
这引出两个问题,首先, 缓存资源的生命周期。
浏览器缓存不会无限增大,缓存池中的数据必然出现更替,WebKit 采用 LRU 最近最少使用算法更新缓存池数据。WebKit 遵循 HTTP 协议,当页面刷新时,判断资源是否在资源池。若存在,则附上该资源在本地的一些信息(如修改时间等),发送 HTTP 请求给服务器,服务器根据信息作出判断,若资源没更新则网络状态为 304,利用现有资源;否则执行资源加载过程。
其次, 资源加载过程。
资源池中没有该资源时,执行加载过程。WebKit 可以并行(多线程)下载普通资源和 JavaScript 资源。在当前主线程被阻塞时,WebKit 会启动另一个线程去遍历后边的网页,收集需要的资源 URL再发请求,避免阻塞。
基于资源加载, 前端优化建议:
- 利用缓存机制,缓存常用且短时期内不会变更的资源,或给资源设置过期时间。
比如设置 ETag/Last-Modified 和 Expires/Cache-Control。
Expires/Cache-Control 两者作用一致,指明资源有效期,如果本地缓存还在有效期内,浏览器直接使用本地缓存,不再发送请求。两者同时配置时,Cache-Control 高于 Expires。配置 ETag/Last-Modified 后,浏览器再次访问 URL 时,还会向服务器发送请求,确认文件是否已修改,没修改则服务器返回304,浏览器直接从本地缓存获取数据;修改过则服务器返回数据给浏览器。两者同时配置,服务器会优先检测 ETag,一致才会继续检测 Last-Modified。两者同时配置,可以使服务器更准确的判断浏览器是否已有需要的缓存数据。
ETag/Last-Modified 和 Expires/Cache-Control 两对都设置时, Expires/Cache-Control 优先级更高。所以,只要本地缓存在有效期内,就不会发送请求。但页面 F5 刷新和强刷时,缓存将失效。
- 鉴于资源下载中可能被阻塞,将 JavaScript 文件放置页面下方。JavaScript 资源就是阻塞主线程的那个,而重建一个线程也是需要时间滴,所以把 JavaScript 扔最后吧~ 但 JavaScript 资源并不影响之前资源的加载和 DOM 树的构建。
四、从 URL 到 DOM 树的构建
当我们拿到页面所需的资源后,渲染引擎便启动 HTML 解释器,对获取的资源进行解析处理。网页代码(字节流)经过词法分析器解码,再由语法分析器解释成词语 Token,并构建成节点 Node,直到最终构建成一棵 DOM 树。
期间,当节点为 JavaScript 节点时,将启动 JavaScript 引擎,这时将阻塞 DOM 树的构建。因为 JavaScript 执行过程中, JavaScript 很可能会对 DOM 树进行读写操作。直到 JavaScript 执行完毕, DOM 树才会恢复构建。
其他资源并不影响 DOM 树的构建。
在 前端优化中,建议将 CSS 文件放在页首,以便构建 DOM 树;而将 JavaScript 文件尽量放在页面下方,防止阻塞构建 DOM 树;而 JavaScript 的 onload 事件里,不要写太多影响首屏渲染的、操作 DOM 树的 JavaScript 代码。
另外强调一下:
DOMContentLoaded: DOM 树构建完;
DOM 的onload事件: DOM 树构建完且网页依赖的资源都加载完了~
五、网页排版过程:由 DOM 树到构建 RenderLayer 树
这一过程,就像是页面的排版过程。它通过 CSS 样式信息,对 DOM 树进行排版,形成 RenderObject 树及 RenderLayer 树。
在 DOM 树构建完成后,WebKit 为 DOM 树节点构建 RenderObject 对象。WebKit 将根据盒模型计算节点的位置、大小等样式信息(即布局计算或排版),并将这些信息保存到对应的 RenderObject 对象。
1. CSS解释器
CSS解释过程,是从 CSS 字符串经过 CSS 解释器(CSSParser、CSSGrammer)处理后,变成渲染引擎的内部样式规则表示的过程。样式规则是解释器的 输出结构,是样式匹配的 输入数据。
具体过程:WebKit 在渲染元素时,CSS 解释器获取样式信息,返回匹配好的结果样式信息。每个元素可能需要匹配不同来源的规则,依次是用户代理(浏览器)规则集合、用户规则集合和HTML页面中包含的自定义规则集合。三者匹配方式类似。
对于每个规则集合,先查找 ID 规则,检查有无匹配的规则,然后依次检查类型规则、标签规则等。匹配好的规则,保存到匹配结果中。WebKit 对这些规则进行排序。对于元素需要的样式属性,WebKit 选择从高优先级规则中选取,并将样式属性值返回。
2. 渲染基础:RenderObject 树
DOM 树经过布局计算、CSS parse 后,将样式信息存储在 RenderObject 对象中,并构建成 RenderObject 树。同时,WebKit 会根据网页的层次结构创建 RenderLayer 树,完成绘图上下文。DOM 树、Render 树和绘图上下文同时并存,直到页面销毁。
RenderObject 树,基于 DOM 树的一棵新树,是布局计算和渲染等机制的基础设施。
DOM 节点建立新的 RenderObject 对象的时机:
- DOM 树的 Document 节点。
-
DOM 树的可视节点,如html、body、div 等。非可视节点如meta、head、script 等不创建。
-
为满足 WebKit 处理,需要建立匿名 RenderObject 节点,它不对应于 DOM 树的任何节点。如:匿名的 RenderBlock 节点。
DOM 树的每个节点对象会递归检查是否需要创建 RenderObject,并根据 DOM 节点类型创建 RenderObject 节点;动态加入的 DOM 元素,会相应的创建 RenderObject 节点。所有这些节点构成一棵 RenderObject 树。
3. 渲染基础:网页层次和 RenderLayer 树
在 HTML 页面上,网页分层展示。目的有两个:1. 方便开发网页、设置网页的层次;2. 简化 WebKit 渲染的逻辑。
在RenderObject 树基础上,WebKit 根据需要为其中的某些节点创建新的 RenderLayer 节点,并形成一棵 RenderLayer 树。
RenderObject 节点建立新 RenderLayer 对象的时机:
- DOM 树的 Document 节点对应的 RenderView 节点。
-
DOM 树的 Document 的子节点,即 HTML 节点对应的 RenderBlock 节点。
-
显式的指定 CSS 位置的 RenderObject 节点。
-
有透明效果的 RenderObject 节点。
-
节点有溢出 overflow、alpha 或反射等效果的 RenderObject 节点。
-
使用Canvas 2D、3D (WebGL)技术的 RenderObject 节点。
-
Video 节点对应的 RenderObject 节点。
RenderLayer 节点的使用可以有效减少网页结构的复杂程度,并在许多情况下能减少重新渲染的开销。
4. 布局计算及重绘时机
CSS 盒模型,是布局计算的基础;渲染引擎用来确定如何排版元素、及元素间的位置关系。
布局计算,是针对 RenderObject 树及其子树的计算,是一种递归计算,其节点信息需要先计算其子节点的位置、大小等信息。RenderObject 对象会将计算结果存储,等待渲染时机。
- 每个元素会实现自己的 layout。
- 页面元素定义了宽高,则按自定义宽高确定元素大小。
- 文本节点等内联元素,需要结合字号大小、文字多少确定宽高。
- 页面元素确定的宽高超过了布局容器包含块提供的宽高,同时 overflow 为 visible 或 auto,WebKit 则提供滚动条保证可显示所有内容。
- 一般页面元素的宽高是在布局时通过计算得来。除非网页定义了页面元素的宽高。
重绘时机:只要样式发生变化,就重新计算。
- 首次打开页面,浏览器设置网页的可视区域,并调用计算布局的方法。可视区域改变时,网页包含块的大小也会改变,WebKit 需要重新计算布局。
-
网页的动画会触发布局计算。动画可能改变样式属性。
-
JavaScript 通过 CSSOM(CSS 对象模型) 直接修改样式,会触发 WebKit 重新计算布局。
-
用户交互,如滚动网页。
前端优化建议,因布局计算耗时间,一旦布局发生变化,WebKit 就需要后面的重新绘制操作。SO,减少样式的变动~减少重绘~利用 CSS3 新功能(如 CSS3 变形 translate、scale、rotate 等方法,过渡 transition 方法等)可有效提高网页的渲染效率。
六、 网页渲染过程:由 RenderLayer 树到最终的图像
在上一个过程,网页完成了 DOM 树到 RenderLayer 树的布局计算和排版处理。接下来,由渲染引擎(一般是绘图类工具)完成对 RenderLayer 树的绘制,并最终形成图像,展示给用户。
1. 绘图上下文
绘图上下文,所有的绘图操作都是在该上下文中进行的。它是一个与平台无关的抽象类,它将每个绘图操作桥接到不同的绘图具体实现类。
2D 绘图上下文:
- 提供基本绘图单元的绘制接口及设置绘图的样式。
-
绘图接口包括:画点、画线、画图、画多边形、画文字等。绘图样式包括颜色、线宽、字号、渐变等。
-
CPU 来完成 2D 操作。或用 3D 图形接口( OpenGL )完成。
3D 绘图上下文:支持 CSS3D、WebGL 等。
- 使用 3D 图形接口(OpenGL、Direct3D 等)
2. 渲染方式
软件渲染:CPU。通常渲染的结果是一个位图,绘制每一层时都使用该位图,区别在于位置可能不同,每一层按从后到前的顺序。没必要为每层分配一个位图,没必要合成。
缺点:对 HTML5 新技术,
- 能力不足,CSS3D、WebGL;
-
性能不好,如视频、Canvas 2D;
-
使用率下降,特别是移动端。
优势:对更新区域处理,软件渲染可能只需要计算极小区域,硬件则需要绘制其中一层或多层,再合成。硬件代价大。
硬件加速渲染:GPU 必须有合成的步骤。分层绘制+合成。不过对于更新区域,如果只是在一个层,硬件可能会更快。
WebKit 的实现方式:
- 使用合适的网页分层技术、减少重新计算的布局和绘图。
-
使用CSS 3D 变形和动画技术。CSS 3D 变形技术,能让浏览器仅使用合成器合成所有层就可以达到动画效果。不需要布局计算和重绘~
前端优化建议:
- 减少重绘:因为重绘是要计算布局、绘图、合成三个阶段。其中 计算布局和绘图比较费时,合成要少。
七、总结
至此,从输入 URL 到页面呈现,我们大致做了介绍。但这只是皮毛最上方的一点,更多浏览器内核的实质,值得我们下载一份源码,编译解析深挖~ 相信在前端优化的路上,知其然,知其所以然~ 定会走得跟远~~