在我们公司的业务场景中,有很大一部分用户是使用老款安卓机浏览页面,这些老款安卓机性能较差,如果优化不足,页面的卡顿现象会更加明显,此时页面性能优化的重要性就凸显出现。优化页面的性能,需要对浏览器的渲染过程有深入的了解,针对浏览器的每一步环节进行优化。
页面高性能的判断标准是 60fps
。这是因为目前大多数设备的屏幕刷新率为 60 次/秒,也就是 60fps
, 如果刷新率降低,也就是说出现了掉帧, 对于用户来说,就是出现了卡顿的现象。
这就要求,页面每一帧的渲染时间仅为16毫秒 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有其他工作要做,因此这一帧所有工作需要在 10毫秒内完成。如果工作没有完成,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。
浏览器渲染流程
浏览器一开始会从网络层获取请求文档的内容,请求过来的数据是 Bytes,然后浏览器将其编译成HTML的代码。
但是我们写出来的HTML代码浏览器是看不懂的,所以需要进行解析。
渲染引擎解析 HTML 文档,将各个dom标签逐个转化成“DOM tree”上的 DOM 节点。同时也会解析内部和外部的css, 解析为CSSOM tree
, css tree
和dom tree
结合在一起生成了render tree
。
render tree
构建好之后,渲染引擎随后会经历一个layout
的阶段: 计算出每一个节点应该出现在屏幕上的确切坐标。
之后的阶段被称为paiting
阶段,渲染引擎会遍历render tree
, 然后由用户界面后端层将每一个节点绘制出来。
最后一个阶段是 composite
阶段,这个阶段是合并图层。
浏览器内核
浏览器是一个极其复杂庞大的软件。常见的浏览器有chrome, firefox。firefox是完全开源,Chrome不开源,但Chromium项目是部分开源。
Chromium和Chrome之间的关系类似于尝鲜版和正式版的关系,Chromium会有很多新的不稳定的特性,待成熟稳定后会应用到Chrome。
浏览器功能有很多,包括网络、资源管理、网页浏览、多页面管理、插件和扩展、书签管理、历史记录管理、设置管理、下载管理、账户和同步、安全机制、隐私管理、外观主题、开发者工具等。
因此浏览器内部被划分为不同的模块。其中和页面渲染相关的,是下图中虚线框的部分渲染引擎。
渲染引擎的作用是将页面转变成可视化的图像结果。
目前,主流的渲染引擎包括Trident、Gecko和WebKit,它们分别是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
**,解析是一个不断往返的过程。
如下图所示,parser
向lexer
要一个新的token
,lexer
会返回一个token
, parser
拿到token
之后,会尝试将这个token
与某条语法规则进行匹配。
如果该token
匹配上了语法规则,parser
会将一个对应的节点添加到 parse tree (解析树,如果是html就是dom tree,如果是css就是 cssom tree)中,然后继续问parser要下一个node。
当然,也有可能该tokens
没有匹配上语法规则,parser
会将tokens
暂时保存,然后继续问lexer
要tokens
, 直至找到可与所有内部存储的标记匹配的规则。如果找不到任何匹配规则,parser
就会引发一个异常。这意味着文档无效,包含语法错误。
syntax analysis
的输出结果是parse tree, parse tree 的结构表示了句法结构。比如说我们输入"John hit the ball"作为一句话,那么 syntax analysis
的结果就是:
一旦我们拿到了parse tree
, 还有最后一步工作没有做,那就是:translation
,还有一些博客将这个过程成为 compilation / transpilation / interpretation
Lexicons 和 Syntaxes
上面提到了lexer
和 parser
这两个用于解析工具,我们通常不会自己写,而是用现有的工具去生成。我们需要提供一个语言的 lexicon
和 syntaxes
,才可以生成相应的 lexer
和 parser
。
webkit 使用的 lexer 和 parser 是 Flex 和 Bison 。
flex
和css
的flex
布局没有关系,是fast-lexer
的简写,用来生成lexer
。 它需要一个lexicon
,这个lexicon
是用一堆正则表达式来定义的 。- 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 范式(说的就是我 --),它是一种形式化符号来描述给定语言的语法。
它的内容大致为:
- 在双引号中的字("word")代表着这些字符本身。
- 而double_quote用来代表双引号。
- 在双引号外的字(有可能有下划线)代表着语法部分。
- 尖括号( < > )内包含的为必选项。
- 方括号( [ ] )内包含的为可选项。
- 大括号( { } )内包含的为可重复0至无数次的项。
- 竖线( | )表示在其左右两边任选一项,相当于"OR"的意思。
- ::= 是“被定义为”的意思。
下面是用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 标签的容错性很高,需要上下文敏感的语法。
比如说对于下面两段代码:
<html lang="en-US">
<head>
<title>Valid HTMLtitle>
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会在头部有, 这行代码会触发浏览器的标准模式。
所以你看,html 的容错性是非常高的,这样是有代价的,这增加了解析的困难,让词法解析解析更加困难。
DOM Tree
HTML 解析出来的产物,经过加工,就得到了DOM Tree。
对于下面这种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
: :
; ;
{ \{
} \}
( \(
) \)
[ \[
] \]
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 Tree。 CSSOM 被称为“css 对象模型”。
CSSOM Tree 对外定义接口,可以通过js去获取和修改其中的内容。开发者可以通过document.styleSheets
的接口获取到当前页面中所有的css样式表。
CSSOM
那么CSSOM 到底长什么样子呢,我们下面来看一下:
"en">
"stylesheet" href="./test3.css">
"test1">TEST CSSOM1
"test2">TEST CSSOM2
"test3">TEST CSSOM3
复制代码
上面的代码在浏览器中打开,然后在控制台里面输入document.styleSheets
,就可以打印出来CSSOM,如下图所示:
可以看到,CSSOM是一个对象,其中有三个属性,均是 CSSStylelSheet 对象。CSSStylelSheet 对象用于表示每个样式表。由于我们在document里面引入了一个外部样式表,和两个内联样式表,所以CSSOM对象中包含了3个CSSStylelSheet对象。
CSSStyleSheet
CSSStylelSheet对象又长什么样子呢?如下图所示:
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控制该属性,从而控制页面是否应用该样式表
样式表的解析
浏览器的渲染引擎是从上往下进行解析的。
当渲染引擎遇到 节点的时候,会立马暂停解析
html
, 转而解析CSS规则
,一旦CSS规则
解析完成,渲染引擎会继续解析html
当渲染引擎遇到 节点的时候,浏览器的网络组件会发起对 style 文件的请求,同时渲染引擎不会暂停,而是继续往下解析。等到 style 文件从服务器传输到浏览器的时候,渲染引擎立马暂停解析
html
, 转而解析CSS规则
,一旦CSS规则
解析完成,渲染引擎会继续解析html
。
可以联想一下script的解析。
当渲染引擎遇到 节点的时候,会立马暂停解析
html
。
如果这个 节点是内联,则 JS 引擎会马上执行js代码,同时渲染引擎也暂停了工作。什么时候等 JS 代码执行完了,什么时候渲染引擎重新继续工作。如果JS 代码执行不完,那渲染引擎就继续等着吧。
如果这个 节点是外链的,浏览器的网络组件会发起对 script 文件的请求,渲染引擎也暂停了执行。什么时候等 JS 代码下载完毕,并且执行完了,什么时候渲染引擎重新继续工作。
在2011年的时候,浏览器厂商推出了“推测性”解析的概念。
“推测性”解析就是,当让渲染引擎干等着js代码下载和运行的时候,会起一个第三个进程,继续解析剩下的html。当js代码下载好了,准备开始执行js代码的时候,第三个进程就会马上发起对剩下html所引用的资源——图片,样式表和js代码的请求。
这样就节省了之后加载和解析时间。
被称为“推测性”解析是因为,前面的js代码存在一定的概率修改DOM节点,有可能会让后面的DOM节点消逝,那么我们的工作就白费了。浏览器“推测”这样的发生的概率比较小。
让渲染引擎干等着不工作是非常低效率的,所以雅虎军规会让把 script 标签放在body的底部。
言归正传,样式表放在head的前边,有两个原因:
- 尽快加载样式表
- 不要耽误js代码选择dom节点
Render Tree
当浏览器忙着构建DOM Tree和 CSSOM Tree的时候, 浏览器同时将两者结合生成Render Tree。也就是说,浏览器构建DOM Tree和 CSSOM 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 Tree 和 Render Object Tree 之间的关系是什么样的?
Render Object Tree 并不严格等于Dom Tree,先看一张DOM Tree 和 Render Object Tree的直观的对比图:
上面左侧DOM tree的节点对应右侧Render Object Tree上的节点。细心的你会注意到,上图左侧的DOM Tree中的HTMLDivElement 会变成RenderBlock, HTMLSpanElement 会变成RenderInline,也就是说,DOM节点对应的 render object 节点并不一样。
DOM节点对应的 render object 节点并不一样分这几种情况:
-
display : none
的DOM 节点没有对应的 Render Object Tree 的节点这里的
display:none
属性,有可能是我们在CSS里面设置的,也有可能是浏览器默认的添加的属性。比如说下面的元素就会有默认的display:none
的属性。
- 一个DOM节点,可能有多个 Render Object Tree的节点
下面的各个DOM元素,会对应多个Render Object Tree的节点
-
-
-
-
-
-
-
比如说,
就会有两个renderer:
- 脱离了文档流的DOM节点,DOM Tree 和 Render Object Tree 是对应不上的。
脱离文档流的情况,要么是float
, 要么是position: absolute / fixed
。
比如说对于下面的结构:
<body>
<div>
<p>Lorem ipsump>
div>
body>
复制代码
它的 DOM tree
和 Render Tree
如下图所示:
如果增加脱离文档流的样式,如下:
p {
position: absolute;
}
复制代码
情况就会变成下面这样:
节点对应的 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 的节点类型分为下面几种:
-
RenderBlock
display: block
的DOM节点对应的render object节点类型为RenderBlock -
RenderInline
display:inline
的DOM节点对应的render object节点类型为RenderInline -
RenderReplaced
可能我们之前听说过“替换元素” 的概念,比如说常见的“替换元素”有下面:
为啥被称为“替换元素”,是因为他们的内容会被一个独立于HTML/CSS上下文的外部资源所替代。
“替代元素” 的DOM节点对应的render object 节点类型为RenderReplaced
-
RenderTable
元素的DOM节点对应的render object 节点类型为 RenderTable
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 节点)就是用于处理不遵守这两种规则的代码的,如果出现不符合这两个准则的情况,比如说下面:
- 若在块状元素里面同时出现了块状元素和行内元素:
<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> 复制代码
- 还有另外一种非常糟糕的情况,就是在行内元素中出现了块状元素:
<i>Italic only <b>italic and bold <div> Wow, a block! div> <div> Wow, another block! div> More italic and bold textb> More italic texti> 复制代码
上面的代码中,render object tree需要做更多的事情去修复这种糟糕的DOM tree: 三个anonymounse renderers会被创建,上面的代码会被分割成三段,被三个匿名的block 包裹。
<anonymous pre block> <i>Italic only <b>italic and boldb>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 textb> More italic texti> anonymous post block> 复制代码
注意到,
元素和
元素都被分割进了
和
两个类型为 RenderBlock 的节点中,他们通过一种叫*continuation chain(延续链)*的机制来链接。负责上面递归拆分行内元素的生产*continuation chain(延续链)*的方法被称为
splitFlow
。因此,一旦你写出了不符合规范的html结构, 在构建render object tree时就需要更多的工作去纠正,从而造成页面性能的下降。
构建 Render Object Tree
Gecko 和 WebKit 采用了不同的方案来构建 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的信息,才可以被绘制在屏幕上,这就需要样式计算这一过程。
而样式计算需要两部分“原材料”:
- DOM Tree
- 一堆样式规则
DOM Tree在HTML解析之后就可以拿到了,一堆样式规则可以来自下面:
- 浏览器默认的样式
- 外链样式
- 内联样式
- DOM节点上的style属性
那么样式规则是如何构成的呢?
- 样式表是一堆 规则(rules)的集合;
- 当然也不光都是 规则(rules), 还会有一些奇怪的东西:@import, @media, @namespace 等等
- 一个**规则(rules)是由选择器(selector)和声明块(declaration block)**构成的
- **声明块(declaration block)由一堆声明(declaration)**加中括号构成
- **声明(declaration ** 由 property 和 value 构成。
样式计算存在以下三个难点:
- style 样式数据太多,会占用大量内存
- 匹配元素会影响性能
- css规则的应用顺序
下面我们介绍这个三个难点是如何解决的。
样式规则的应用顺序
某一个DOM节点上可能有多个规则,比如下下面:
div p { color: goldenrod; } p { color: red; } 复制代码
那么这个DOM节点究竟用的是哪个规则?
规则的权重是:先看 order , 然后再算specificity, 最后再看哪个规则靠的更近。
order
order的权重从高到底:
- 用户的
! important 声明
(浏览器可以让用户导入自定的样式) - 程序员写的 ! important 声明
- 程序员写的普通css样式
- 浏览器的默认css样
Specificity
Specificity是一个相加起来的值
#foo .bar > [name="baz"]::first-line {} /* Specificity: 0 1 2 1 */ 复制代码
-
第一位的数值(a)
是否有DOM节点上style属性的值,有则是1,否则是0
-
第二位的数值 (b)
id选择器的数量之和
-
第三位的数值 (c)
class选择器,属性选择器,伪类选择器个数之和
-
第四位的数值 (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 + p
,div ~ 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。如下表所示:
上图中的,computed style 里就不用存储CSS全部的200多个属性,而是保存着对这些 style struct对象的引用。
这样一来,一些具有相同属性的DOM 节点就可以引用相同的
style struct
, 不仅如此,因为子节点有一些属性会继承父节点,那么保存这些属性的style struct
就会被父节点和子节点所共享。匹配元素会影响性能
对于每一个DOM节点,css引擎需要去遍历所有的css规则看是否匹配。对于大部分的DOM节点,css规则的匹配并不会发生改动。
比如说,用户把鼠标hover到一个父元素上面,这个父元素的css规则匹配是发生了变化,我们不仅仅要重新计算这个父元素的样式,还需要重新计算这个父元素的子元素的样式(因为要处理继承的样式),但是能匹配这些子元素的样式规则,是不会变的。
如果我们能记下来,有哪些selector可以匹配到就好了。
为了优化这一点,CSS 引擎在进行 selector 匹配时,会根据权重的顺序把他们排成一串,然后把这一串加到右边的 CSS rule tree 上面。
CSS引擎希望右边CSS rule tree 的分支数越少越好,因此会将新加入的一串尽量的合并到已有的分支,所以上面的过程会是下面这样的:
然后遍历每一个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 都对此进行了优化,在遍历完一个节点之后,会把计算好的样式放到缓存中,在遍历下一个节点之前,会做一个判断,看是否可以复用缓存中的样式。
这个判断包括一下几点:
-
两个节点是否有相同的id, class
-
两个节点是否有相同的style 属性
-
两个节点对应的父亲节点是否共享一份计算好的样式,那该两个节点继承的样式也是相同的。
解析阶段如何优化
更加符合规范的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 textb> <div> More italic and bold text <p>More italic and bold textp> div> div> More italic and bold text More italic and bold text b> More italic text i> 复制代码
在控制台里面,设置cpu 为6x slowdown,然后记录渲染数据如下:
其中花费了 12888ms 进行了rendering 过程。
如果我们对html代码仅仅做几处修改,在不考虑css优化、样式优化的前提下:
<div v-for="n in nums"> <p>Italic onlyp> <div>italic and bold <div> Wow, a block! div> <div> Wow, another block! <b>More italic and bold textb> <div> More italic and bold text <p>More italic and bold textp> div> div> More italic and bold text More italic and bold text div> div> 复制代码
在控制台里面,设置cpu 为6x slowdown,然后记录渲染数据如下:
可以发现,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 tree, render 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 :-
全局layout
render tree 的根节点自身的layout方法被调用,然后整个render tree 被更新。
-
局部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 有三种情况:
-
selfNeedsLayout
Rederer 自身是 “脏”的。当一个 rederer 自身被设置为“脏”的,它相关的父亲节点也会被设置一个标识来指出它们有一个“脏”的子节点
-
posChildNeedsLayout
设置了postion不为static的子节点被弄脏了
-
normalChildNeedsLayout
在文档流中的子节点被弄脏了
上面之所以要区分子节点是否在文档流中,是为了layout过程的优化。
Containing Block (包含块)
上面提到了相关父节点(the relevant ancestor chain),那么究竟是如何判断哪个节点是 **相关父节点 **?
答案就是通过 containing block.
Container Block(包含块) 身份有两个
-
子节点的相关的父节点
-
子节点的相对坐标系
子节点都有 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) 的位置。下面是图示:
黑色的框代表的是 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 ,原文如下:
而webkit core 认为css postion 的值为 fixed的renderer节点的containing block是RenderView。RenderView并不会表现的和viewport一样,但是RenderView会根据页面滚动的距离算出css postion 的值为 fixed的renderer节点的位置。这是因为单独为viewport 生成一个renderer 节点并不简单。原文如下:
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 的属性
-
盒子模型相关的属性
-
width
-
height
-
padding
-
margin
-
border
-
display
-
……
-
-
定位属性和浮动
- top
- bottom
- left
- right
- position
- float
- clear
-
节点内部的文字结构
- text - aligh
- overflow
- font-weight
- font- family
- font-size
- line-height
上面只是一部分,更全部的可以点击 csstriggers 来查阅;
csstrigger 里面需要注意的有几点。
-
opacity的改动,在blink内核和Gecko内核上不会触发layout 和 repaint
-
transform的改动,在blink内核和Gecko内核上不会触发layout 和 repaint
-
visibility 的改动,在Gecko 内核上不会触发 layout repaint, 和 composite
会触发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 类:
- 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 应用动画
- OverflowClipPaintLayer
- overflow 不为 visible
- NoPaintLayer
- 不需要 paint 的 PaintLayer,比如一个没有视觉属性(背景、颜色、阴影等)的空 div。
4. 从 PaintLayers 到 GraphicsLayers
某些特殊的paintLayer会被当成合成层,合成层拥有单独的 GraphicsLayer,而其他不是合成层的paintLayer,则和其第一个拥有GraphicsLayer 父层公用一个。
每个 GraphicsLayer 都有一个GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 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%
这个其实有两个地方,第一是,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阶段,
如果继续优化,我们通过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.
所以,对于这种动效,优先选择 CSS animation > transform 修改 scale > 绝对定位 修改width > 文档流中修改width
跑马灯的优化
跑马灯的动效是:每隔3秒进行向左侧滑动淡出,然后再滑动重新淡入,更新文本为“**砍价9元”
之前的滑动和淡出的效果是通过vue提供的
来实现的
原理当我们想要用到过渡效果,会在vue中写这样的代码:
"toggle"> "test"> 复制代码但是其实渲染到浏览器中的代码,会依次是下面这样的:
// 过渡进入开始的一瞬间
"test toggle-enter"> // 过渡进入的中间阶段"test toggle-enter-active"> // 过渡进入的结束阶段"test toggle-enter-active toggle-enter-to"> // 过渡淡出开始的一瞬间"test toggle-leave"> // 过渡淡出的中间阶段"test toggle-leave-active"> // 过渡淡出的结束阶段"test toggle-leave-active toggle-leave-to"> 复制代码也就是说,过渡效果的实现,是通过不停的修改、增加、删除该dom节点的class来实现。
影响页面性能一方面,
v-if
会修改dom节点的结构,修改dom节点会造成浏览器重走一遍layout
阶段,也就是重排。另一方面,dom节点的class被不停的修改,也会导致浏览器的重排现象,因此页面性能会比较大的受到影响。若页面中
控制的节点过多时,页面的性能就会比较受影响。为了证明,下面代码模拟了一种极端的情况:
"n in testArr">复制代码"toggle"> "info-block" v-if="isShow">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
动画来控制过渡:"n in testArr">复制代码"info-block">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查看其兼容性
在过去,检测一个元素是否在可视区内,或者两个元素之间的距离如何,是一个非常艰巨的任务。 但获取这些信息是非常必要的:
- 用于懒加载
- 用于无限加载,就是微博那种刷到底接着请求新数据可以接着刷
- 检测广告的可见性
在过去,我们需要不断的调用
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
"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了。参考链接:
- hacks.mozilla.org/2017/08/ins…
- codeburst.io/how-browser…
- developer.mozilla.org/en-US/docs/…
- www.chromium.org/developers/…
- www.w3.org/TR/CSS22/vi…
- css性能优化
- render tree
你可能感兴趣的:(移动开发,前端,javascript,ViewUI)