浏览器的渲染原理的研究
最近看了好几篇大神的文章,发现自己虽然前端已经学了快一年半,可是再css却仿佛停留在了第一个瓶颈上,会使用,但是却很少去研究它是怎么来的,浏览器是如何去解析的。挺多东西还是一知半解,很羞愧,故写下我对 浏览器对整个页面的渲染原理 的理解。
说到这点,我们一定很想知道,一个项目 html css js是如何与浏览器发生‘化学反应’,最终生成美丽的页面的。
假设为第一次访问:
1. 用户输入网址,浏览器向服务器发出请求,服务器返回html文件;
2. 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文件;
3. 这是 浏览器 就应该 又发出CSS文件的请求,服务器返回这个CSS文件;
4. 浏览器继续载入html中<body>部分的代码,并且CSS文件已经拿到手了,可以开始渲染页面了;
5. 浏览器在代码中发现一个<img>标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码;
6. 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;
7. 浏览器发现了一个包含一行Javascript代码的<script>标签,赶快运行它;
8. Javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个<div> (style.display=”none”)。突然就少了这么一个元素,浏览器不得不重新渲染这部分代码;
9. 终于等到了</html>的到来,浏览器泪流满面……
10. 等等,还没完,用户点了一下界面中的“换肤”按钮,Javascript让浏览器换了一下<link>标签的CSS路径;
11. 浏览器召集了所有的标签,浏览器向服务器请,又得重新来过……
求了新的CSS文件,重新渲染页面。
浏览器对CSS的匹配原理
浏览器CSS匹配是从右到左进行查找。
比如 DIV#divBox p span.red{color:red;},浏览器的查找顺序如下:先查找html中所有class=’red’的span元素,找到后,再查找其父辈元素中是否有p元素,再判断p的父元素中是否有id为 divBox的div元素,如果都存在则CSS匹配上。
浏览器从右到左进行查找的好处是为了尽早过滤掉一些无关的样式规则和元素。firefox称这种查 找方式为keyselector(关键字查询),所谓的关键字就是样式规则中最后(最右边)的规则,上面的key就是span.red。
浏览器是如果解析html与css的呢
我们先看一张图
从上面这个图中,我们可以看到那么几个事:
1)浏览器会解析三个东西:
- 一个是HTML/SVG/XHTML,事实上,Webkit有三个C++的类对应这三类文档。解析这三种文件会产生一个DOM Tree。
- CSS,解析CSS会产生CSS规则树。
- Javascript,脚本,主要是通过DOM API和CSSOM API来操作DOM Tree和CSS Rule Tree.
2)解析完成后,浏览器引擎会通过DOM Tree 和 CSS Rule Tree 来构造 Rendering(渲染) Tree。注意:
- Rendering Tree渲染树 并不等同于DOM树,因为一些像Header或display:none的东西就没必要放在渲染树中了。
- CSS 的 Rule Tree主要是为了完成匹配并把CSS Rule附加上Rendering Tree上的每个DOM结点。也就是所谓的Frame(框架)。
- 然后,计算每个Frame(也就是每个节点)的位置,这又叫layout(布局)和reflow(回流)过程。
3)最后通过调用操作系统Native GUI的API绘制。
DOM解析
HTML的DOM Tree解析如下:
CSS解析
CSS的解析大概是下面这个样子,假设我们有下面的HTML和CSS文档:
于是DOM Tree是这个样子:
于是我们的CSS Rule Tree会是这个样子:
注意,图中的第5条规则出现了两次,一次是独立的,一次是在规则4的子结点。所以,我们可以知道,建立CSS Rule Tree是需要比照着DOM Tree来的。CSS匹配DOM Tree主要是从右到左解析CSS的Selector,好多人以为这个事会比较快,其实并不一定。关键还看我们的CSS的Selector怎么写了。
注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,你就会在N多地方看到很多人都告诉你,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去,……
通过这两个树,我们可以得到一个叫Style Context Tree,也就是下面这样(把CSS Rule结点Attach到DOM Tree上):
渲染
渲染的流程基本上如下(黄色的四个步骤):
- 计算CSS样式
- 构建Render Tree
- Layout – 定位坐标和大小,是否换行,各种position, overflow, z-index属性 ……
- 正式开画
这个图流程中有很多连接线,这表示了Javascript动态修改了DOM属性或是CSS属会导致重新Layout,有些改变不会:就是那些指到天上的箭头,比如,修改后的CSS rule没有被匹配到,等。
这里重要要说两个概念,一个是Reflow,另一个是Repaint。这两个不是一回事。
- Repaint——屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变。
- Reflow——意味着元件的几何尺寸变了,我们需要重新验证并计算渲染树。是Render Tree的一部分或全部发生了变化。这就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式布局,所以,如果某元件的几何尺寸发生了变化,下面的就需要重新布局,也就叫reflow)reflow 会从<html>这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置。
所以,下面这些动作有很大可能会是动作比较大的。
- 当你增加、删除、修改DOM结点时,会导致Reflow或Repaint
- 当你移动DOM的位置,或是搞个动画的时候。
- 当你修改CSS样式的时候。
- 当你调整窗口的时候(响应),或是滚动的时候。
- 当你修改网页的默认字体时。
注:display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发现位置变化。
关于滚屏,通常来说,如果在滚屏的时候,我们的页面上的所有的像素都会跟着滚动,那么性能上没什么问题,因为我们的显卡对于这种把全屏像素往上往下移的算法是很快。但是如果你有一个fixed的背景图,或是有些Element不跟着滚动,有些Elment是动画,那么这个滚动的动作对于浏览器来说会是相当相当痛苦的一个过程。你可以看到很多这样的网页在滚动的时候性能有多差。因为滚屏也有可能会造成reflow。
基本上来说,reflow有如下的几个原因:
- Initial(初始化)。网页初始化的时候。
- Incremental(增减计算)。一些Javascript在操作DOM Tree时。
- Resize(调整大小)。其些元件的尺寸变了。
- StyleChange。如果CSS的属性发生变化了。
- Dirty。几个Incremental的reflow发生在同一个frame的子树上。
好了,我们来看一个示例吧:
我们的浏览器是聪明的,它不会像上面那样,你每改一次样式,它就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。
2)把DOM离线后修改。如:
- 使用documentFragment 对象在内存里操作DOM
- 先把DOM给display:none(有一次reflow),然后你想怎么改就怎么改。比如修改100次,然后再把他显示出来。
- clone一个DOM结点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下。
3)不要把DOM结点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。
4)尽可能的修改层级比较低的DOM。当然,改变层级比较底的DOM有可能会造成大面积的reflow,但是也可能影响范围很小。
5)为动画的HTML元件使用fixed或absoult的position,修改他们的CSS是不会reflow的,因为脱离了文档流。
6)少使用table布局。因为可能很小的一个小改动会造成整个table的重新布局。
关于 页面优化 的几点:
1.DOM结构不要复杂,不必要的标签坚决不要,例如:<div class="clearfix"></div>这样一个清除浮动的标签
增加伪元素after
兼容IE67也是可以利用IE67的一些特有的属性来清除浮动,比如zoom:1
2.少引用css,js文件,少增加图片的请求次数
3. css 命名 、书写规范;(好的代码看上去就很整齐 很有条理性这样方便日后的维护和管理)
4.少用滤镜,少用hack,多用继承属性。
5.使用简写样式,如background,margin,padding等。
6.不要在ID选择器和class选择器前 使用标签名
例如:div.box { color: #f00; }; 直接 可以 用类名, .box { color:#f00;} 这样浏览器找到这个class后 就不用再匹配是否存在div标签.从而提高了渲染效率。
7.css的层级关系不要太深 用class直接代替多余的层级元素。css渲染是从上到下,从右到左的。
例如 .box .box-con .box-list li { line-height: 24px; } 所以直接这样写就可以了.box-list li { line-height: 24px; };
8.平铺背景图片不要过小,影响渲染速率。
9. float使用要谨慎。
10.合理化布局(模块化布局);可以把样式划分为 基类 和扩展类 ;模块化布局 :模块基本相同的样式写在 基类里,不同的在重新用class来定义称为扩展类 。
11. 在css渲染效率中id和class的效率是基本相当的
参考文档:前端必读:浏览器内部工作原理