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