由一道面试题引起的思考:css
从用户输入浏览器输入url到页面最后呈现 有哪些过程?
一道很常规的题目,考的是基本网络原理,和浏览器加载css,js过程。html
答案大体以下:前端
用户输入URL地址html5
浏览器解析URL解析出主机名web
浏览器将主机名转换成服务器ip地址(浏览器先查找本地DNS缓存列表 没有的话 再向浏览器默认的DNS服务器发送查询请求 同时缓存)面试
浏览器将端口号从URL中解析出来算法
浏览器创建一条与目标Web服务器的TCP链接(三次握手)chrome
浏览器向服务器发送一条HTTP请求报文浏览器
服务器向浏览器返回一条HTTP响应报文缓存
关闭链接 浏览器解析文档
若是文档中有资源 重复6 7 8 动做 直至资源所有加载完毕
以上答案基本简述了一个网页基本的响应过程背后的原理。
但这也只是一部分,浏览器获取数据的部分,至于浏览器拿到数据以后,怎么渲染页面的,一直没太关注。
因此抽出时间研究下浏览器渲染页面的过程。
经过研究,了解一些基本常识的原理:
为何要将js放到页脚部分
引入样式的几种方式的权重
css属性书写顺序建议
何种类型的DOM操做是耗费性能的
不一样的浏览器内核不一样,因此渲染过程不太同样。
WebKit 主流程
Mozilla 的 Gecko 呈现引擎主流程
由上面两张图能够看出,虽然主流浏览器渲染过程叫法有区别,可是主要流程仍是相同的。
Gecko 将视觉格式化元素组成的树称为“框架树”。每一个元素都是一个框架。WebKit 使用的术语是“呈现树”,它由“呈现对象”组成。对于元素的放置,WebKit 使用的术语是“布局”,而 Gecko 称之为“重排”。对于链接 DOM 节点和可视化信息从而建立呈现树的过程,WebKit 使用的术语是“附加”。
因此能够分析出基本过程:
HTML解析出DOM Tree
CSS解析出Style Rules
将两者关联生成Render Tree
Layout 根据Render Tree计算每一个节点的信息
Painting 根据计算好的信息绘制整个页面
HTML Parser的任务是将HTML标记解析成DOM Tree
这个解析能够参考React解析DOM的过程,
可是这里面有不少别的规则和操做,好比容错机制,识别</br>
和<br>
等等。
感兴趣的能够参考 《How Browser Work》,中文翻译
举个例子:一段HTML
<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>
通过解析以后的DOM Tree差很少就是
将文本的HTML文档,提炼出关键信息,嵌套层级的树形结构,便于计算拓展。这就是HTML Parser的做用。
CSS Parser将CSS解析成Style Rules,Style Rules也叫CSSOM(CSS Object Model)。
StyleRules也是一个树形结构,根据CSS文件整理出来的相似DOM Tree的树形结构:
于HTML Parser类似,CSS Parser做用就是将不少个CSS文件中的样式合并解析出具备树形结构Style Rules。
浏览器解析文档,当遇到<script>
标签的时候,会当即解析脚本,中止解析文档(由于JS可能会改动DOM和CSS,因此继续解析会形成浪费)。
若是脚本是外部的,会等待脚本下载完毕,再继续解析文档。如今能够在script标签上增长属性 defer
或者async
。
脚本解析会将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM Tree和Style Rules上。
Render Tree的构建其实就是DOM Tree和CSSOM Attach的过程。
呈现器是和 DOM 元素相对应的,但并不是一一对应。Render Tree实际上就是一个计算好样式,与HTML对应的(包括哪些显示,那些不显示)的Tree。
在 WebKit 中,解析样式和建立呈现器的过程称为“附加”。每一个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树须要调用新的节点“attach”方法。
样式计算是个很复杂的问题。DOM中的一个元素能够对应样式表中的多个元素。样式表包括了全部样式:浏览器默认样式表,自定义样式表,inline样式元素,HTML可视化属性如:width=100。后者将转化以匹配CSS样式。
WebKit 节点会引用样式对象 (RenderStyle)。这些对象在某些状况下能够由不一样节点共享。这些节点是同级关系,而且:
这些元素必须处于相同的鼠标状态(例如,不容许其中一个是“:hover”状态,而另外一个不是)
任何元素都没有 ID
标记名称应匹配
类属性应匹配
映射属性的集合必须是彻底相同的
连接状态必须匹配
焦点状态必须匹配
任何元素都不该受属性选择器的影响,这里所说的“影响”是指在选择器中的任何位置有任何使用了属性选择器的选择器匹配
元素中不能有任何 inline 样式属性
不能使用任何同级选择器。WebCore 在遇到任何同级选择器时,只会引起一个全局开关,并停用整个文档的样式共享(若是存在)。这包括 + 选择器以及 :first-child 和 :last-child 等选择器。
为了简化样式计算,Firefox 还采用了另外两种树:规则树和样式上下文树。WebKit 也有样式对象,但它们不是保存在相似样式上下文树这样的树结构中,只是由 DOM 节点指向此类对象的相关样式。
样式上下文包含端值。要计算出这些值,应按照正确顺序应用全部的匹配规则,并将其从逻辑值转化为具体的值。
例如,若是逻辑值是屏幕大小的百分比,则须要换算成绝对的单位。规则树的点子真的很巧妙,它使得节点之间能够共享这些值,以免重复计算,还能够节约空间。
全部匹配的规则都存储在树中。路径中的底层节点拥有较高的优先级。规则树包含了全部已知规则匹配的路径。规则的存储是延迟进行的。规则树不会在开始的时候就为全部的节点进行计算,而是只有当某个节点样式须要进行计算时,才会向规则树添加计算的路径。
举个例子 咱们有段HTML代码:
<html> <body> <div class="err" id="div1"> <p> this is a <span class="big"> big error </span> this is also a <span class="big"> very big error</span> error </p> </div> <div class="err" id="div2">another error</div> </body> </html>
对应CSS规则以下:
1. .div {margin:5px;color:black} 2. .err {color:red} 3. .big {margin-top:3px} 4. div span {margin-bottom:4px} 5. #div1 {color:blue} 6. #div2 {color:green}
则CSS造成的规则树以下图所示(节点的标记方式为“节点名 : 指向的规则序号”)
假设咱们解析 HTML 时遇到了第二个 <div> 标记,咱们须要为此节点建立样式上下文,并填充其样式结构。
通过规则匹配,咱们发现该 <div> 的匹配规则是第 一、2 和 6 条。这意味着规则树中已有一条路径可供咱们的元素使用,咱们只须要再为其添加一个节点以匹配第 6 条规则(规则树中的 F 节点)。
咱们将建立样式上下文并将其放入上下文树中。新的样式上下文将指向规则树中的 F 节点。
如今咱们须要填充样式结构。首先要填充的是 margin 结构。因为最后的规则节点 (F) 并无添加到 margin 结构,咱们须要上溯规则树,直至找到在先前节点插入中计算过的缓存结构,而后使用该结构。咱们会在指定 margin 规则的最上层节点(即 B 节点)上找到该结构。
咱们已经有了 color 结构的定义,所以不能使用缓存的结构。因为 color 有一个属性,咱们无需上溯规则树以填充其余属性。咱们将计算端值(将字符串转化为 RGB 等)并在此节点上缓存通过计算的结构。
第二个 <span> 元素处理起来更加简单。咱们将匹配规则,最终发现它和以前的 span 同样指向规则 G。因为咱们找到了指向同一节点的同级,就能够共享整个样式上下文了,只需指向以前 span 的上下文便可。
对于包含了继承自父代的规则的结构,缓存是在上下文树中进行的(事实上 color 属性是继承的,可是 Firefox 将其视为 reset 属性,并缓存到规则树上)
因此生成的上下文树以下:
样式对象具备与每一个可视化属性一一对应的属性(均为 CSS 属性但更为通用)。若是某个属性未由任何匹配规则所定义,那么部分属性就可由父代元素样式对象继承。其余属性具备默认值。
若是定义不止一个,就会出现问题,须要经过层叠顺序来解决。
一些例子:
* {} /* 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 */
利用上面的方法,基本能够快速肯定不一样选择器的优先级。
建立渲染树后,下一步就是布局(Layout),或者叫回流(reflow,relayout),这个过程就是经过渲染树中渲染对象的信息,计算出每个渲染对象的位置和尺寸,将其安置在浏览器窗口的正确位置,而有些时候咱们会在文档布局完成后对DOM进行修改,这时候可能须要从新进行布局,也可称其为回流,本质上仍是一个布局的过程,每个渲染对象都有一个布局或者回流方法,实现其布局或回流。
对渲染树的布局能够分为全局和局部的,全局即对整个渲染树进行从新布局,如当咱们改变了窗口尺寸或方向或者是修改了根元素的尺寸或者字体大小等;而局部布局能够是对渲染树的某部分或某一个渲染对象进行从新布局。
大多数web应用对DOM的操做都是比较频繁,这意味着常常须要对DOM进行布局和回流,而若是仅仅是一些小改变,就触发整个渲染树的回流,这显然是很差的,为了不这种状况,浏览器使用了脏位系统,只有一个渲染对象改变了或者某渲染对象及其子渲染对象脏位值为”dirty”时,说明须要回流。
表示须要布局的脏位值有两种:
“dirty”–自身改变,须要回流
“children are dirty”–子节点改变,须要回流
布局是一个从上到下,从外到内进行的递归过程,从根渲染对象,即对应着HTML文档根元素,而后下一级渲染对象,如对应着元素,如此层层递归,依次计算每个渲染对象的几何信息(位置和尺寸)。
每个渲染对象的布局流程基本如:
1.计算此渲染对象的宽度(width);
2.遍历此渲染对象的全部子级,依次:
2.1设置子级渲染对象的坐标
2.2判断是否须要触发子渲染对象的布局或回流方法,计算子渲染对象的高度(height)
3.设置此渲染对象的高度:根据子渲染对象的累积高,margin和padding的高度设置其高度;
4.设置此渲染对象脏位值为false。
在绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。绘制工做是使用用户界面基础组件完成的。
CSS2 规范定义了绘制流程的顺序。绘制的顺序其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,所以这样的顺序会影响绘制。块呈现器的堆栈顺序以下:
背景颜色
背景图片
边框
子代
轮廓
这里还要说两个概念,一个是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,由于没有发现位置变化。
基本上来讲,reflow有以下的几个缘由:
Initial。网页初始化的时候。
Incremental。一些Javascript在操做DOM Tree时。
Resize。其些元件的尺寸变了。
StyleChange。若是CSS的属性发生变化了。
Dirty。几个Incremental的reflow发生在同一个frame的子树上。
看几个例子:
$('body').css('color', 'red'); // repaint $('body').css('margin', '2px'); // reflow, repaint 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。
页面的渲染详细过程能够经过chrome开发者工具中的timeline查看
发起请求;
解析HTML;
解析样式;
执行JavaScript;
布局;
绘制
浏览器对上文介绍的关键渲染路径进行了不少优化,针对每一次变化产生尽可能少的操做,还有优化判断从新绘制或布局的方式等等。
在改变文档根元素的字体颜色等视觉性信息时,会触发整个文档的重绘,而改变某元素的字体颜色则只触发特定元素的重绘;改变元素的位置信息会同时触发此元素(可能还包括其兄弟元素或子级元素)的布局和重绘。某些重大改变,如更改文档根元素的字体尺寸,则会触发整个文档的从新布局和重绘,据此及上文所述,推荐如下优化和实践:
HTML文档结构层次尽可能少,最好不深于六层;
脚本尽可能后放,放在前便可;
少许首屏样式内联放在标签内;
样式结构层次尽可能简单;
在脚本中尽可能减小DOM操做,尽可能缓存访问DOM的样式信息,避免过分触发回流;
减小经过JavaScript代码修改元素样式,尽可能使用修改class名方式操做样式或动画;
动画尽可能使用在绝对定位或固定定位的元素上;
隐藏在屏幕外,或在页面滚动时,尽可能中止动画;
尽可能缓存DOM查找,查找器尽可能简洁;
涉及多域名的网站,能够开启域名预解析
浏览器渲染是个很繁琐的过程,其中每一步都有对应的算法。
了解渲染过程原理能够有针对的性能优化,并且也能够懂得一些基本的要求和规范的原理。
最后文章中间不少语句都是直接复制的原文,本身的语言概况仍是不及原文精彩。