浏览器工做原理简介

1、浏览器主要构成css

  1. 用户界面 - 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口以外的其余部分。html

  2. 浏览器引擎 - 用来查询及操做渲染引擎的接口。html5

  3. 渲染引擎 - 用来显示请求的内容,例如,若是请求内容为html,它负责解析html及css,并将解析后的结果显示出来。node

  4. 网络 - 用来完成网络调用,例如http请求,它具备平台无关的接口,能够在不一样平台上工做。web

  5. UI后端 - 用来绘制相似组合选择框及对话框等基本组件,具备不特定于某个平台的通用接口,底层使用操做系统的用户接口。算法

  6. JS解释器 - 用来解释执行JS代码。后端

  7. 数据存储 - 属于持久层,浏览器须要在硬盘中保存相似cookie的各类数据,HTML5定义了web database技术,这是一种轻量级完整的客户端存储技术浏览器

 

2、渲染引擎缓存

  本文所讨论的浏览器——Firefox、Chrome和Safari是基于两种渲染引擎构建的,Firefox使用Geoko——Mozilla自主研发的渲染引擎,Safari和Chrome都使用webkit。cookie

  Webkit是一款开源渲染引擎,它原本是为Linux平台研发的,后来由Apple移植到Mac及Windows上,相关内容请参考http://webkit.org

  渲染引擎首先经过网络得到所请求文档的内容,一般以8K分块的方式完成。

  下面是渲染引擎在取得内容以后的基本流程:

  解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树

  渲染引擎开始解析html,并将标签转化为内容树中的dom节点。接着,它解析外部CSS文件及style标签中的样式信息。这些样式信息以及html中的可见性指令将被用来构建另外一棵树——render树。

  Render树由一些包含有颜色和大小等属性的矩形组成,它们将被按照正确的顺序显示到屏幕上。

  Render树构建好了以后,将会执行布局过程,它将肯定每一个节点在屏幕上的确切坐标。再下一步就是绘制,即遍历render树,并使用UI后端层绘制每一个节点。

  值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽量早的将内容呈现到屏幕上,并不会等到全部的html都解析完成以后再去构建和布局render树。它是解析完一部份内容就显示一部份内容,同时,可能还在经过网络下载其他内容。

webkit:

Gecko:

图4:Mozilla的Geoko渲染引擎主流程

  从图3和4中能够看出,尽管webkit和Gecko使用的术语稍有不一样,他们的主要流程基本相同。Gecko称可见的格式化元素组成的树为frame树,每一个元素都是一个frame,webkit则使用render树这个名词来命名由渲染对象组成的树。Webkit中元素的定位称为布局,而Gecko中称为回流。Webkit称利用dom节点及样式信息去构建render树的过程为attachment,Gecko在html和dom树之间附加了一层,这层称为内容接收器,至关制造dom元素的工厂。

3、DOM解析

HTML的DOM Tree解析以下:

<html>  
<head>  
    <title>Web page parsing</title>
</head>  
<body>  
    <div>
        <h1>Web page parsing</h1>
        <p>This is an example Web page.</p>
    </div>
</body>  
</html> 


上面这段HTML会解析成这样:

下面是另外一个有SVG标签的状况。

 

4、CSS解析

Webkit CSS解析器(Webkit CSS parser)

  Webkit使用Flex和Bison解析生成器从CSS语法文件中自动生成解析器。回忆一下解析器的介绍,Bison建立一个自底向上的解析器,Firefox使用自顶向下解析器。它们都是将每一个css文件解析为样式表对象,每一个对象包含css规则,css规则对象包含选择器和声明对象,以及其余一些符合css语法的对象。

  

处理脚本及样式表的顺序(The order of processing scripts and style sheets)

  脚本

  web的模式是同步的,开发者但愿解析到一个script标签时当即解析执行脚本,并阻塞文档的解析直到脚本执行完。若是脚本是外引的,则网络必须先请求到这个资源——这个过程也是同步的,会阻塞文档的解析直到资源被请求到。这个模式保持了不少年,而且在html4及html5中都特别指定了。开发者能够将脚本标识为defer,以使其不阻塞文档解析,并在文档解析结束后执行。Html5增长了标记脚本为异步的选项,以使脚本的解析执行使用另外一个线程。

  预解析(Speculative parsing)

  Webkit和Firefox都作了这个优化,当执行脚本时,另外一个线程解析剩下的文档,并加载后面须要经过网络加载的资源。这种方式可使资源并行加载从而使总体速度更快。须要注意的是,预解析并不改变Dom树,它将这个工做留给主解析过程,本身只解析外部资源的引用,好比外部脚本、样式表及图片,也就是不会使外部脚本操做DOM树。

  样式表(Style sheets)

  样式表采用另外一种不一样的模式。理论上,既然样式表不改变Dom树,也就没有必要停下文档的解析等待它们,然而,存在一个问题,脚本可能在文档的解析过程当中请求样式信息,若是样式尚未加载和解析,脚本将获得错误的值,显然这将会致使不少问题,这看起来是个边缘状况,但确实很常见。Firefox在存在样式表还在加载和解析时阻塞全部的脚本,而Chrome只在当脚本试图访问某些可能被未加载的样式表所影响的特定的样式属性时才阻塞这些脚本。

  css解析大体过程

CSS的解析大概是下面这个样子(下面主要说的是Gecko也就是Firefox的玩法),假设咱们有下面的HTML文档:

<doc>  
<title>A few quotes</title>  
<para>  
  Franklin said that <quote>"A penny saved is a penny earned."</quote>
</para>  
<para>  
  FDR said <quote>"We have nothing to fear but <span>fear itself.</span>"</quote>
</para>  
</doc>  

DOM树:

CSS文档:

/* rule 1 */ doc { display: block; text-indent: 1em; }
/* rule 2 */ title { display: block; font-size: 3em; }
/* rule 3 */ para { display: block; }
/* rule 4 */ [class="emph"] { font-style: italic; }

 

css rule tree:

注意,图中的第4条规则出现了两次,一次是独立的,一次是在规则3的子结点。因此,咱们能够知道,创建CSS Rule Tree是须要比照着DOM Tree来的。CSS匹配DOM Tree主要是从右到左解析CSS的Selector,好多人觉得这个事会比较快,其实并不必定。关键还看咱们的CSS的Selector怎么写了。

注意:CSS匹配HTML元素是一个至关复杂和有性能问题的事情。因此,DOM树要小,CSS尽可能用id和class,千万不要过渡层叠下去

将DOM树和csss rule tree attach在一块儿,获得下面的Style Context Tree

因此,Firefox基本上来讲是经过CSS 解析 生成 CSS Rule Tree,而后,经过比对DOM生成Style Context Tree,而后Firefox经过把Style Context Tree和其Render Tree(Frame Tree)关联上,就完成了。注意:Render Tree会把一些不可见的结点去除掉。而Firefox中所谓的Frame就是一个DOM结点,不要被其名字所迷惑了。

注:Webkit不像Firefox要用两个树来干这个,Webkit也有Style对象,它直接把这个Style对象存在了相应的DOM结点上了。

 

5、渲染树的构建

  当Dom树构建完成时,浏览器开始构建另外一棵树——渲染树。渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这棵树是为了以正确的顺序绘制文档内容。

  Firefox将渲染树中的元素称为frames,WebKit则用renderer或渲染对象来描述这些元素。

  一个渲染对象知道怎么布局及绘制本身及它的children。

  RenderObject是Webkit的渲染对象基类,它的定义以下:

class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node;//the DOM node
RenderStyle* style;// the computed style
RenderLayer* containgLayer; //the containing z-index layer
}

  每一个渲染对象用一个和该节点的css盒模型相对应的矩形区域来表示,正如css2所描述的那样,它包含诸如宽、高和位置之类的几何信息。盒模型的类型受该节点相关的display样式属性的影响(参考样式计算章节)。下面的webkit代码说明了如何根据display属性决定某个节点建立何种类型的渲染对象。

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;
}

  元素的类型也须要考虑,例如,表单控件和表格带有特殊的框架。

  在Webkit中,若是一个元素想建立一个特殊的渲染对象,它须要重写“createRenderer”方法,使渲染对象指向不包含几何信息的样式对象。

  渲染树和Dom树的关系(The render tree relation to the DOM tree)

  渲染对象和Dom元素相对应,但这种对应关系不是一对一的,不可见的Dom元素不会被插入渲染树,例如head元素。另外,display属性为none的元素也不会在渲染树中出现(visibility属性为hidden的元素将出如今渲染树中)。

  还有一些Dom元素对应几个可见对象,它们通常是一些具备复杂结构的元素,没法用一个矩形来描述。例如,select元素有三个渲染对象——一个显示区域、一个下拉列表及一个按钮。一样,当文本由于宽度不够而折行时,新行将做为额外的渲染元素被添加。另外一个多个渲染对象的例子是不规范的html,根据css规范,一个行内元素只能仅包含行内元素或仅包含块状元素,在存在混合内容时,将会建立匿名的块状渲染对象包裹住行内元素。

  一些渲染对象和所对应的Dom节点不在树上相同的位置,例如,浮动和绝对定位的元素在文本流以外,在两棵树上的位置不一样,渲染树上标识出真实的结构,并用一个占位结构标识出它们原来的位置。

 建立树的流程(The flow of constructing the tree)

  Firefox中,表述为一个监听Dom更新的监听器,将frame的建立委派给Frame Constructor,这个构建器计算样式(参看样式计算)并建立一个frame。

  Webkit中,计算样式并生成渲染对象的过程称为attachment,每一个Dom节点有一个attach方法,attachment的过程是同步的,调用新节点的attach方法将节点插入到Dom树中。

  处理html和body标签将构建渲染树的根,这个根渲染对象对应被css规范称为containing block的元素——包含了其余全部块元素的顶级块元素。它的大小就是viewport——浏览器窗口的显示区域,Firefox称它为viewPortFrame,webkit称为RenderView,这个就是文档所指向的渲染对象,树中其余的部分都将做为一个插入的Dom节点被建立。

 

6、布局

 layout通常有下面这几个部分:

  1. parent渲染对象决定它的宽度

  2. parent渲染对象读取chilidren,并:

    a. 放置child渲染对象(设置它的x和y)

    b. 在须要时(它们当前为dirty或是处于全局layout或者其余缘由)调用child渲染对象的layout,这将计算child的高度

    c. parent渲染对象使用child渲染对象的累积高度,以及margin和padding的高度来设置本身的高度-这将被parent渲染对象的parent使用

    d. 将dirty标识设置为false

  Firefox使用一个“state”对象(nsHTMLReflowState)作为参数去布局(firefox称为reflow),state包含parent的宽度及其余内容。

  Firefox布局的输出是一个“metrics”对象(nsHTMLReflowMetrics)。它包括渲染对象计算出的高度。

  宽度计算

  渲染对象的宽度使用容器的宽度、渲染对象样式中的宽度及margin、border进行计算。例如,下面这个div的宽度:

  <div />

  webkit中宽度的计算过程是(RenderBox类的calcWidth方法):

  • 容器的宽度是容器的可用宽度和0中的最大值,这里的可用宽度为:contentWidth=clientWidth()-paddingLeft()-paddingRight(),clientWidth和clientHeight表明一个对象内部的不包括border和滑动条的大小
  • 元素的宽度指样式属性width的值,它能够经过计算容器的百分比获得一个绝对值
  • 加上水平方向上的border和padding

  到这里是最佳宽度的计算过程,如今计算宽度的最大值和最小值,若是最佳宽度大于最大宽度则使用最大宽度,若是小于最小宽度则使用最小宽度。最后缓存这个值,当须要layout但宽度未改变时使用。

  Line breaking

  当一个渲染对象在布局过程当中须要折行时,则暂停并告诉它的parent它须要折行,parent将建立额外的渲染对象并调用它们的layout。

 

  这里重要要说两个概念,一个是Reflow,另外一个是Repaint。这两个不是一回事。

  • Repaint——屏幕的一部分要重画,好比某个CSS的背景色变了。可是元素的几何尺寸没有变。
  • Reflow——意味着元件的几何尺寸变了,咱们须要从新验证并计算Render Tree。是Render Tree的一部分或所有发生了变化。这就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式布局,因此,若是某元件的几何尺寸发生了变化,须要从新布局,也就叫reflow)reflow 会从<html>这个root frame开始递归往下,依次计算全部的结点几何尺寸和位置,在reflow过程当中,可能会增长一些frame,好比一个文本字符串必需被包装起来。

  Reflow的成本比Repaint的成本高得多的多。DOM Tree里的每一个结点都会有reflow方法,一个结点的reflow颇有可能致使子结点,甚至父点以及同级结点的reflow。在一些高性能的电脑上也许还没什么,可是若是reflow发生在手机上,那么这个过程是很是痛苦和耗电的。

因此,下面这些动做有很大可能会是成本比较高的。

  • 当你增长、删除、修改DOM结点时,会致使Reflow或Repaint。
  • 当你移动DOM的位置,或是搞个动画的时候。
  • 当你修改CSS样式的时候。
  • 当你Resize窗口的时候(移动端没有这个问题),或是滚动的时候。
  • 当你修改网页的默认字体时。

注:display:none会触发reflow,而visibility:hidden只会触发repaint,由于没有发现位置变化。

多说两句关于滚屏的事,一般来讲,若是在滚屏的时候,咱们的页面上的全部的像素都会跟着滚动,那么性能上没什么问题,由于咱们的显卡对于这种把全屏像素往上往下移的算法是很快。可是若是你有一个fixed的背景图,或是有些Element不跟着滚动,有些Elment是动画,那么这个滚动的动做对于浏览器来讲会是至关至关痛苦的一个过程。你能够看到不少这样的网页在滚动的时候性能有多差。由于滚屏也有可能会形成reflow。

基本上来讲,reflow有以下的几个缘由

  • Initial。网页初始化的时候。
  • Incremental。一些Javascript在操做DOM Tree时。
  • Resize。其些元件的尺寸变了。
  • StyleChange。若是CSS的属性发生变化了。
  • Dirty。几个Incremental的reflow发生在同一个frame的子树上。

好了,咱们来看一个示例吧:

var bstyle = document.body.style; // cache

bstyle.padding = "20px"; // reflow, repaint  
bstyle.border = "10px solid red"; //  再一次的 reflow 和 repaint

bstyle.color = "blue"; // repaint  
bstyle.backgroundColor = "#fad"; // repaint

bstyle.fontSize = "2em"; // reflow, repaint

// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!')); 

 

  固然,咱们的浏览器是聪明的,它不会像上面那样,你每改一次样式,它就reflow或repaint一次。通常来讲,浏览器会把这样的操做积攒一批,而后作一次reflow,这又叫异步reflow或增量异步reflow。可是有些状况浏览器是不会这么作的,好比:resize窗口,改变了页面默认的字体,等。对于这些操做,浏览器会立刻进行reflow。

可是有些时候,咱们的脚本会阻止浏览器这么干,好比:若是咱们请求下面的一些DOM值:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • IE中的 getComputedStyle(), 或 currentStyle

由于,若是咱们的程序须要这些值,那么浏览器须要返回最新的值,而这样同样会flush出去一些样式的改变,从而形成频繁的reflow/repaint。

减小reflow/repaint,下面是一些Best Practices:

1.不要一条一条地修改DOM的样式。与其这样,还不如预先定义好css的class,而后修改DOM的className

// bad
var left = 10,  
top = 10;  
el.style.left = left + "px";  
el.style.top  = top  + "px";

// Good
el.className += " theclassname";

// Good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";  

 

2.把DOM离线后修改。如:

  • 使用documentFragment 对象在内存里操做DOM
  • 先把DOM给display:none(有一次repaint),而后你想怎么改就怎么改。好比修改100次,而后再把他显示出来。
  • clone一个DOM结点到内存里,而后想怎么改就怎么改,改完后,和在线的那个的交换一下。

3.不要把DOM结点的属性值放在一个循环里当成循环里的变量。否则这会致使大量地读写这个结点的属性。

  注:js中操做或者遍历对象的属性,也是很耗性能

4.尽量的修改层级比较低的DOM。固然,改变层级比较底的DOM有可能会形成大面积的reflow,可是也可能影响范围很小。

5.为动画的HTML元件使用fixed或absoult的position,那么修改他们的CSS是不会reflow的。

  fixed或absoult的position脱离DOM树

6.千万不要使用table布局。由于可能很小的一个小改动会形成整个table的从新布局。

 

7、绘制

绘制顺序

  css2定义了绘制过程的顺序——http://www.w3.org/TR/CSS21/zindex.html。这个就是元素压入堆栈的顺序,这个顺序影响着绘制,堆栈从后向前进行绘制。

  一个块渲染对象的堆栈顺序是:

  1. 背景色

  2. 背景图

  3. border

  4. children

  5. outline

相关文章
相关标签/搜索