综述css
以前使用ExtJS时遇到一个问题:为何依次设置多个组件的可见性界面会卡顿?在了解HTML的dom操做相关内容的时候也好奇这个东西究竟是怎么回事,而后尤为搞不懂CSS和Html分管样式和网页结构,这个东西是怎么实现的,是否是很复杂?html
带着这些问题,看了一些文章,尤为是据说了Redraw和Reflow的概念以后,开始研究了dom的性能调优,最近看了一篇《how browser work》,以为写得很详细,结合以前看的文章,解决了很多的困惑,写一篇对这个文章的读后总结,顺便记下来本身掌握的一些浏览器性能的知识。web
浏览器的整体结构正则表达式
浏览器主要部件包括:express
具体结构以下图所示:编程
Figure : Browser components浏览器
呈现引擎(rendering engine)缓存
呈现引擎的工做就是呈现,包括呈现HTML/XML/pdf/image/CSS等等,固然咱们主要关心呈现HTML+CSS这两个部分。网络
呈现引擎这个名字咱们可能不熟,可是WebKit、Blink你们应该听过,Safari的呈现引擎就是Webkit,Chrome目前的呈现引擎是Blink,是Webkit的一个分支,另外Firefox也有本身的呈现引擎Gecko,IE的是Trident(本文写做的时候应该没有Edge)。本文介绍呈现引擎主要围绕Webkit和Trident来说,会涉及到二者的异同,也就是Chrome、Safari和Firefox。dom
呈现引擎基本工做流程
如上图所示,呈现引擎从网络中收到资源文件后,首先Parse HTML文件,生成Dom树,而后开始parse外部和内部的CSS样式,生成CSS规则组,而后以Dom树为基础生成Render tree,render tree虽然没有渲染在页面上,但包含了足够的信息render出一个像素页面。所以调用render tree的layout方法开始根据dom tree结构,上面每一个元素的display/width/height/minwidth/minheight/maxwidth/maxheight/border/padding/margin/position/float/left/right/top/bottom等layout相关的属性计算出对应元素的真实位置信息,在此基础上调用render tree的paint方法依据位置排布按特定顺序逐个绘制组件。
Webkit和firefox Gecko呈现引擎的工做流图
Figure : WebKit main flow
Figure : Mozilla's Gecko rendering engine main flow
能够看出整体流程大同小异,更多的是名词叫法的差别。
本文随后将按照上面的整体流程,分为parse(包括dom tree和style rules生成)、render tree(frame tree)、layout(reflow)&paint(draw)三个大章节介绍呈现引擎。
Parse: Dom tree and style rules
Parse术语浅析
对于一个操做,包括编程语言和方程、公式,做者首先以文本形式写成,但要计算机理解并执行,就须要按照必定语法写成,这样计算机才能根据必定的原则,把文本转化成结构化的操做树,而后再根据这些操做来执行命令,从文本转化为操做树的过程即Parse。
例如我输入了2+3-1这段文本,将会返回以下parse tree:
具体来讲,2+3-1能parse成结构化的操做,分红了两步,第一步是词法分析,第二步是语法分析。
词法分析
词法分析就是根据这门语言、方程的特色,将文本中的一个个的字符,逐个提取成这个语言的合法词语的过程,在这个示例中,就是把2+3-1提取出2,+,3,-,1五个词的过程。若是是20+30-11,就得能提取出20,+, 30,-,11;若是是20+-1就得提取出20,+,-,1。
咱们看到了最后一个式子的错误,这个不归词法分析管,后面语法分析负责找出这类问题。
词法分析具体的实现通常是经过正则表达式,正则规定出语言全部的操做、变量、各类类型的值的正则,词法分析器逐个去匹配提取出词语。
词法分析正则表达式:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
语法分析
在词法分析基础上,语法分析就能够进行了,语法分析比词法分析复杂一些,首先须要指定咱们的语言的语法规则:
依据这些规则设计出语法分析器,语法分析器判断词法分析器输入的词拼起来是否知足语法规则,知足后构建出parse tree。
语法分析规则:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
context free grammer
通常的语法分析均可以经过BNF的格式来实现,上述的语法规则示例就是一种BNF。
可以只经过单纯的BNF就彻底描述清楚并实现的语法被称做"context free grammer",也就是不依赖上下文的语法,只要当前这段语句分析了就能有肯定的意思,一个词汇不会有两个意思。
我理解这个“肯定的意思”并不包含运行时层面的一些东西,主要是指语法上一个词汇会不会有歧义,没有歧义的就是context free grammer。有歧义的就不是,parse的过程就会更复杂,例如HTML就不是,后续会讲到,因此在这里提这个概念。
HTML Parser
not context free
具体到 HTML Parser,首先就是Html的语法不是context free的,为何呢?
首先XML是Context free的,每个标签都须要闭合,标签之间也有明确的包含关系,使每个标签都有肯定的含义,因此Html不是context free的缘由不在于它的基础语法,而是由于它的包容性。
好比容许br标签不闭合,甚至容许用</br>和<br>两种写法,好比标签之间没有造成嵌套关系<div><p></div></p>也不会报错,会推断修复这类问题。
DOM
Dom元素咱们都熟悉,在浏览器调试窗口element一栏就能够看到咱们的html+JavaScript生成的Dom结构(这里只说Html)。
所谓HTML的parsing过程就是把HTML的语法写出的文本转化成Dom tree的过程,所以用html标记语言以及JavaScript操做Dom元素的过程也被称做Dom编程。
例以下面的代码会被parse成下面的Dom tree。
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
Dom的官方规范见这个连接: https://www.w3.org/DOM/DOMTR
parse流程
执行parse的流程也是词法分析和语法分析,tokeniser即词法分析,tree construction为语法分析部分
tokeniser
如上图所示,主要过程就是定位出每个尖括号包裹的标签,包括打开标签和关闭标签分别捕获
tree construction
经过找到的标签,给每一个打开标签添加到树中,直到闭合标签以前的其余标签成为本身的children,若是有特殊状况特殊处理。
CSS parsing
同HTML不一样,CSS是context free grammer,在此简单列出一组CSS的词法分析和语法分析的规则,只作简要介绍
词法分析:
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
词法分析也是基于正则表达式,定位CSS文本文档中一个个的关键词,其中name是id,ident是classname,从中能够看出css的classname容许输入哪些字符,不然会报错。
语法分析:
这个比较抽象,看一组例子,一个CSS样式文本和命中的规则:
Webkit CSS Parser
如上图所示,一个webkit parse出的style rules也是一个树形结构,第二层是一个一个的CSS rule,每一个rule都有分支,用来存放全部的selector和存放属性的声明。
render tree(frame tree)
从dom tree到render tree
通过了parse,咱们知道已经获得了dom tree和style rules,接下来的过程就是从将二者合并成一个render tree,而且计算出真正render的必要信息。
要理解这句话,就要说清楚到dom tree这一步进展到了什么程度,到render tree这一步又进展到了什么程度。由于从整体上来讲,构建出Dom->rendertree->layout->draw是很抽象的说法,具体到好比width有个width:30%,在哪一步计算出了绝对宽度值,到哪一步真正给dom对象设置了这个宽度,到哪一步真正把这个对象按照这个大小布置出来了,布置出来以后何时绘制出来的。
下面首先简单说一下我对dom tree和render tree分界线的理解,也就是dom tree和style rules这两个东西都包括什么,进展到了哪一步:
dom tree实现了一个树形的dom object,一层一层的都从HTML文档转化为HTMLObject,而且树形都转化成了HTMLObject的属性。style rules(css rules)把CSS文档转换成了一组规则对象,每一个对象都包含了对应的css selector和css属性和值,从文档变成了对象。而后就没有了,这个对象尚未真正开始计算样式的实际值,没有真正开始计算最终绘制颜色像素相关的东西。
这个计算实际值、计算最终绘制须要的一些内容的过程就是合并这二者构建render tree的过程。抽象来讲,render tree包含了最终layout和paint(或者叫reflow和redraw)须要的必要信息,并且都各类样式都计算出了最终的值,所谓layout和paint的过程也是调用了render tree上元素的layout和paint两个方法,一个典型的render tree对象类以下所示:
render tree和dom tree的关系
render tree和dom tree不是一对一的关系,例如display:none的element不会体如今rendertree中;可是属性hidden会绘制render tree object;select dom元素会绘制3个render object。
dom tree 到render tree
计算CSS合并Dom和css rules
要将CSS和Dom合并面临着三个大问题:
共享样式信息
浏览器进行了一些设计来解决这些问题,首先是共享样式信息。
若是兄弟元素知足了一系列的条件,那么他们就共享样式对象,不用重复计算。
Firefox rule tree
另外,为了解决上述问题的1和3,firefox设计出了一种rule tree+style context tree的结构。
这一点和webkit有所不一样,webkit含有相似的东西,但没有生成这样完整的tree。
rule tree+style context的实现有点复杂,主要目的是经过这样的一颗树结构化保存计算过得样式信息,用于复用,提升性能。我也没有特别搞懂,所以不详细介绍理论,直接上个例子:
有以下html:
以下Css样式表:
依据html生成的dom tree,取到一个dom节点后,便利样式表招到匹配的样式,根据匹配程度由低到高,由上到下列出这个dom全部匹配的样式,生成rule tree:
例如针对第一个div元素,从上到下生成了B、C、D三个节点依次表明了1/2/5三条规则,优先级从低到高;这里顺便介绍一下为何要生成这个tree,为何知道优先级从低到高了,还要保留低优先级的:
给一个元素在rule tree上生成了一个从上到下(优先级从低到高)的path以后,就能够给元素生成对应的style context了。具体生成的style context以下图所示:
仍是拿第一个div举例子,在生成其 style context的时候,就把匹配第一个div元素的path最下面的样式做为该元素的指定样式,这就是style context。
而后根据style context和rule tree构建style structs。style structs基于样式属性的维度,也就是每个属性构建一个struct来组成structs。在本例中针对第一个div构建color属性,规则D中有color,搜索结束使用这个color的值;针对它的margin属性则不一样,规则D中没有关于margin的配置,向上到C中也没有,知道B中找到了执行B中的margin值。
还有额外的状况就是找到path的顶层了也没有,也就是没有显式声明的样式针对这个属性,那么久根据该属性的具体状况,若是是继承式的属性,就再去看父元素的path,若是不是继承性的属性,就直接取该属性的默认值。
样式表计算的优先级
来源的优先级
从低到高排列以下
其中Author是指网页的做者,User是指在页面上修改属性的人,这个对咱们平常调试有帮助,说明已经配置的属性,若是不加important没法覆盖网页加载(做者)样式。
Specify
优先级从高到低:
以上先计算高优先级的属性出现的次数,若是同样,再计算低优先级的属性出现的次数
layout(reflow)&paint(draw)
从render tree到layout&paint
在render tree构建完成以后,一个新的tree造成了,其中的每一个元素都包含了全部最终的样式属性的引用,值都计算出了最终值,不是相对值;可是并无真正的拿这些属性去绘制图形和放置图形的位置,只是具备了全部绘制图形所需的完善的信息,不须要再对这些数值信息进行加工了。
此时调用layout方法便可以计算出每一个元素的布局位置和尺寸等信息,包括z-index的信息,调用paint方法就能够计算出元素的最终像素绘制信息。
layout
正如上文所说,layout以前并无真正计算出元素的坐标和尺寸、z-index等信息,layout将经过display、width、height、postion、float、left、right、min-height、max-height等尺寸相关的属性,计算出元素所在的x轴Y轴Z轴的信息和最终的尺寸信息。
dirty bit system
计算完尺寸信息以后,dom结构会不断变化,呈现引擎有一个标记脏值的方法,经过标记新增或者改变属性的元素为dirty的方法,在下一次layout的时候没必要所有layout,而是只layout标记为dirty的元素、元素的子元素和元素的迭代向上父元素(具体状况视改变的属性不一样而不一样)。具体来讲,若是改变了全局的字号或者直接改变了viewport的大小,会触发全局的layout,不然改变了元素的尺寸、字号等,则会触发局部的layout;还有一些属性改变不会触发layout,会在paint中说明。
Layout过程
Painting
在layout以后,能够对rendertree的成员进行painting。
Painting和layout同样,有局部和全局两种状况。全局不用多说,说一下局部Painting的状况。
根据render tree的变化,将绘制的须要从新绘制的renderer置为disable,操做系统会认为这个绘制区域已通过期(dirty),操做系统会把多个这样的区域结合起来,一块儿触发一次paint事件,而后调用paint 线程执行重绘。
重绘顺序
重绘针对painting阶段的属性(非layout相关属性)会按照固定的顺序操做,所以会按照逆序将对应的属性压入Stacking context栈,从后向前弹出执行,堆栈内容从后向前以下:
最小改变
在dom元素或属性变动后,,浏览器会优化尝试重绘(paint)或重排(layout)最少的内容。若是改变了一个元素的颜色或背景色,将只会repaint这个元素;若是改变了元素的position将不得不致使重排+重绘该元素及其子元素,有时候还须要重排其兄弟元素;添加dom元素也会致使本身及其父元素的重绘和重排。若是改变Html字体将会清空render相关缓存并对整个页面重排和重绘。
呈现引擎(rendering engine)线程
呈现引擎通常是一个单独的线程,他们一般都是该页面的主线程,而网络交互部分则会根据请求树简历多个网络线程(2-6个)。主线程是一个无限循环,监听须要重绘和重排的事件做出对应的render。