浏览器是如何渲染网页的?
我们从浏览器渲染一个页面的行为说起:
- 从服务器端获取的HTML文档中构建出DOM树(文档对象模型);
- 样式被加载和分析,构建CSSOM(CSS对象模型);
- 以DOM和CSSOM为基础,文档树被构建,一系列对象被渲染(Webkit称每一个为‘renderer’或’render 对象(render object)‘,Gecko中称为‘frame’).渲染树反应了除不可见元素(
,
display:none
)之外的DOM结构中一切可见元素,每一段字符串在渲染树中都被当做独立的渲染对象,每一个渲染对象都是其对应的DOM结构和计算所得样式的混合体,换句话说渲染树是DOM树的视觉表现; - 对于每一个渲染树元素,它的坐标都是通过计算获得的,这被称作排版(布局layout),浏览器通过文档流的方式(也就是一次就能完成所有元素的布局)(tables需要多于一次的布局);
- 最终渲染树出现在浏览器窗口上,这个过程称为绘制(painting)。
当用户与一个页面交互或者脚本修改时,由于文档结构的变化,以上的一些操作步骤会重新执行。
重现绘制(Repaint)
当元素样式变化并不影响该元素在一个网页上的位置时(background-color
,border-color
,visibility
),浏览器只会把新样式应用到该元素。
Reflow
当改变影响了文档内容或结构或者元素的位置时,reflow发生(重新布局),这一般由以下因素触发:
- DOM操作(添加,删除,更改,或者变更元素顺序);
- 内容改变(包括表格区域内文本的变化)(所占位置大小变了);
- 计算或者改变CSS属性(位置改变);
- 添加或者删除样式表;
- 改变类属性(class)(可能会改变位置);
- 浏览器窗口操作(改变大小,滚动);
- 伪类激活(可能改变位置)
怎么让浏览器充分利用渲染机制
浏览器会尽可能的限制被改变元素所在区域的重排重绘,比如说display:fixed/absolute
元素改变时只会影响它本身和它的子元素,而display:static
元素改变时会使其随后的元素都被重绘;(影响尽量少的元素)
另一个最大化性能的机制在于,当运行一系列JavaScript片段时,浏览器会缓存它们,然后一次运行。看下面的例子可以很好的理解:
var $body = $('body');
$body.css('padding', '1px'); // reflow, repaint
$body.css('color', 'red'); // repaint
$body.css('margin', '2px'); // reflow, repaint
// only 1 reflow and repaint will actually happen(由于缓存,只会重绘一次)
然而,就像上面已经提到的,调用一个元素的属性会触发强制性的reflow,当我们加上一行读元素属性的代码时就会发生;
var $body = $('body');
$body.css('padding', '1px');
$body.css('padding'); // reading a property, a forced reflow(强制发生)
$body.css('color', 'red');
$body.css('margin', '2px');
//另外一次reflow
因此,会有两次reflow,因此应该组合来读元素属性已最大化性能一个详细的例子;
$(function() {
var $body = $('body');
$body
.on('click', '.block-1', function(e) {
// 1 reflow
$body.css('padding', '1px');
$body.css('color', 'red');
$body.css('margin', '2px');
})
.on('click', '.block-2', function(e) {
// 2 reflows
$body.css('padding', '1px');
$body.css('padding');
$body.css('color', 'red');
$body.css('margin', '2px');
})
.on('click', '.block-3', function(e) {
// 3 repaints
$body.css('color', 'red');
$body.css('color');
$body.css('color', 'yellow');
$body.css('background');
$body.css('color', 'blue');
$body.css('outline');
})
.on('click', '.block-4', function(e) {
// 1 repaint
$body.css('color', 'red');
$body.css('color', 'yellow');
$body.css('color', 'blue');
$body.css('color');
$body.css('background');
$body.css('outline');
})
.on('click', '.block-5', function(e) {
// 3 reflows
$body.css('padding', '1px');
$body[0].offsetHeight;
$body.css('padding', '2px');
$body[0].offsetTop;
$body.css('padding', '3px');
$body[0].offsetWidth;
})
.on('click', '.block-6', function(e) {
// 1 reflow
$body.css('padding', '1px');
$body.css('padding', '2px');
$body.css('padding', '3px');
$body[0].offsetHeight;
$body[0].offsetTop;
$body[0].offsetWidth;
});
});
有些时候,可能你会需要一次强制性的reflow,例如:我们需要运用两次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;
}
过程代码:
// our element that has a "has-transition" class by default
var $targetElem = $('#targetElemId');
// remove the transition class
$targetElem.removeClass('has-transition');
// change the property expecting the transition to be off, as the class is not there
// anymore
$targetElem.css('margin-left', 100);
// put the transition class back
$targetElem.addClass('has-transition');
// change the property
$targetElem.css('margin-left', 50);
上述代码并不按预期工作,因为改变被缓存并在最后执行了一次,这时候我们就需要一次强制性的执行了:
达到预期效果的代码
// remove the transition class
$(this).removeClass('has-transition');
// change the property
$(this).css('margin-left', 100);
// trigger a forced reflow, so that changes in a class/property get applied immediately
$(this)[0].offsetHeight; // an example, other properties would work, too
// put the transition class back
$(this).addClass('has-transition');
// change the property
$(this).css('margin-left', 50);
现在达到预期效果了!
性能优化的建议
总结了一些有用的信息,本文有以下建议
- 构建有效的HTML和CSS,不要忘记声明文档编码方式,样式表应该包含在
标签内,脚本文件应该放在
标签的底部;
- 简化并且充分利用CSS选择器(这一条被大多数使用CSS预处理器的开发者忽略),维护最少的层状结构,以下是各选择器的效率排行
-
#id
; -
.class
; - tag
div
; - 相邻元素选择器:
a+i
; - 父选择器:
ul>li
; - 全局选择器:
*
; - 属性选择器:
input[type='text']
- 伪类/伪元素:
a:hover
-
- 应该引起注意的是,浏览器是从右向左读取选择器的,所以最右边的应该选择效率比较高的选择器
#id,.class
;
div * {...} // bad
.list li {...} // bad
.list-item {...} // good
#list .list-item {...} // good
- 在脚本中,应该尽可能的减少DOM操作,如果对象和属性会被重用,就缓存它们。在最好离线元素(未被插入文档树)(offline)上进行操作,然后把它插入DOM结构中;
- 如果使用jQuery,遵循[jQuery选择器基本原则(http://learn.jquery.com/performance/optimize-selectors/);
- 修改元素的样式时,修改class属性是做好的方法,其位置越深,越好(also because this helps decouple logic from presentation);
- 只使
display:fixed/absolute
的元素具有动画; - 不适用复杂的
:hover
动画也是一个好的实践(给添加
no-hover
属性),延展阅读;
延展阅读已获得更多信息:
- How browsers works;
- Rendering: repaint, reflow/relayout, restyle;
希望这边译文对您有用
原文链接