@yangfch3
2017-03-07T22:32:13.000000Z
字数 6471
阅读 12000
FE
browser
浏览器内核又叫渲染引擎,主要负责 HTML、CSS 的解析,页面布局、渲染与复合层合成。浏览器内核的不同带来的主要问题是对 CSS 的支持度与属性表现差异。
现在主流的内核有:Blink、Webkit、Gecko、EdgeHTML、Trident,这里面有几个需要注意的地方:
Blink 是在 Webkit 的基础上的改进,是现在对新特性支持度最好的内核
移动端基本上全部是 Webkit 或 Blink 内核(除去 Android 上腾讯家的 X5),这两个内核对新特性的支持度较高,所以新特性可以在移动端大展身手。
Trident 是 IE4+ 的内核,一直持续到 IE11,EdgeHTML 是微软抛弃 IE 后开发的全新内核
更多资料请看附录表格
一般现代浏览器都会有以下几种渲染模式:
在 IE5 与 NS4 那个年代,浏览器大战,标准未立,Web 则在经历早期快速地发展。
后来标准逐步建立,新标准的规范与以前 IE5、NS4 的实现存在着不可避免的差异差异,但是此时的网络世界许许多多旧时的页面正在运行,如果按照新标准的实现来渲染的话会有大量的问题出现。
所以此时大部分现代浏览器厂商想到了区别性地使用不同渲染模式来对待这些 Web 页面。
而 IE 随着升级,对现代标准的支持也越来越完善,所以 IE 为了正常渲染旧时页面,支持我们指定哪个版本的 IE 模式来渲染页面。
总结就是:
标准未立之前,HTML 文档是没有文档头的,同时在 HTML5 之前的 HTML4/3 的文档头都有各自的特征,所以在大部分现代浏览器下触发的机制如下:
DOCTYPE
头触发怪异模式DOCTYPE
头不正确(不是 html)也触发怪异模式
如:
<!DOCTYPE svg>
DOCTYPE
头为 HTML3 头触发怪异模式
DOCTYPE
头为 HTML4 头则触发接近标准模式(或称有限怪异模式)在 IE 下,除了文档头的差异可以自动触发渲染模式的选择,我们还能手动指定(在 IE8+ 适用)使用哪个版本的 IE 渲染模式来渲染我们的页面(扩展阅读):
<!-- 使用当前操作系统已装的最新的 IE -->
<!-- chrome=1 是针对双核浏览器使优先使用 Chrome -->
<meta http-equiv="x-ua-compatible" content="ie=edge,chrome=1">
<!-- 使用 IE9 -->
<meta http-equiv="x-ua-compatible" content="ie=9">
<!-- 如果你需要使用 IE5 的怪异模式 -->
<meta http-equiv="x-ua-compatible" content="ie=5">
怪异模式与标准模式
- 怪异模式使用不同于标准的盒模型(也就相当于 IE8+ 下的:
box-sizing: border-box
)- 怪异模式下某些行内(inline)元素的垂直对齐行为表现怪异:怪异模式下对齐图片至包含它们的盒子的下边框,而 标准模式图片对其到父盒的 baseline
接近标准模式(有限怪异模式)与标准模式
主要区别即为上面的第 2 点
JavaScript 负责 JavaScript 代码的解释与执行,主流的 JavaScript 引擎有:V8、SpiderMonkey、JavaScriptCore、Chakra。
浏览器与引擎详情见附录表格。
当我们点击一个链接,服务器将 HTML 代码传输到我们的浏览器,浏览器在接收到这份 HTML 代码之后是如何一步步将页面呈现出来的呢?这里面浏览器需要做哪些工作?如何优化呈现的过程提升 Web 应用质量?
一个页面的呈现,粗略的说会经过以下这些步骤:
为什么是 Re-caculate Style 呢?这是因为浏览器本身有
User Agent StyleSheet
,所以最终的样式是我们的样式代码样式与用户代理默认样式覆盖/重新计算得到的。
绘制(Paint)
复合图层化(Composite)
图层化是自己理解后形象的意译
其中布局(Layout)环节主要负责各元素尺寸、位置的计算,绘制(Paint)环节则是绘制页面像素信息,合成(Composite)环节是多个复合层的合成,最终合成的页面被用户看到。
虽然六部曲看似和谐,分工合作,有序进行。但是实际上这里面却是波云诡谲,风起云涌,就像平时的工作一样,看似你和我各司其职,分工明确,但是实际干起活来却可能因为某一个人的某一环而阻滞整个进度。
我们来分析这六部曲中存在的阻塞问题:
当遇到 JavaScript 脚本或者外部 JavaScript 代码时,浏览器便停止 DOM 的构建(阻塞 1)
那是否停下 DOM 的构建的同时,立马就执行 JavaScript 代码或者下载外部脚本执行,其实还是要视情况而定,见 2
当遇到 <script>
标签需要执行脚本代码时,浏览器会检查是否这个 <script>
标签以上的 CSS 文件是否已经加载并用于构建了 CSSOM,如果 <script>
上部还有 CSS 样式没加载,则浏览器会等待 <script>
上方样式的加载完成才会执行该 <script>
内的脚本(阻塞 2)
DOM 树与 CSSOM 树的成功构建是后面步骤的根基(同步阻塞)
同时外部脚本、外部样式表的下载也是耗费时间较多的点
浏览器构建 DOM 树可以简单的总结为以下几步:
转码(Bytes -> Characters)—— 读取接收到的 HTML 二进制数据,按指定编码格式将字节转换为 HTML 字符串
Tokens 化(Characters -> Tokens)—— 解析 HTML,将 HTML 字符串转换为结构清晰的 Tokens,每个 Token 都有特殊的含义同时有自己的一套规则
构建 Nodes(Tokens -> Nodes)—— 每个 Node 都添加特定的属性(或属性访问器),通过指针能够确定 Node 的父、子、兄弟关系和所属 treeScope(例如:iframe 的 treeScope 与外层页面的 treeScope 不同)
构建 DOM 树(Nodes -> DOM Tree)—— 最重要的工作是建立起每个结点的父子兄弟关系
在 Chrome 开发者工具下 Timeline 面板的
Parse HTML
阶段对应着 DOM 树的构建。
留意这篇文章的这些点:
- DOM 构建时对 DOCType 处理
- DOCType 的不同或漏缺带来的文档解析模式(怪异模式、有限怪异模式、标准模式)的影响
- 处理开标签与闭标签的压栈、弹栈处理
- Chromium 对待自定义标签的处理
- JavaScript 方法查找 DOM 的过程,使用 ID、类名、复杂选择器查找 DOM 的对比
CSSOM 树的构建 “原料” 的来源有:外部 CSS 文件、内部样式、内联样式。
CSSOM 树的构建其实是一个 样式的重新计算 的过程,为什么是重新计算呢?
用户代理(即浏览器)本身有一套内置样式表,所以我们最终的 CSSOM 树其实是用户代理样式与页面所有样式的重新计算。
所以在 Chrome 浏览器开发者工具的 Timeline 面板下,CSSOM 树的构建对应的是
Recalculate Style
阶段
与 DOM 树的构建过程相似,CSSOM 的构建也要经历以下过程:
最终构建的 CSSOM 树大致如下:
DOM 树与 CSSOM 树融合成渲染树
渲染树只包括渲染页面需要的节点
排除
<script>
<meta>
等功能化、非视觉节点
排除display: none
的节点
Layout 阶段做的工作:确定页面各元素的位置、尺寸。
Layout 在 Chrome 开发者工具 Timeline 面板中被归并到 Paint 阶段
当元素某些样式变更/JavaScript 执行某些样式请求,会导致 Layout trashing
,又叫做回流(Reflow)。
一旦布局(Layout)步骤完成,浏览器便触发 “Paint Setup” 与 “Paint” 事件(渲染引擎底层概念),执行 paint
操作,结合渲染树与布局信息绘制实际像素。
注:在 Timeline 工具内,
Layout
与Paint
两个过程被统一归并到Paint
阶段。
在很多情况下,我们不会将复合图层化归入页面呈现的必要过程。图层化是浏览器为了充分利用已有渲染成果(缓存渲染成果),最小化 GPU 运算,将“脏区”提升为复合图层,隔离变化影响的操作。
见 链接
知道了页面渲染的原理,那么我们也就得到了页面性能优化的依据。提炼六部曲中每一步的优化空间,针对六部曲中的每一步提出针对性的优化方案也就能达到我们最终的优化目的。
关键呈现路径里的一些概念:
优化关键呈现路径的指导原则:
优化关键呈现路径常规步骤:
优化关键呈现路径的具体建议:
async
) JavaScript 资源,或使用延迟(defer
)执行的 JavaScript<script>
脚本的靠后书写 避免运行时间长的 JavaScript,耗时任务的拆分,chunk 化运行
例如:使用定时器将大任务拆分为小任务,使得浏览器得到空隙做其他事情。
避免使用 CSS import
内联、内部化阻止呈现的 CSS
一般不采用,百度、Google 这样的极度重视性能与体验的服务才可能这样做。
因为浏览器有图层化这个机制,那么我们就搞懂它并充分利用吧。
某些属性的变更(transform
、opacity
)满足以下条件:
那么这些属性变更时就需要一种机制:机制需要能将属性变更的部分与页面其他部分隔离开来,其他部分已经渲染完好进行缓存,变更的部分在单独的图层上进行,然后对缓存的部分与变更的图层进行合成。
所以图层化的关键字:缓存、隔离、图层合成
使用 transform
与 opacity
进行属性变更是经典的符合图层化方法,以下是其他会提升元素为复合图层的场景:
translate3d
, translateZ
等等(JS 一般通过这种方式,使元素获得复合图层)<video>
<iframe>
<canvas>
<webgl>
等元素。opacity
和 transform
做 CSS 动画。will-change
属性。position:fixed
。z-index
较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上方)很容易看出来:充分利用缓存、隔离的思想,无需像回流、重绘那么大性能(GPU、CPU)开支,图层化能带来动画性能的提升。
那么图层化的弊端在哪里呢?
因为图层化的存在,每个图层对需要在内存中存储该图层相关的信息,当图层太多会造成内存开销过大的情况(如下图)。
内存开销在桌面端可能还能接受,但是在资源有限的移动端,复合图层过多便可能导致内存开支过大,页面反而变得停滞、卡顿,甚至浏览器假死,系统无法正常运行。
transfrom
, opacity
background-color
等position
- top
bottom
left
right
width
height
等margin
, padding
, border
JavaScript 存在这样的机制:当连续有大量 DOM 样式的操作时,出于性能考虑,防止零碎变更导致频繁的回流、重绘,会尽可能地将这些操作先缓存起来,然后一次性地变更。这个机制我们难以察觉但是确实存在。
然而当我们进行某些 DOM 样式的读、写时,出于时效性的考虑,则会立即触发浏览器回流、重绘以返回正确、合理的值。
其他针对回流的优化措施:
ClassName
的变更来一次性修改。将 DOM 离线后再修改,例如:先将 DOM remove 或先 display:none
再修改
先把 DOM 节点 display:none;(会触发一次 Reflow)。然后做大量的修改后,再把它显示出来。
使用 documentFragment
文档碎片对象在内存里操作 DOM。
clone/create 一个 DOM 节点在内存里,修改之后;与在线的节点相替换。
不要使用 table
布局,一个小改动会造成整个 table 的重新布局。
在移动端(桌面端一般性能充足)选择性地(有节制地,原因见下问扩展部分)开启 GPU 渲染加速(将元素提为一复合层以防止大层更新)
-webkit-translate3d
translateZ
-webkit-backface-visibility: hidden;
-webkit-perspective: 1000;
在现代浏览器下(有节制地)使用 will-change
已经比较明白了,那就略吧
也已经比较明白,也略吧
使用 Timeline 工具我们能做以下事:
而我们在日常开发中,用 Timeline 最多的场景是:
选择性地开启以下开关
开启开发者工具实验性特性开关
Timeline 官方简介
Timeline 事件参考
推荐:Timeline 进行帧分析,避免页面卡顿
浏览器/RunTime | 内核(渲染引擎) | JavaScript 引擎 |
---|---|---|
Chrome | Blink(28~) Webkit(Chrome 27) |
V8 |
FireFox | Gecko | SpiderMonkey |
Safari | Webkit | JavaScriptCore |
Edge | EdgeHTML | Chakra(for JavaScript) |
IE | Trident | Chakra(for JScript) |
PhantomJS | Webkit | JavaScriptCore |
Node.js | - | V8 |