本文主要是两方面内容:
- 浅析浏览器内核的工作原理(以 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 到页面呈现,我们大致做了介绍。但这只是皮毛最上方的一点,更多浏览器内核的实质,值得我们下载一份源码,编译解析深挖~ 相信在前端优化的路上,知其然,知其所以然~ 定会走得跟远~~