深刻理解浏览器解析和执行过程

在咱们公司的业务场景中,有很大一部分用户是使用老款安卓机浏览页面,这些老款安卓机性能较差,若是优化不足,页面的卡顿现象会更加明显,此时页面性能优化的重要性就凸显出现。优化页面的性能,须要对浏览器的渲染过程有深刻的了解,针对浏览器的每一步环节进行优化。css

页面高性能的判断标准是 60fps。这是由于目前大多数设备的屏幕刷新率为 60 次/秒,也就是 60fps , 若是刷新率下降,也就是说出现了掉帧, 对于用户来讲,就是出现了卡顿的现象。html

这就要求,页面每一帧的渲染时间仅为16毫秒 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有其余工做要作,所以这一帧全部工做须要在 10毫秒内完成。若是工做没有完成,帧率将降低,而且内容会在屏幕上抖动。 此现象一般称为卡顿,会对用户体验产生负面影响。前端

浏览器渲染流程

浏览器一开始会从网络层获取请求文档的内容,请求过来的数据是 Bytes,而后浏览器将其编译成HTML的代码。vue

可是咱们写出来的HTML代码浏览器是看不懂的,因此须要进行解析。html5

渲染引擎解析 HTML 文档,将各个dom标签逐个转化成“DOM tree”上的 DOM 节点。同时也会解析内部和外部的css, 解析为CSSOM tree, css treedom tree结合在一块儿生成了render treejava

render tree构建好以后,渲染引擎随后会经历一个layout的阶段: 计算出每个节点应该出如今屏幕上的确切坐标。node

以后的阶段被称为paiting阶段,渲染引擎会遍历render tree, 而后由用户界面后端层将每个节点绘制出来。git

最后一个阶段是 composite 阶段,这个阶段是合并图层。程序员

webkit 渲染引擎的主流程

浏览器内核

浏览器是一个极其复杂庞大的软件。常见的浏览器有chrome, firefox。firefox是彻底开源,Chrome不开源,但Chromium项目是部分开源。web

Chromium和Chrome之间的关系相似于尝鲜版和正式版的关系,Chromium会有不少新的不稳定的特性,待成熟稳定后会应用到Chrome。

浏览器功能有不少,包括网络、资源管理、网页浏览、多页面管理、插件和扩展、书签管理、历史记录管理、设置管理、下载管理、帐户和同步、安全机制、隐私管理、外观主题、开发者工具等。

所以浏览器内部被划分为不一样的模块。其中和页面渲染相关的,是下图中虚线框的部分渲染引擎。

image-20180712105822858

渲染引擎的做用是将页面转变成可视化的图像结果。

目前,主流的渲染引擎包括TridentGeckoWebKit,它们分别是IE、火狐和Chrome的内核(2013年,Google宣布了Blink内核,它实际上是从WebKit复制出去的),其中占有率最高的是 WebKit。

WebKit

最先,苹果公司和KDE开源社区产生了分歧,复制出一个开源的项目,就是WebKit。

WebKit被不少浏览器采用做为内核,其中就包括goole的chrome。

后来google公司又和苹果公司产生了分歧,google从webkit中复制出一个blink项目。

所以,blink内核和webkit内核没有特别的不一样,所以不少老外会借用 chromium的实现来理解webkit的技术内幕,也是彻底能够的。

浏览器源码

浏览器的代码很是的庞大,曾经有人尝试阅读Chromium项目的源码,git clone 到本地发现有10个G,光编译时间就3个小时(听说火狐浏览器编译须要更多的时间,大约为6个小时)。所以关于浏览器内部到底是如何运做的,大部分的分享是浏览器厂商参与研发的内部员工。

国外有个很是有毅力的工程师Tali Garsiel 花费了n年的时间探究了浏览器的内幕,本文关于浏览器内部工做原理的介绍,主要整理自她的博客how browser work , 和其余人的一些分享。

国内关于浏览器技术内幕主要有《WebKit技术内幕》

下面,咱们将针对浏览器渲染的环节,深刻理解浏览器内核作了哪些事情,逐一的介绍如何去进行前端页面的优化。

浏览器渲染第一步:解析

解析是浏览器渲染引擎中第一个环节。咱们先大体了解一下解析究竟是怎么一回事。

什么是解析

通俗来说,解析文档是指将文档转化成为有意义的结构,好让代码去使用他们

以上图为例,右边就是解析好的树状结构,这个结构就能够“喂“给其余的程序, 而后其余的程序就能够利用这个结构,生成一些计算的结果。

解析的过程能够分红两个子过程:lexical analysis(词法分析)syntax analysis(句法分析)

lexical analysis(词法分析)

lexical analysis 被称为词法分析的过程,有的文章也称为 tokenization,其实就是把输入的内容分为不一样的tokens(标记),tokens是最小的组成部分,tokens就像是人类语言中的一堆词汇。好比说,咱们对一句英文进行lexical analysis——“The quick brown fox jumps”,咱们能够拿到如下的token:

  • “The”
  • “quick”
  • “brown”
  • “fox”
  • “jumps”

用来作lexical analysis的工具,被称为**lexer**, 它负责把输入的内容拆分为不一样的tokens。不一样的浏览器内核会选择不一样的lexer , 好比说webkit 是使用Flex (Fast Lexer)做为lexer。

syntax analysis(句法分析)

syntax analysis是应用语言句法中的规则, 简单来讲,就是判断一串tokens组成的句子是否是正确的。

若是我说:“我吃饭工做完了”, 这句话是不符合syntax analysis的,虽然里面的每个token都是正确的,可是不符合语法规范。须要注意的是,符合语法正确 的句子不必定是符合语义正确的。好比说,“一个绿色的梦想沉沉的睡去了”,从语法的角度来说,形容词 + 主语 + 副词 + 动词没有问题,可是语义上倒是什么鬼。

负责syntax analysis工做的是**parser**,解析是一个不断往返的过程。

以下图所示,parserlexer要一个新的tokenlexer会返回一个token, parser拿到token以后,会尝试将这个token与某条语法规则进行匹配。

若是该token匹配上了语法规则,parser会将一个对应的节点添加到 parse tree (解析树,若是是html就是dom tree,若是是css就是 cssom tree)中,而后继续问parser要下一个node。

固然,也有可能该tokens没有匹配上语法规则,parser会将tokens暂时保存,而后继续问lexertokens, 直至找到可与全部内部存储的标记匹配的规则。若是找不到任何匹配规则,parser就会引起一个异常。这意味着文档无效,包含语法错误。

syntax analysis 的输出结果是parse tree, parse tree 的结构表示了句法结构。好比说咱们输入"John hit the ball"做为一句话,那么 syntax analysis 的结果就是:

一旦咱们拿到了parse tree, 还有最后一步工做没有作,那就是:translation,还有一些博客将这个过程成为 compilation / transpilation / interpretation

Lexicons 和 Syntaxes

上面提到了lexerparser 这两个用于解析工具,咱们一般不会本身写,而是用现有的工具去生成。咱们须要提供一个语言的 lexiconsyntaxes ,才能够生成相应的 lexerparser

webkit 使用的 lexer 和 parser 是 FlexBison

  1. flexcssflex 布局没有关系,是 fast-lexer 的简写,用来生成 lexer。 它须要一个lexicon,这个lexicon 是用一堆正则表达式来定义的 。
  2. bison 用来生成parsers, 它须要一个符合BNF范式的syntax。

lexicons

lexicons 是经过正则表达式被定义的,好比说,js中的保留字,就是lexicons 的一部分。

下面就是js中的保留字的正则表达式 的一部分。

/^(?!(?:do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$)*$/
复制代码

syntaxes

syntaxes 一般是被一个叫无上下文语法所定义,关于无上下文语法能够点击这个连接,反正只须要知道,无上下文语法要比常规的语法更复杂就行了。

BNF范式

非科班出身的前端可能不了解 BNF 范式(说的就是我 --),它是一种形式化符号来描述给定语言的语法。

它的内容大体为:

  1. 在双引号中的字("word")表明着这些字符自己。
  2. 而double_quote用来表明双引号。
  3. 在双引号外的字(有可能有下划线)表明着语法部分。
  4. 尖括号( < > )内包含的为必选项。
  5. 方括号( [ ] )内包含的为可选项。
  6. 大括号( { } )内包含的为可重复0至无数次的项。
  7. 竖线( | )表示在其左右两边任选一项,至关于"OR"的意思。
  8. ::= 是“被定义为”的意思。

下面是用BNF来定义的Java语言中的For语句的实例。

FOR_STATEMENT ::=
"for" "(" ( variable_declaration |
( expression ";" ) | ";" )
[ expression ] ";"
[ expression ]
")" statement
复制代码

BNF 的诞生仍是挺有意思的一件事情, 有了BNF才有了真正意义上的计算机语言。巴科斯范式直到今天,仍然是个迷,巴科斯是如何想到的

小结

咱们如今对解析过程有了一个大体的了解,总结成一张图就是这样:

对解析(parse)有了初步的了解以后,咱们看一下HTML的解析过程。

解析HTML

HTML是不规范的,咱们在写html的代码时候,好比说漏了一个闭合标签,浏览器也能够正常渲染没有问题的。这是一把双刃剑,咱们能够很容易的编写html, 可是却给html的解析带来很多的麻烦,更详细的信息能够点击:连接

HTML lexicon

Html 的 lexicon 主要包括6个部分:

  • doctype
  • start tag
  • end tag
  • comment
  • character
  • End-of-file

当一个html文档被lexer 处理的时候,lexer 从文档中一个字符一个字符的读出来,而且使用 finite-state machine 来判断一个完整的token是否已经被完整的收到了。

HTML syntax

这里就是html 解析的复杂所在了。html 标签的容错性很高,须要上下文敏感的语法。

好比说对于下面两段代码:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <title>Valid HTML</title>
  </head>
  <body>
    <p>This is a paragraph. <span>This is a span.</span></p>
    <div>This is a div.</div>
  </body>
</html>
复制代码
<html lAnG = EN-US>
<p>This is a paragraph. <span>This is a span. <div>This is a div.
复制代码

第一段是规范的html代码,第二段代码有很是多的错误,可是这两段代码在浏览器中都是大体相同的结构:

上面两处代码渲染出来的惟一的不一样就是,正确的html会在头部有<!DOCTYPE html>, 这行代码会触发浏览器的标准模式。

因此你看,html 的容错性是很是高的,这样是有代价的,这增长了解析的困难,让词法解析解析更加困难。

DOM Tree

HTML 解析出来的产物,通过加工,就获得了DOM Tree。

对于下面这种html的结构:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  </head>
  <body>
    <p>
      This is text in a paragraph.
      <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Rubber_Duck_%288374802487%29.jpg/220px-Rubber_Duck_%288374802487%29.jpg">
    </p>
    <div>
      This is text in a div.
    </div>
  </body>
</html>
复制代码

上面的html 的结构解析出来应该是:

说完了html的解析,咱们就该说CSS的解析了。

解析CSS

和html 解析相比,css 的解析就简单不少了。

CSS lexicon

关于css的 lexicon, the W3C’s CSS2 Level 2 specification 中已经给出了。

CSS 中的 token 被列在了下面,下面的定义是采用了Lex风格的正则表达式。

IDENT	{ident}
ATKEYWORD	@{ident}
STRING	{string}
BAD_STRING	{badstring}
BAD_URI	{baduri}
BAD_COMMENT	{badcomment}
HASH	#{name}
NUMBER	{num}
PERCENTAGE	{num}%
DIMENSION	{num}{ident}
URI	url\({w}{string}{w}\)
|url\({w}([!#$%&*-\[\]-~]|{nonascii}|{escape})*{w}\)
UNICODE-RANGE	u\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?
CDO	<!--
CDC	-->
:	:
;	;
{	\{
}	\}
(	\(
)	\)
[	\[
]	\]
S	[ \t\r\n\f]+
COMMENT	\/\*[^*]*\*+([^/*][^*]*\*+)*\/
FUNCTION	{ident}\(
INCLUDES	~=
DASHMATCH	|=
DELIM	any other character not matched by the above rules, and neither a single nor a double quote
复制代码

花括号里面的宏被定义成以下:

ident	[-]?{nmstart}{nmchar}*
name	{nmchar}+
nmstart	[_a-z]|{nonascii}|{escape}
nonascii	[^\0-\237]
unicode	\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
escape	{unicode}|\\[^\n\r\f0-9a-f]
nmchar	[_a-z0-9-]|{nonascii}|{escape}
num	[0-9]+|[0-9]*\.[0-9]+
string	{string1}|{string2}
string1	\"([^\n\r\f\\"]|\\{nl}|{escape})*\" string2 \'([^\n\r\f\\']|\\{nl}|{escape})*\' badstring {badstring1}|{badstring2} badstring1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\\?
badstring2	\'([^\n\r\f\\']|\\{nl}|{escape})*\\?
badcomment	{badcomment1}|{badcomment2}
badcomment1	\/\*[^*]*\*+([^/*][^*]*\*+)*
badcomment2	\/\*[^*]*(\*+[^/*][^*]*)*
baduri	{baduri1}|{baduri2}|{baduri3}
baduri1	url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}
baduri2	url\({w}{string}{w}
baduri3	url\({w}{badstring}
nl	\n|\r\n|\r|\f
w	[ \t\r\n\f]*
复制代码

CSS Syntax

下面是css的 syntax 定义:

stylesheet  : [ CDO | CDC | S | statement ]*;
statement   : ruleset | at-rule;
at-rule     : ATKEYWORD S* any* [ block | ';' S* ];
block       : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
ruleset     : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*;
selector    : any+;
declaration : property S* ':' S* value;
property    : IDENT;
value       : [ any | block | ATKEYWORD S* ]+;
any         : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
              | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
              | DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
              | '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
              ] S*;
unused      : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*;
复制代码

CSSOM Tree

CSS解析获得的parse tree 通过加工以后,就获得了CSSOM TreeCSSOM 被称为“css 对象模型”。

CSSOM Tree 对外定义接口,能够经过js去获取和修改其中的内容。开发者能够经过document.styleSheets的接口获取到当前页面中全部的css样式表。

CSSOM

那么CSSOM 到底长什么样子呢,咱们下面来看一下:

<!doctype html>
<html lang="en">
<head>
    <style>
        .test1 {
            color: red;
        }
    </style>
    <style>
        .test2 {
            color: green;
        }
    </style>
    <link rel="stylesheet" href="./test3.css">
</head>
<body>
    <div class="test1">TEST CSSOM1</div>
    <div class="test2">TEST CSSOM2</div>
    <div class="test3">TEST CSSOM3</div>
</body>
</html>
复制代码

上面的代码在浏览器中打开,而后在控制台里面输入document.styleSheets,就能够打印出来CSSOM,以下图所示:

image-20180712113259360

能够看到,CSSOM是一个对象,其中有三个属性,均是 CSSStylelSheet 对象。CSSStylelSheet 对象用于表示每一个样式表。因为咱们在document里面引入了一个外部样式表,和两个内联样式表,因此CSSOM对象中包含了3个CSSStylelSheet对象。

CSSStyleSheet

CSSStylelSheet对象又长什么样子呢?以下图所示:

image-20180712113334918

CSSStyleSheet 对象主要包括下面的属性:

  • type

    字符串 “text/css”

  • href

    表示该样式表的来源,若是是内联样式,则href值为null

  • parentStyleSheet

    父节点的styleSheet

  • ownerNode

    该样式表所匹配到的DOM节点,若是没有则为空

  • ownerRule

    父亲节点的styleSheet中的样式对本节点的合并

  • media

    该样式表中相关联的 MediaList

  • title

    style 标签的title属性,不常见

    <style title="papaya whip"> body { background: #ffefd5; } </style>
    复制代码
  • disabled

    是否禁用该样式表,可经过js控制该属性,从而控制页面是否应用该样式表

样式表的解析

浏览器的渲染引擎是从上往下进行解析的。

当渲染引擎遇到 <style> 节点的时候,会立马暂停解析html, 转而解析CSS规则,一旦CSS规则解析完成,渲染引擎会继续解析html

当渲染引擎遇到 <link>节点的时候,浏览器的网络组件会发起对 style 文件的请求,同时渲染引擎不会暂停,而是继续往下解析。等到 style 文件从服务器传输到浏览器的时候,渲染引擎立马暂停解析html, 转而解析CSS规则,一旦CSS规则解析完成,渲染引擎会继续解析html

能够联想一下script的解析。

当渲染引擎遇到 <script> 节点的时候,会立马暂停解析html

若是这个 <script> 节点是内联,则 JS 引擎会立刻执行js代码,同时渲染引擎也暂停了工做。何时等 JS 代码执行完了,何时渲染引擎从新继续工做。若是JS 代码执行不完,那渲染引擎就继续等着吧。

若是这个 <script> 节点是外链的,浏览器的网络组件会发起对 script 文件的请求,渲染引擎也暂停了执行。何时等 JS 代码下载完毕,而且执行完了,何时渲染引擎从新继续工做。

在2011年的时候,浏览器厂商推出了“推测性”解析的概念。

“推测性”解析就是,当让渲染引擎干等着js代码下载和运行的时候,会起一个第三个进程,继续解析剩下的html。当js代码下载好了,准备开始执行js代码的时候,第三个进程就会立刻发起对剩下html所引用的资源——图片,样式表和js代码的请求。

这样就节省了以后加载和解析时间。

被称为“推测性”解析是由于,前面的js代码存在必定的几率修改DOM节点,有可能会让后面的DOM节点消逝,那么咱们的工做就白费了。浏览器“推测”这样的发生的几率比较小。

让渲染引擎干等着不工做是很是低效率的,因此雅虎军规会让把 script 标签放在body的底部。

言归正传,样式表放在head的前边,有两个缘由:

  1. 尽快加载样式表
  2. 不要耽误js代码选择dom节点

Render Tree

当浏览器忙着构建DOM TreeCSSOM Tree的时候, 浏览器同时将二者结合生成Render Tree。也就是说,浏览器构建DOM TreeCSSOM Tree ,和结合生成Render Tree,这两个是同时进行的。

Render Forest

Levi Weintraub(webkit 的做者之一)在一次分享(分享的视频点这里分享的ppt点这里)中开玩笑说,准确的来讲,咱们你们提的Render Tree应该是Render Forest (森林)。由于事实上,存在多条Tree:

  • render object tree ( 稍后会详细讲解)
  • layer tree
  • inline box tree
  • style tee

这里作一点说明。

有不少其余的文章中提到了 Render Tree,其中的每个构成的节点都是 Render Obejcts, 所以其余文章中的 Render Tree 概念,在本文中等同于 Render Object Tree ( Levi Weintraub 和 Webkit core 的叫法都是Render Object Tree, 其余文章中 Render Tree的本义也应是 Render Object Tree)。

Render Object Tree 与 Dom Tree

DOM TreeRender Object Tree 之间的关系是什么样的?

Render Object Tree 并不严格等于Dom Tree,先看一张DOM Tree 和 Render Object Tree的直观的对比图:

image-20180711153615199

上面左侧DOM tree的节点对应右侧Render Object Tree上的节点。细心的你会注意到,上图左侧的DOM Tree中的HTMLDivElement 会变成RenderBlock, HTMLSpanElement 会变成RenderInline,也就是说,DOM节点对应的 render object 节点并不同。

DOM节点对应的 render object 节点并不同分这几种状况:

  1. display : none 的DOM 节点没有对应的 Render Object Tree 的节点

    这里的display:none 属性,有多是咱们在CSS里面设置的,也有多是浏览器默认的添加的属性。好比说下面的元素就会有默认的display:none的属性。

  • <head>
  • <meta>
  • <link>
  • <script>
  1. 一个DOM节点,可能有多个 Render Object Tree的节点

下面的各个DOM元素,会对应多个Render Object Tree的节点

  • <input type="**color**">

  • <input type="**date**">

  • <input type="**file**">

  • <input type="**range**">

  • <input type="**radio**">

  • <input type="**checkbox**">

  • <select>

    好比说,<input type="range"> 就会有两个renderer:

  1. 脱离了文档流的DOM节点,DOM Tree 和 Render Object Tree 是对应不上的。

脱离文档流的状况,要么是float, 要么是position: absolute / fixed

好比说对于下面的结构:

<body>	
  <div>
    <p>Lorem ipsum</p>
  </div>
</body>
复制代码

​ 它的 DOM treeRender Tree 以下图所示:

​ 若是增长脱离文档流的样式,以下:

p {
  position: absolute;
}
复制代码

​ 状况就会变成下面这样:

<p> 节点对应的 Render Tree 的节点,从父节点脱离出来,挂到了顶部的RenderView 节点下面。

为何脱离了文档流的节点,在 Render Object Tree中的结构不一样?脱离了文档流的节点在构建Render Object Tree又是如何处理的?会在下面的内容中介绍。

Render Object Tree 上的节点

render object tree 是由 render object 节点构成的。render object 节点在不一样的浏览器叫法不一样,在webkit中被称为 renderer, 或者 被称为 render objects, 在firfox中,被称为frames

render object 的节点的类是 RenderObject,定义在源码的目录webkit/Source/WebCore/rendering/RenderObject.h中。

下面是RenderObject.h的简化版本:

// Credit to Tali Garsiel for this simplified version of WebCore's RenderObject.h
class RenderObject {
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  // 这个render tree的节点所指向的那个Dom节点
  RenderStyle* style;  // 这个render tree节点的计算出来的样式
  RenderLayer* containingLayer;  // 包含这个 render tree 的 z-index layer
}
复制代码

RenderBox 是RenderObject的一个子类,它主要是负责DOM树上的每个节点的盒模型。

RenderBox 包括一些计算好尺寸的信息,好比说:

  • height
  • width
  • padding
  • border
  • margin
  • clientLeft
  • clientTop
  • clientWidth
  • clientHeight
  • offsetLeft
  • offsetTop
  • offsetWidth
  • offsetHeight
  • scrollLeft
  • scrollTop
  • scrollWidth
  • scrollHeight

render object 的节点的做用以下:

  • 负责 layout 和 paint

  • 负责查询DOM元素查询尺寸API

    好比说获取offsetHeight, offsetWidth的属性

render object 节点的类型

咱们在CSS中接触过文档流的概念,文档流中的元素分为块状元素和行内元素,好比说div是块状元素,span是行内元素。块状元素和行内元素在文档流中的表现不一样,就是在这里决定的。

Render Object 的节点类型分为下面几种:

  1. RenderBlock

    display: block 的DOM节点对应的render object节点类型为RenderBlock

  2. RenderInline

    display:inline 的DOM节点对应的render object节点类型为RenderInline

  3. RenderReplaced

    可能咱们以前据说过“替换元素” 的概念,好比说常见的“替换元素”有下面:

    • <iframe>
    • <video>
    • <embed>
    • <img>

    为啥被称为“替换元素”,是由于他们的内容会被一个独立于HTML/CSS上下文的外部资源所替代。

    “替代元素” 的DOM节点对应的render object 节点类型为RenderReplaced

  4. RenderTable

    <table> 元素的DOM节点对应的render object 节点类型为 RenderTable

  5. RenderText

    文本内容的DOM节点对应的render object 的节点类型为 RenderText

源码大概长这个样子:

RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
    Document* doc = node->document();
    RenderArena* arena = doc->renderArena();
    ...
    RenderObject* o = 0;

    switch (style->display()) {
        case NONE:
            break;
        case INLINE:
            o = new (arena) RenderInline(node);
            break;
        case BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case INLINE_BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case LIST_ITEM:
            o = new (arena) RenderListItem(node);
            break;
       ...
    }

    return o;
}
复制代码

上面5中类型的Render Object 的节点之间的关系组合并非没有准则的,在咱们写出嵌套不规范的HTML时,渲染引擎帮咱们作了不少事情。

Anonymous renderers

render object tree 遵照2个准则:

  • 在文档流中的块状元素的子节点,要么都是块状元素,要么都是行内元素。
  • 在文档流中的行内元素的子节点,只能都是行内元素。

anonymounse renderers(匿名的render object 节点)就是用于处理不遵照这两种规则的代码的,若是出现不符合这两个准则的状况,好比说下面:

  1. 若在块状元素里面同时出现了块状元素和行内元素:
<div>
  Some text
  <div>
    Some more text
  </div>
</div>
复制代码

上面的代码中,最外层的div节点有两个子节点,第一个子节点是行内元素,第二个子节点是块状元素。render object tree 中会构建一个anonymounse renderer去包裹 text 节点,所以上面的代码变成了下面的:

<div>
  <anonymous block>
    Some text
  </anonymous block>
  <div>
   Some more text
  </div>
</div>
复制代码
  1. 还有另一种很是糟糕的状况,就是在行内元素中出现了块状元素:
<i>Italic only <b>italic and bold
  <div>
    Wow, a block!
  </div>
  <div>
    Wow, another block!
  </div>
More italic and bold text</b> More italic text</i>

复制代码

上面的代码中,render object tree须要作更多的事情去修复这种糟糕的DOM tree: 三个anonymounse renderers会被建立,上面的代码会被分割成三段,被三个匿名的block 包裹。

<anonymous pre block>
  <i>Italic only <b>italic and bold</b></i>
</anonymous pre block>

<anonymous middle block>
  <div>
  Wow, a block!
  </div>
  <div>
  Wow, another block!
  </div>
</anonymous middle block>

<anonymous post block>
  <i><b>More italic and bold text</b> More italic text</i>
</anonymous post block>

复制代码

注意到,<i> 元素和 <b> 元素都被分割进了<anonymous pre block><anonymous post block> 两个类型为 RenderBlock 的节点中,他们经过一种叫*continuation chain(延续链)*的机制来连接。

负责上面递归拆分行内元素的生产*continuation chain(延续链)*的方法被称为 splitFlow

所以,一旦你写出了不符合规范的html结构, 在构建render object tree时就须要更多的工做去纠正,从而形成页面性能的降低。

构建 Render Object Tree

GeckoWebKit 采用了不一样的方案来构建 Render Tree

Gecko 是把样式计算和构建Render Object Tree 的工做代理到 FrameConstructor 对象上。而 webkit 采用的方案是,每个DOM节点本身计算本身的样式,而且构建本身 的Render object tree 对应的节点。

Gecko 针对DOM的更新增长了一个 listener,当DOM 更新的时候,更新的DOM节点被传到一个指定的对象FrameConstructor, 这个FrameConstructor 会为 DOM 节点计算样式,同时为这个DOM节点建立一个合适的 Render Object Tree节点

WebKit构建 Render Object tree 的过程被称为attachment, 每个DOM节点被赋予一个 attach() 方法,这是一个同步的方法,当每个DOM节点被插入DOM树的时候, 该DOM节点的attach()方法就会被调用。

样式计算

在构建Render Object Tree的时候,须要进行样式计算,也就是Render Tree每个节点都须要有一个visual information的信息,才能够被绘制在屏幕上,这就须要样式计算这一过程。

而样式计算须要两部分“原材料”:

  1. DOM Tree
  2. 一堆样式规则

DOM Tree在HTML解析以后就能够拿到了,一堆样式规则能够来自下面:

  • 浏览器默认的样式
  • 外链样式
  • 内联样式
  • DOM节点上的style属性

那么样式规则是如何构成的呢?

  • 样式表是一堆 规则(rules)的集合;
  • 固然也不光都是 规则(rules), 还会有一些奇怪的东西:@import, @media, @namespace 等等
  • 一个**规则(rules)是由选择器(selector)声明块(declaration block)**构成的
  • **声明块(declaration block)由一堆声明(declaration)**加中括号构成
  • **声明(declaration ** 由 property 和 value 构成。

样式计算存在如下三个难点:

  1. style 样式数据太多,会占用大量内存
  2. 匹配元素会影响性能
  3. css规则的应用顺序

下面咱们介绍这个三个难点是如何解决的。

样式规则的应用顺序

某一个DOM节点上可能有多个规则,好比下下面:

div p {
  color: goldenrod;
}
p {
  color: red;
}

复制代码

那么这个DOM节点究竟用的是哪一个规则?

规则的权重是:先看 order , 而后再算specificity, 最后再看哪一个规则靠的更近。

order

order的权重从高到底:

  1. 用户的 ! important 声明(浏览器可让用户导入自定的样式)
  2. 程序员写的 ! important 声明
  3. 程序员写的普通css样式
  4. 浏览器的默认css样
Specificity

Specificity是一个相加起来的值

#foo .bar > [name="baz"]::first-line {} /* Specificity: 0 1 2 1 */

复制代码
  1. 第一位的数值(a)

    是否有DOM节点上style属性的值,有则是1,不然是0

  2. 第二位的数值 (b)

    id选择器的数量之和

  3. 第三位的数值 (c)

    class选择器,属性选择器,伪类选择器个数之和

  4. 第四位的数值 (d)

    标签选择器,伪元素选择器个数之和

下面是例子:

*             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

复制代码

style数据太多,占用大量内存

这里的style数据太多,不是说咱们写的css样式太多,而是Render Object Tree每个节点上都须要存储所有的CSS样式,那些没有被指定的样式,其值为继承父节点的样式,或者是浏览器的默认样式,或者干脆是个空值。

webkit 和 gecko 采用了不一样的解决方案。

webkit:共享样式数据

WebKit 的解决方案是,节点们会引用RenderStyle对象。这些对象在如下状况下能够由不一样的DOM节点共享,从而节省空间和提升性能。

  • 这些节点是同级关系
  • 这些节点有相同的伪类状态:hover、:active、:focus
  • 这些节点都没有id
  • 这些节点有相同的tag名称
  • 这些节点有相同的class
  • 这些节点都没有经过style属性设置的样式
  • 这些节点没有一个是使用 兄弟选择器的,好比说: div + pdiv ~ p:last-child:first-child:nth-child():nth-of-type()
Gecko:style struct sharing

Gecko 采用了一种 style struct sharing 的机制。有一些css属性能够聚合在一块儿,好比说font-size, font-family, color 等等,浏览器就把这些能够被划分为一组的属性,单独的保存到一个对象里面,这个对象被称为 style struct。以下表所示:

image-20180705203212950

上图中的,computed style 里就不用存储CSS所有的200多个属性,而是保存着对这些 style struct对象的引用。

这样一来,一些具备相同属性的DOM 节点就能够引用相同的 style struct, 不只如此,由于子节点有一些属性会继承父节点,那么保存这些属性的 style struct 就会被父节点和子节点所共享。

匹配元素会影响性能

对于每个DOM节点,css引擎须要去遍历全部的css规则看是否匹配。对于大部分的DOM节点,css规则的匹配并不会发生改动。

好比说,用户把鼠标hover到一个父元素上面,这个父元素的css规则匹配是发生了变化,咱们不只仅要从新计算这个父元素的样式,还须要从新计算这个父元素的子元素的样式(由于要处理继承的样式),可是能匹配这些子元素的样式规则,是不会变的。

若是咱们能记下来,有哪些selector能够匹配到就行了。

为了优化这一点,CSS 引擎在进行 selector 匹配时,会根据权重的顺序把他们排成一串,而后把这一串加到右边的 CSS rule tree 上面。

image-20180706013726314

CSS引擎但愿右边CSS rule tree 的分支数越少越好,所以会将新加入的一串尽可能的合并到已有的分支,因此上面的过程会是下面这样的:

image-20180706013959120

而后遍历每个DOM节点去找能匹配到的CSS Rule Tree的分支,从CSS rule Tree的底部开始,一路向上开始匹配,直到找到对应的那一条 style rule Tree分支。这就是人们口中常说的,css选择器是从右边匹配的。

当浏览器由于某种缘由(用户交互,js修改DOM)进行从新渲染的时候,CSS引擎会快速检查一下,对父节点的改动是否会影响到子节点的 selector 匹配,若是不影响,CSS引擎就直接拿到每个子节点对CSS rule Tree 对应那个分支的指针,节省掉匹配选择器的时间。

尽管如此,咱们仍是须要在第一次遍历每个DOM节点去找到对应的CSS Rule Tree的分支。若是咱们有10000个相同的节点,就须要遍历10000次。

Gecko 和 Webkit 都对此进行了优化,在遍历完一个节点以后,会把计算好的样式放到缓存中,在遍历下一个节点以前,会作一个判断,看是否能够复用缓存中的样式。

这个判断包括一下几点:

  1. 两个节点是否有相同的id, class

  2. 两个节点是否有相同的style 属性

  3. 两个节点对应的父亲节点是否共享一份计算好的样式,那该两个节点继承的样式也是相同的。

    image-20180706021216963

解析阶段如何优化

更加符合规范的html结构

上面在构建render object tree 的过程当中,会额外作不少工做处理咱们不符合规范的DOM 结构,好比说,调用splitflow 方法分割代码,用 anonymous renderBlock 包裹不符合规范的节点。

以前咱们都听过建议:“要编写更有语义,更符合规范的html结构“,缘由就在于此,可让渲染引擎作更少的事情。

下面是模拟一种不不符合规范的状况:

<i v-for="n in 1000">
        Italic only
        <b>italic and bold
          <div>
            Wow, a block!
          </div>
          <div>
            Wow, another block!
            <b>More italic and bold text</b>
            <div>
              More italic and bold text
              <p>More italic and bold text</p>
            </div>
          </div>
          More italic and bold text
          More italic and bold text
        </b> More italic text
      </i>

复制代码

在控制台里面,设置cpu 为6x slowdown,而后记录渲染数据以下:

image-20180712122251219

其中花费了 12888ms 进行了rendering 过程。

若是咱们对html代码仅仅作几处修改,在不考虑css优化、样式优化的前提下:

<div v-for="n in nums">
        <p>Italic only</p>
        <div>italic and bold
          <div>
            Wow, a block!
          </div>
          <div>
            Wow, another block!
            <b>More italic and bold text</b>
            <div>
              More italic and bold text
              <p>More italic and bold text</p>
            </div>
          </div>
          More italic and bold text
          More italic and bold text
        </div>
      </div>

复制代码

在控制台里面,设置cpu 为6x slowdown,而后记录渲染数据以下:

image-20180712122830930

能够发现,render 阶段的渲染时间为11506ms,rendering 阶段渲染的时间相比于12888ms减小了1382ms,时间缩短了12%

一次测量可能有偏差,但不管进行屡次测量,都会发现第二种的代码的渲染时间要小于第一种代码的渲染时间。

选择器的优化

不一样的选择器,匹配的效率会有差距,可是差距不大。

咱们用一个有1000个DOM节点的页面来测试,分别在5个浏览器中尝试如下20种匹配器:

1. Data Attribute (unqualified)
	*/
	[data-select] {
		color: red;
	}

	/*
		2. Data Attribute (qualified)
	

	a[data-select] {
		color: red;
	}
	*/
	

	/*
		3. Data Attribute (unqualified with value)
	

	[data-select="link"] {
		color: red;
	}
	*/


	/*
		4. Data Attribute (qualified with value)
	

	a[data-select="link"] {
		color: red;
	}
	*/


	/*
		5. Multiple Data Attributes (qualified with values)
	

	div[data-div="layer1"] a[data-select="link"] {
		color: red;
	}
	*/


	/*
		6. Solo Pseudo selector
	

	a:after {
		content: "after";
		color: red;
	}
	*/


	/*
		7. Combined classes
	

	.tagA.link {
		color: red;
	}
	*/


	/*
		8. Multiple classes 
	

	.tagUl .link {
		color: red;
	}
	*/


	/*
		9. Multiple classes (using child selector)
	
	.tagB > .tagA {
		color: red;
	}
	*/


	/*
		10. Partial attribute matching

	[class^="wrap"] {
		color: red;
	}	
	*/


	/*
		11. Nth-child selector
	
	.div:nth-of-type(1) a {
		color: red;
	}
	*/


	/*
		12. Nth-child selector followed by nth-child selector
	
	.div:nth-of-type(1) .div:nth-of-type(1) a {
		color: red;
	}
	*/


	/*
		13. Insanity selection (unlucky for some)
	
	div.wrapper > div.tagDiv > div.tagDiv.layer2 > ul.tagUL > li.tagLi > b.tagB > a.TagA.link {
		color: red;
	}
	*/


	/*
		14. Slight insanity
	
	.tagLi .tagB a.TagA.link {
		color: red;
	}
	*/


	/*
		15. Universal
	
	* {
		color: red;
	}
	*/


	/*
		16. Element single
	
	a {
		color: red;
	}
	*/


	/*
		17. Element double
	
	div a {
		color: red;
	}
	*/


	/*
		18. Element treble
	
	div ul a {
		color: red;
	}
	*/


	/*
		19. Element treble pseudo
	
	div ul a:after; {
		content: "after";
		color: red;
	}
	*/


	/*
		20. Single class
	
	.link {
		color: red;
	}
复制代码

测试的结果以下:

Test Chrome 34 Firefox 29 Opera 19 IE9 Android 4
1 56.8 125.4 63.6 152.6 1455.2
2 55.4 128.4 61.4 141 1404.6
3 55 125.6 61.8 152.4 1363.4
4 54.8 129 63.2 147.4 1421.2
5 55.4 124.4 63.2 147.4 1411.2
6 60.6 138 58.4 162 1500.4
7 51.2 126.6 56.8 147.8 1453.8
8 48.8 127.4 56.2 150.2 1398.8
9 48.8 127.4 55.8 154.6 1348.4
10 52.2 129.4 58 172 1420.2
11 49 127.4 56.6 148.4 1352
12 50.6 127.2 58.4 146.2 1377.6
13 64.6 129.2 72.4 152.8 1461.2
14 50.2 129.8 54.8 154.6 1381.2
15 50 126.2 56.8 154.8 1351.6
16 49.2 127.6 56 149.2 1379.2
17 50.4 132.4 55 157.6 1386
18 49.2 128.8 58.6 154.2 1380.6
19 48.6 132.4 54.8 148.4 1349.6
20 50.4 128 55 149.8 1393.8
Biggest Diff. 16 13.6 17.6 31 152
Slowest 13 6 13 10 6

解释

在浏览器的引擎内部,这些选择器会被从新的拆分,组合,优化,编译。而不一样的浏览器内核采用不一样的方案,因此几乎没有办法预测,选择器的优化究竟能带来多少收益。

结论:

合理的使用选择器,好比说层级更少的class,的确会提升匹配的速度,可是速度的提升是有限的 。

若是你经过dev tool 发现匹配选择器的确是瓶颈,那么就选择优化它。

精简没有用的样式代码

大量无用代码会拖慢浏览器的解析速度。

用一个3000行的无用css样式表和1500行的无用样式表,进行测试:

Test Chrome 34 Firefox 29 Opera 19 IE9 Android 4
3000 64.4 237.6 74.2 436.8 1714.6
1500 51.6 142.8 65.4 358.6 1412.4

对于火狐来讲,在其余环节一致的状况下,页面渲染的速度几乎提高了一倍

尽管如今的惯例是把css 打包成一个巨大单一的css文件。这样作的确是有好处的,减小http请求的数量。可是拆分css文件可让加载速度更快,浏览器的解析速度更快。

这一项的优化是很是显著的,一般能够省下来 2ms ~ 300ms的时间。

精简的过程可使用uncss 工具来自动化的完成。

浏览器渲染第二步:layout

在上一节咱们提到了 render object treerender object 的节点第一次被建立而后添加到 render object tree时,它身上没有关于位置和尺寸的信息。接下来,肯定每个render object的位置和尺寸的过程被称为layout。

咱们能在不一样的文章中看到不一样的名词: 布局layout , 回流reflow , 这些名词说的都是一回事,不一样浏览器的叫法不一样。

每个renderer节点 都有layout 方法。 在构建renderer节点的时候就声明了这个方法:

class RenderObject {
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  // 这个render tree的节点所指向的那个Dom节点
  RenderStyle* style;  // 这个render tree节点的计算出来的样式
  RenderLayer* containingLayer;  // 包含这个 render tree 的 z-index layer
}
复制代码

layout ()是一个递归的过程。layout 过程到底是谁来负责的呢? 一个名为 FrameView 的 class。

FrameView 能够运行下面两种类型的 layout :

  1. 全局layout

    render tree 的根节点自身的layout方法被调用,而后整个render tree 被更新。

  2. 局部layout

    只是区域性的更新,只适用于某个分支的改动不会影响到周围的分支。

    目前局部layout只会在 text 更新的时候使用

Dirty Bits

在layout 阶段,采用一种称为 Dirty Bits 的机制去判断一个节点是否须要layout。当一个新的节点被插到tree中时,它不只仅“弄脏“了它自身,还“弄脏“了相关的父节点(the relevant ancestor chain,下面会介绍)。有没有被“弄脏”是经过设置bits (set bits)来标识的。

bool needsLayout() const { return m_needsLayout || m_normalChildNeedsLayout || m_posChildNeedsLayout; }
复制代码

上面 needsLayout 为 true 有三种状况:

  1. selfNeedsLayout

    Rederer 自身是 “脏”的。当一个 rederer 自身被设置为“脏”的,它相关的父亲节点也会被设置一个标识来指出它们有一个“脏”的子节点

  2. posChildNeedsLayout

    设置了postion不为static的子节点被弄脏了

  3. normalChildNeedsLayout

    在文档流中的子节点被弄脏了

上面之因此要区分子节点是否在文档流中,是为了layout过程的优化。

Containing Block (包含块)

上面提到了相关父节点(the relevant ancestor chain),那么到底是如何判断哪一个节点是 **相关父节点 **?

答案就是经过 containing block.

Container Block(包含块) 身份有两个

  1. 子节点的相关的父节点

  2. 子节点的相对坐标系

    子节点都有 XPos 和 YPos 的坐标,这些坐标都是相对于他们的Containing Block (包含块)而言的。

下面介绍Container Block(包含块) 概念。

包含块的定义

通俗来说,Container Block 是决定子节点位置的父节点。每一个子节点的位置都是相对于其container block来计算的。更详细的信息能够点这个 css2.1 官方的解释点这里

有一种特殊的containing block —— initial containing block (最初的container block)。

当Docuement 节点上的 renderer() 方法被调用时,会返回一个节点对象为render tree 的根节点,被称做 RenderView, RenderView 对应的containing bock 就是 initial containing block。

initial containing block 的尺寸永远是viewport的尺寸,且永远是相对于整个文档的 position(0,0) 的位置。下面是图示:

image-20180712172110827

黑色的框表明的是 initial containing block (最初的container block) , 灰色的框表示整个 document。当document往下滚动的时候, initial containing block (最初的container block) 就会被移出了屏幕。 initial containing block (最初的container block) 始终在document 的顶部,而且大小始终是 viewport 的尺寸。

那么render Tree上的节点,它们各自的 containing block 是什么?

  • 根节点的 containing block 始终是 RenderView

  • 若是一个renderer节点的css postion 的值为 relative 或 static,则其 containing block 为最近的父节点

  • 若是一个renderer节点的css postion 的值为 absolute, 则其containing block 为最近的 css postion 的值不为static 的父节点。若是这样的父节点不存在,则为 RenderView,也就是根节点的containing block

  • 若是一个renderer节点的css postion 的值为 fixed。这个状况有一些特殊,由于 W3C 标准和 webkit core 介绍的不同。W3C 最新的标准认为css postion 的值为 fixed的renderer节点的containing block是viewport ,原文以下:

    image-20180712230128732

    而webkit core 认为css postion 的值为 fixed的renderer节点的containing block是RenderView。RenderView并不会表现的和viewport同样,可是RenderView会根据页面滚动的距离算出css postion 的值为 fixed的renderer节点的位置。这是由于单独为viewport 生成一个renderer 节点并不简单。原文以下:

    image-20180712230517294

render tree 有两个方法用来判断 renderer 的position:

bool isPositioned() const;   // absolute or fixed positioning
bool isRelPositioned() const;  // relative positioning

复制代码

render tree 有一个方法用来获取某一个块状 rederer 的containing block(相对父节点)

RenderBlock* containingBlock() const

复制代码

render tree 还有一个方法是兼容了行内元素获取相对父节点的方法,用来代替containingBlock (由于containingBlock只适用于块状元素)

RenderObject* container() const

复制代码

当一个 renderer 被标记为须要 layout的时候,就会经过container()找到相对父节点,把isPositioned 的状态传递给相对父节点。若是 renderer 的position是absolute 或 fixed ,则相对父节点的posChildNeedsLayout为true,若是 renderer的position 是 static 或 relative , 则相对父节点的 normalChildNeedsLayout 为 true。

会触发layout 的属性

  1. 盒子模型相关的属性

    • width

    • height

    • padding

    • margin

    • border

    • display

    • ……
  2. 定位属性和浮动

    • top
    • bottom
    • left
    • right
    • position
    • float
    • clear
  3. 节点内部的文字结构

    • text - aligh
    • overflow
    • font-weight
    • font- family
    • font-size
    • line-height

上面只是一部分,更所有的能够点击 csstriggers 来查阅;

csstrigger 里面须要注意的有几点。

  1. opacity的改动,在blink内核和Gecko内核上不会触发layout 和 repaint

    image-20180706152622532

  2. transform的改动,在blink内核和Gecko内核上不会触发layout 和 repaint

    image-20180706152905325

  3. visibility 的改动,在Gecko 内核上不会触发 layout repaint, 和 composite

    image-20180706153256502

会触发layout 的方法

几乎任何测量元素的宽度,高度,和位置的方法都会不可避免的触发reflow, 包括可是不限于:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • and a lot more…

如何避免重复Layout

不要频繁的增删改查DOM

不要频繁的修改默认根字体大小

不要一条条去修改DOM样式,而是经过切换className

虽然切换className 也会形成性能上的影响,可是次数上减小了。

“离线”修改DOM

好比说必定要修改这个dom节点100次,那么先把dom的display设置为 none ( 仅仅会触发一次回流 )

使用flexbox

老的布局模型以相对/绝对/浮动的方式将元素定位到屏幕上 Floxbox布局模型用流式布局的方式将元素定位到屏幕上,flex性能更好。

不要使用table

使用table布局哪怕一个很小的改动都会形成从新布局

避免强制性的同步layout

layout根据区域来划分的,分为全局性layout, 和局部的layout。好比说修改根字体的大小,会触发全局性layout。

全局性layout是同步的,会马上立刻被执行,而局部性的layout是异步的,分批次的。浏览器会尝试合并屡次局部性的layout为一次,而后异步的执行一次,从而提升效率。

可是js一些操做会触发强制性的同步布局,从而影响页面性能,好比说读取 offsetHeight、offsetWidth 值的时候。

浏览器渲染第三步:paint

第三个阶段是paint 阶段

会触发paint 的属性

  • color
  • border - style
  • border - radius
  • visibility
  • Text -decoration
  • background
  • background
  • Background - image
  • background - size
  • Background - repeat
  • background - position
  • outline - color
  • outline
  • outline - style
  • outline - width
  • box - shadow

如何优化

使用transform代替top, left 的变化

使用transform不会触发layout , 只会触发paint。

若是你想页面中作一些比较炫酷的效果,相信我,transform能够知足你的需求。

// 位置的变换
transform: translate(1px,2px)

// 大小的变换
transform: scale(1.2)

复制代码

使用opacity 来代替 visibility

由于 visibility属性会触发重绘,而opacity 则不会触发重绘

避免使用耗性能的属性

能够点击这个连接进行测试测试链接

.link {
    background-color: red;
    border-radius: 5px;
    padding: 3px;
    box-shadow: 0 5px 5px #000;
    -webkit-transform: rotate(10deg);
    -moz-transform: rotate(10deg);
    -ms-transform: rotate(10deg);
    transform: rotate(10deg);
    display: block;
}

复制代码

测试结果:

Test Chrome 34 Firefox 29 Opera 19 IE9 Android 4
Expensive Styles 65.2 151.4 65.2 259.2 1923

须要注意的是,高耗css样式若是不会频繁的触发回流和重绘,只会在页面渲染的时候被执行一次,那么对页面的性能影响是有限的。若是频繁的触发回流和重绘,那么最基本的css样式也会影响到页面的性能。

那么哪些 css 样式会形成页面性能的问题呢?

  • Border-radius
  • Shadow
  • gradients
  • transform rotating

更多的内容请参考 链接

浏览器渲染第四步:composite

什么是合成层

上面几个阶段能够用下面一张图来表示:

1. 从 Nodes 到 LayoutObjects

DOM 树每一个 Node 节点都有一个对应的 LayoutObject 。LayoutObject 知道如何在屏幕上 paint Node 的内容。

2. 从 LayoutObjects 到 PaintLayers

有相同坐标的 LayoutObjects ,在同一个PaintLayer内。 根据建立PaintLayer 的缘由不一样,能够将其分为常见的 3 类:

  1. NormalPaintLayer
  • 根元素
  • relative、fixed、sticky、absolute
  • opacity 小于 1
  • CSS 滤镜(fliter)
  • 有 CSS mask 属性
  • 有 CSS mix-blend-mode 属性(不为 normal)
  • 有 CSS transform 属性(不为 none)
  • backface-visibility 属性为 hidden
  • 有 CSS reflection 属性
  • 有 CSS column-count 属性(不为 auto)或者 有 CSS column-width 属性(不为 auto)
  • 当前有对于 opacity、transform、fliter、backdrop-filter 应用动画
  1. OverflowClipPaintLayer
  • overflow 不为 visible
  1. NoPaintLayer
  • 不须要 paint 的 PaintLayer,好比一个没有视觉属性(背景、颜色、阴影等)的空 div。

4. 从 PaintLayers 到 GraphicsLayers

某些特殊的paintLayer会被当成合成层,合成层拥有单独的 GraphicsLayer,而其余不是合成层的paintLayer,则和其第一个拥有GraphicsLayer 父层公用一个。

每一个 GraphicsLayer 都有一个GraphicsContextGraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,做为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,而后 draw 到屏幕上,此时,咱们的页面也就展示到了屏幕上。

渲染层提高为合成层的缘由

渲染层提高为合成层的缘由有一下几种:

  • 直接缘由
    • 硬件加速的 iframe 元素(好比 iframe 嵌入的页面中有合成层
    • video元素
    • 3d transiform
    • 在 DPI 较高的屏幕上,fix 定位的元素会自动地被提高到合成层中。但在 DPI 较低的设备上却并不是如此
    • backface-visibility 为 hidden
    • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(须要注意的是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提高合成层也会失效)
    • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等须要设置明确的定位属性,如 relative 等)
  • 后代元素缘由
    • 有合成层后代同时自己有 transform、opactiy(小于 1)、mask、fliter、reflection 属性
    • 有合成层后代同时自己 overflow 不为 visible(若是自己是由于明确的定位因素产生的 SelfPaintingLayer,则须要 z-index 不为 auto)
    • 有合成层后代同时自己 fixed 定位
    • 有 3D transfrom 的合成层后代同时自己有 preserves-3d 属性
    • 有 3D transfrom 的合成层后代同时自己有 perspective 属性
  • overlap 重叠缘由

为啥overlap 重叠也会形成提高合成层渲染? 图层之间有重叠关系,须要按照顺序合并图层。

如何优化

若是把一个频繁修改的dom元素,抽出一个单独的图层,而后这个元素的layout, paint 阶段都会在这个图层进行,从而减小对其余元素的影响。

使用will-change 或者 transform3d

使用 will-change 或者 transform3d

1. will-change: transform/opacity
 2. transform3d(0,0,0,)

复制代码

使用加速视频解码的

由于视频中的每一帧都是在动的,因此视频的区域,浏览器每一帧都须要重绘。因此浏览器会本身优化,把这个区域的给抽出一个单独的图层

拥有3D(webgl) 上下文或者加速的2D上下文的 节点

混合插件(flash)

若是某一个元素,经过z-index在复合层上面渲染,则该元素也会被提高到复合层

须要注意的是,gif 图片虽然也变化很频繁,可是 img 标签不会被单独的提到一个复合层,因此咱们须要单独的提到一个独立独立的图层之类。

composite更详尽的知识能够了解下面这个博客: 《GPU Accelerated Compositing in Chrome》

页面性能优化实践

Bounce-btn优化

bounce-btn是相似于下面这种的:

若是想实现这种效果,假设不考虑性能问题,写出下面的代码话:

<div class="content-box"></div>
    <div class="content-box"></div>
    <div class="content-box"></div>
    <div class="bounce-btn"></div>
    <div class="content-box"></div>
    <div class="content-box"></div>
    <div class="content-box"></div>

复制代码
.bounce-btn {
  width: 200px;
  height: 50px;
  background-color: antiquewhite;
  border-radius: 30px;
  margin: 10px auto;
  transition: all 1s;
}
.content-box {
  width: 400px;
  height: 200px;
  background-color: darkcyan;
  margin: 10px auto;
}

复制代码
let btnArr = document.querySelectorAll('.bounce-btn');
setInterval(() => {
  btnArr.forEach((dom) => {
    if ( dom.style.width ==='200px') {
      dom.style.width = '300px';
      dom.style.height = '70px';
    } else {
      dom.style.width = '200px';
      dom.style.height = '50px';
    }
  })
},2000)

复制代码

能够发现这样的性能是很是差的,咱们打开dev-tool的paint flashing, 发现从新渲染的区域如绿色的区域所示:

而此时的性能是,1000ms 的时间内,layout阶段花费了29.9ms占了18.6%

image-20180706144354901

image-20180706144409030

这个其实有两个地方,第一是,bounce btn 这个元素被js 修改了width 、height 这些属性,从而触发了自身layout ——> repaint ——> composite。第二是,bounce btn 没有脱离文档流,它自身布局的变化,影响到了它下面的元素的布局,从而致使下面元素也触发了layout ——> repaint ——> composite

那么咱们把修改width, 改成 tansform: scale()

let btnArr = document.querySelectorAll('.bounce-btn');
setInterval(() => {
  btnArr.forEach((dom) => {
    if ( dom.style.transform ==='scale(0.8)') {
      dom.style.transform = 'scale(2.5)';
    } else {
      dom.style.transform = 'scale(0.8)';
    }
  })
},2000)

复制代码

页面性能获得了提升:

从新渲染的区域只有它自身了。此时的性能是,1000ms 的时间内,没有存在layout阶段,

image-20180706145450446

image-20180706145652251

若是继续优化,咱们经过aimation动画来实现bounce的效果:

@keyframes bounce {
            0% {
                transform: scale(0.8);
            }
            25% {
                transform: scale(1.5);
            }
            50% {
                transform: scale(1.5);
            }
            75% {
                transform: scale(1.5);
            }
            100% {
                transform: scale(0.8);
            }
        }

复制代码

页面中没有从新渲染的区域:

而且页面性能几乎没有受到任何影响,不会从新经历 layout ——> repaint ——> composite.

image-20180706150428460

image-20180706150438553

因此,对于这种动效,优先选择 CSS animation > transform 修改 scale > 绝对定位 修改width > 文档流中修改width

跑马灯的优化

跑马灯的动效是:每隔3秒进行向左侧滑动淡出,而后再滑动从新淡入,更新文本为“**砍价9元”

以前的滑动和淡出的效果是经过vue提供的 <transision> 来实现的

<transision> 原理

当咱们想要用到过渡效果,会在vue中写这样的代码:

<transition name="toggle">
  <div class="test">
</transition>

复制代码

可是其实渲染到浏览器中的代码,会依次是下面这样的:

// 过渡进入开始的一瞬间
<div class="test toggle-enter">

// 过渡进入的中间阶段
<div class="test toggle-enter-active">

// 过渡进入的结束阶段
<div class="test toggle-enter-active toggle-enter-to">


// 过渡淡出开始的一瞬间
<div class="test toggle-leave">

// 过渡淡出的中间阶段
<div class="test toggle-leave-active">

// 过渡淡出的结束阶段
<div class="test toggle-leave-active toggle-leave-to">

复制代码

也就是说,过渡效果的实现,是经过不停的修改、增长、删除该dom节点的class来实现。

<transision> 影响页面性能

一方面, v-if 会修改dom节点的结构,修改dom节点会形成浏览器重走一遍 layout 阶段,也就是重排。另外一方面,dom节点的class被不停的修改,也会致使浏览器的重排现象,所以页面性能会比较大的受到影响。

若页面中 <transition> 控制的节点过多时,页面的性能就会比较受影响。

为了证实,下面代码模拟了一种极端的状况:

<div v-for="n in testArr">
  <transition name="toggle">
    <div class="info-block" v-if="isShow"></div>
  </transition>
</div>

复制代码
export default {
  	data () {
          return {
            isShow: false,
            testArr: 1000
          }
    },
    methods: {
	    toggle() {
	    	var self = this;
	    	setInterval(function () {
		      self.isShow = !self.isShow
	      }, 1000)
      }
    },
    mounted () {
	 this.toggle()
    }
  }

复制代码
.toggle-show-enter {
    transform: translate(-400px,0);
  }

  .toggle-show-enter-active {
    color: white;
  }

  .toggle-show-enter-to {
    transform: translate(0,0);
  }

  .toggle-show-leave {
    transform: translate(0,0);
  }

  .toggle-show-leave-to {
    transform: translate(-400px,0);
  }

  .toggle-show-leave-active {
     color: white;
  }

复制代码

上面的代码在页面中渲染了 1000 个过渡的元素,这些元素会在1秒的时间内从左侧划入,而后划出。

此时,咱们打开google浏览器的开发者工具,而后在 performance 一栏中记录分析性能,以下图所示:

能够发现,页面明显掉帧了。在7秒内,总共 scripting 的阶段为3秒, rendering 阶段为1956毫秒。

事实上,这种跑马灯式的重复式效果,经过 animation 的方式也能够轻松实现。 咱们优化上面的代码,改成下面的代码,经过 animation 动画来控制过渡:

<div v-for="n in testArr">
      <div class="info-block"></div>
    </div>
复制代码
export default {
  	data () {
  	  return {
            isShow: false,
            testArr: 1000
      }
    }
  }
复制代码
.info-block {
  background-color: red;
  width: 300px;
  height: 100px;
  position: fixed;
  left: 10px;
  top: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: toggleShow 3s ease 0s infinite normal;
}

@keyframes toggleShow {
  0% {
    transform: translate(-400px);
  }
  10% {
    transform: translate(0,0);
  }
  80% {
    transform: translate(0,0);
  }
  100% {
    transform: translate(-400px);
  }
}
复制代码

打开浏览器的开发者工具,能够在 performance 里面看到,页面性能有了惊人的提高:

为了进一步提高页面的性能,咱们给过渡的元素增长一个 will-change 属性,该元素就会被提高到 合成层 用GPU单独渲染,这样页面性能就会有更大的提高。

优化懒加载(需考虑兼容性)

有一些页面使用了懒加载,懒加载是经过绑定 scroll 事件一个回调事件,每一次调用一次回调事件,就会测量一次元素的位置,调用 getBoundingClientRect() 方法,从而计算出是否元素出如今了可视区。

// 懒加载库中的代码,判断是否进入了可视区
const isInView = (el, threshold) => {
  const {top, height} = el.getBoundingClientRect()
  return top < clientHeight + threshold && top + height > -threshold
}

复制代码

scroll 形成页面性能降低

scroll 事件会被重复的触发,每触发一次就要测量一次元素的尺寸和位置。尽管对 scroll 的事件进行了节流的处理,但在低端安卓机上仍然会出现滑动不流畅的现象。

优化的思路是经过新增的api—— IntersectionObserver 来获取元素是否进入了可视区。

使用intersection observer

intersection observer api 能够去测量某一个dom节点和其余节点,甚至是viewport的距离。

这个是实验性的api,你应该查阅https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Browser_compatibility查看其兼容性

在过去,检测一个元素是否在可视区内,或者两个元素之间的距离如何,是一个很是艰巨的任务。 但获取这些信息是很是必要的:

  1. 用于懒加载
  2. 用于无限加载,就是微博那种刷到底接着请求新数据能够接着刷
  3. 检测广告的可见性

在过去,咱们须要不断的调用 Element.getBoundingClientRect() 方法去获取到咱们想拿到的信息,然而这些代码会形成性能问题。

intersection observer api 能够注册回调函数,当咱们的目标元素,进入指定区域(好比说viewport,或者其余的元素)时,回调函数会被触发;

intersectionObserver 的语法

var handleFun = function() {}
  var boxElement = document.getElementById()
  
  var options = {
    root: null,
    rootMargin: "0px",
    threshold: 0.01
  };

  observer = new IntersectionObserver(handleFunc, options);
  observer.observe(boxElement);

复制代码

基于IntersectionObserver的懒加载的库

因而本身尝试封装了一个基于IntersectionObserver的懒加载的库。

html

<img class="J_lazy-load" data-imgsrc="burger.png">
复制代码

你也许注意到上面的代码中,图片文件没有 src 属性么。这是由于它使用了称为 data-imgsrc 的 data 属性来指向图片源。咱们将使用这来加载图片

js

function lazyLoad(domArr) {
	if ('IntersectionObserver' in window) {
		
		let createObserver = (dom) => {
			var fn = (arr) => {
				let target = arr[0].target
				if (arr[0].isIntersecting) {
					let imgsrc = target.dataset.imgsrc
					if (imgsrc) {
						target.setAttribute('src', imgsrc)
					}
					
					// 解除绑定观察
					observer.unobserve(dom)
				}
			}
			
			var config = {
				root: null,
				rootMargin: '10px',
				threshold: 0.01
			}
			
			var observer =  new IntersectionObserver(fn, config)
			observer.observe(dom)
		}
		
		Array.prototype.slice(domArr)
		domArr.forEach(dom => {
			createObserver(dom)
		})
	}
}

复制代码

这个库的使用也很是简单:

// 先引入
import {lazyLoad} from '../util/lazyload.js'

// 进行懒加载
let domArr = document.querySelectorAll('.J_lazy-load')
lazyLoad(domArr)

复制代码

而后测试一下,发现能够正常使用:

比较性能

传统的懒加载 lazy-loder 的页面性能以下:

在12秒内,存在红颜色的掉帧现象,一些地方的帧率偏低(在devtool里面是fps的绿色小山较高的地方),用于 scripting 阶段的总共有600多ms.

使用intersetctionObserver以后的懒加载性能以下:

在12秒内,帧率比较平稳,用于 scripting 阶段的时间只有60多ms了。

参考连接:

  1. hacks.mozilla.org/2017/08/ins…
  2. codeburst.io/how-browser…
  3. developer.mozilla.org/en-US/docs/…
  4. www.chromium.org/developers/…
  5. www.w3.org/TR/CSS22/vi…
  6. css性能优化
  7. render tree
相关文章
相关标签/搜索