详读了很多文章,最终对比总结出来的浏览器渲染机制,并提出相应的优化原则
浏览器如何渲染网页
浏览器将从服务器中获取的HTML文档逐步解析,构建DOM树
在构建DOM树时,如果碰到JS和CSS,会加载执行并阻塞HTML的解析,即HTML解析器会将控制权交给JS或CSS解析器,当这个元素被解析完之后,将控制权重交回给HTML解析器,直到整个DOM树构建完成
css样式被载入和解析后,会构成重叠样式表CSSOM
在DOM和CSSOM的基础上,渲染树(rendering tree)将会被创建,代表一些列将被渲染的对象。渲染树映射除了不可见元素外的所有DOM结构。每段文本字符串都将被划分在不同的渲染对象中,每个渲染对象都包含了它相应的DOM对象以及计算后的样式。即,渲染树是DOM的直观表示
渲染树的每个元素包含的内容都是计算过的,它被称之为布局layout(浏览器使用的一种流式处理的方法,只需要一次pass绘制操作就可以布局所有的元素,..tables需要多次pass绘制,pass表示像素处理和定点处理)
最后布局完成,渲染树将转化为屏幕上的实际内容,即绘制painting
重绘Repaint
当页面中的元素样式不改变元素在文档流的位置时,浏览器只会将新样式赋予元素并进行重绘操作
回流Reflow
当改变影响文档内容或者结构,或者元素位置时,回流操作就被触发,一般有下面几种情况
- DOM操作,对元素的增删改,顺序变化等
- 内容变化,包括表单区域中的文本改变
- CSS属性的更改或者重新计算
- 增删样式表内容
- 修改class属性
- 浏览器窗口变化
- 伪类样式激活
浏览器如何优化渲染
浏览器本身会尽可能的减少重绘或回流的次数,只更改必要的元素。例如position设置为absolute/fixed的元素只会影响其本身和其子元素,而static的元素变化则会影响其之后的所有的页面元素
另外一项技术则是,在js代码运行时,浏览器会缓存所有的变化,然后只通过一次pass绘制操作来应用这些更改。例如下面这些代码就只触发了一次重绘和回流
var $body = $('body');
$body.css('padding', '1px'); // 触发重绘与回流
body.css(‘color′,‘red′);//触发重绘
body.css(‘margin’, ‘2px’); // 触发重绘与回流
// 最终只有一次重绘和回流被触发
然而,根据我们提到过的,获取某个元素的属性会触发强制回流,比如在上面的代码做一些修改
var $body = $('body');
$body.css('padding','1px');//触发重绘与回流
$body.css('color'); // 触发强制回流
$body.css('margin', '2px'); // 触发重绘与回流
// 再进行一次回流,最终会有两次回流被触发
因此,我们在修改元素时,应该尽量避免或者合并读取元素的属性,来减少回流触发,优化性能
当然我们有的时候不得不触发强制回流,比如使一个元素的margin-left先为100px,在通过动画变回50px
.has-transition {
-webkit-transition: margin-left 1s ease-out;
-moz-transition: margin-left 1s ease-out;
-o-transition: margin-left 1s ease-out;
transition: margin-left 1s ease-out;
}
// 此处的属性改变没有动画效果
$targetElem.css('margin-left', 100);
// 再加上动画效果的属性名
$targetElem.addClass('has-transition');
// 这次改变有动画效果
$targetElem.css('margin-left', 50);
事实上这段代码并不像我们注释中写的那样进行,由于每条语句会被缓存,所以我们只能看到最终的结果而不能看到动画,所以我们就需要加一条强制回流语句使得动画效果得以顺利进行
// 此处的属性改变没有动画效果
$targetElem.css('margin-left', 100);
//任意的可能触发回流的语句都可以
$targetElem.css('margin-left')
// 再加上动画效果的属性名
$targetElem.addClass('has-transition');
// 这次改变有动画效果
$targetElem.css('margin-left', 50);
优化渲染效率的几条实践原则
- 合理书写html,减少嵌套和多余标签,样式放在标签中,js文件放在闭合前
优化css选择器,尽量减少选择器的嵌套
浏览器中css选择器是从右向左进行匹配的(为什么选择器要从右到左进行匹配),减少嵌套层能提高匹配效率,并且css选择器根据其优先级具有不同的运行效率- ID选择器: #id(因为其唯一性强)
- 类选择器: .class
- 标签选择器: div
- 相邻选择器: a + i
- 子元素选择器: ul > li
- 通用选择器: *
- 属性选择器: input[type=”text”]
- 伪类选择器: a:hover
在js中,尽量减少DOM操作,缓存所有内容,包括属性和对象(如果他们要被复用的话),尽量在缓存之后再进行操作(减少回流),最后再添加到DOM中
- 使用jQuery时,建议遵循jQuery最佳实践
- 建议通过改变class值来修改样式,选择器越有针对性越好(这也有助于分离页面样式和逻辑)
- 尽量只对position值为absolute/fixed的元素设置动画
- 页面滚动时禁用:hover样式效果
了解更多
如果想更深入底层进行了解可以查阅:
1. How browsers work
2. 【译】浏览器渲染:repaint,reflow/relayout,restyle