作者:Alexander Skutin , 2014.5.26 . 由Max shirshin与2014年6月30日翻译(俄语 -> 英语)
现今我们应更加注重网页渲染,及其在web开发中的重要性。虽然很多文章都曾谈到这一主题,但大多是分散和割裂的。譬如为了对这个主题有更全面的认识需要去搜索很多的信息来源,而这也是笔者决定写这篇文章的原因。笔者相信本篇文章会有益于初级开发者,当然对希望能够更新和整理已有知识的中高级开发者同样能够有所裨益。
当页面布局定义完成后,页面渲染的过程与样式和脚本所承担的重要角色一样,也需要在初期就开始进行优化。专业的开发者需要了解这些技巧以避免性能的问题。
本篇文章并不会详细介绍浏览器的内部工作机制,但将会提供一些通用的规则。这是由于不同的浏览器引擎有着不同的工作方式,太针对与某一个特定浏览器的研究会使这个过程变得过于复杂。
浏览器如何渲染网页?
首先概述浏览器渲染网页的过程:
1、文档对象模型(Document Object Model = DOM)将首先从来自于服务器的HTML中生成。
2、加载并解析样式,形成样式对象模型(CSS Object Mode = CSSOM)。
3、在DOM和CSSOM上层,创建一个渲染树,这是由将被渲染的对象形成的集合(webkit内核称这些对象为“渲染者”或“渲染对象”,在Gecko内核中则称为“帧”)。渲染树会影响DOM结构,但隐藏元素则不在此列(例如<head>标签,或具有display:none属性的元素)。在渲染树中的每个文本字符串都被表述为一个单独的渲染对象,而每个渲染对象都将包含其应有的DOM结构(或文本块)以及计算后的样式。从另一个角度上说,这个渲染树描述了DOM中的可见表征。
4、计算渲染树中元素的坐标,从而成为“层”。浏览器使用一系列方法(这一系列方法仅需要一个节拍(pass))来放置所有的元素(表格需要多个节拍)。
5、最后,页面将在浏览器窗口中显示出来,这个过程称为“绘制”。
当用户与页面交互时,或者脚本改变页面时,前面提及的这些操作都会重复执行,页面布局改变时同样如此。
重绘
当改变了元素样式,而这个变动并不会影响元素在页面中的位置时(例如background-color,border-color,visibility),浏览器会依据新的样式仅重绘这个元素(即重绘和重新应用样式会发生)
重新布局
当改变影响了文档内容、结构、或元素位置时,布局会重新应用和生成。这些改变通常由以下行为触发:
- DOM操作(元素的添加、删除、修改以及调整顺序)
- 内容变更,包括表单域中的文字修改
- CSS属性的计算和修改
- 样式表的添加和移除
- class属性的修改
- 浏览器窗口变动(大小、滚动)
- 伪类激活(:hover)
如何优化
浏览器渲染
浏览器会尽可能的将重绘和重排限制在受影响的元素区域内。例如,对一个绝对定位/悬浮定位的元素尺寸大小的变更会仅仅影响这个元素及他的后代,然而一个静态定位元素的尺寸变化会让在其之后的所有元素都触发重新布局。
另一个优化的技巧是,当运行javascript代码时,浏览器会缓存这些变更,然后在代码运行之后,将变更放到一个单独的节拍中再应用。例如,下面的代码会仅触发一次重绘和重新布局。
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', 'red');
$body.css('margin', '2px');
最终,我们将造成两次重排而并非只是一次。正因如此,你需要将获取元素属性归到一次,来优化性能。(来看示例)
当你不得不触发强制重绘时,会有一些状况发生。例如:我们将要把一条相同的属性两次应用到一个相同的元素上。最初,这个元素会以无动画的方式设为100px,然后需要带动画的调整为50px。你可以通过这个示例研究,但我会更详细的描述这个情况。
我们首先创建一个具有transition属性的css类
.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;
}
然后进行以下处理:
// 具有has-transition类的元素
var $targetElem = $('#targetElemId');
//移除has-transition类
$targetElem.removeClass('has-transition');
// 在css类不再存在后,改变这个属性确认动画已经去除
$targetElem.css('margin-left', 100);
// 增加has-transition类
$targetElem.addClass('has-transition');
// 修改属性
$targetElem.css('margin-left', 50);
这个实现并不会像期望的方式工作。这些改变被缓存起来,并会在最后的代码块中应用。因为我们需要一个强制的重排,通过以下方式可以实现:
//移除has-transition类
$(this).removeClass('has-transition');
//修改属性
$(this).css('margin-left', 100);
// 触发强制重排,从而在class和属性中的改变可以立刻生效
$(this)[0].offsetHeight;
// 示例,其他的属性也可以实现
// 增加has-transition类
$(this).addClass('has-transition');
// 修改属性
$(this).css('margin-left', 50);
现在将会按设想中的工作了。
实际开发中的优化建议
通过总结一些有用的信息,(作者)提供以下建议:
- 创建有效的html和css,但不要忘记明确文档编码。样式可以添加到<head>中,脚本则添加到<body>的尾部。
- 尝试简化和优化css选择器(使用CSS预处理的开发者通常不会注意这种优化方法),尽量保持低级的css嵌套。这也是css选择器标识性能的方法(从最快的那个开始)。
-
- 1、标识符 #id
- 2、类 .class
- 3、标签: div
- 4、兄弟选择器:a+i
- 5、邻接父子节点选择器 ul > li
- 6、通用选择器:*
- 7、特性选择器:input[type="text"]
- 8、伪类和伪元素:a:hover。你需要记住,浏览器处理CSS选择器的顺序是从右至左,这就是为什么最右边的选择器应当是最快的那种,比如#id或.class
div * {...} 坏
.list li {...} 坏
.list-item {...} 好
#list .list-item {...} 好
1、在你的脚本中,尽可能的减少对dom的操作,尽量缓存那些会需要重复用到类型和对象。在执行复杂的操作时,比较好的方法是:操作一个“离线”的元素,并在结束后重新增加到DOM树中(离线的元素即脱离了DOM后在内存中保存的元素)。
3、为了修改元素的样式,修改元素的"class"属性是通常会采用的方法。执行这种操作的DOM节点的层次越深越好。(同时也是由于深层次定义有利于逻辑解耦)
4、如果可以的话,仅对那些绝对定位或悬浮定位的元素执行动画。
5、滚动时去除复杂的伪元素动画(例如:hover,可以给body增加一个额外的没有hover动画的样式)。
若需要更详细的说明,请参见以下文章:
1. How browsers work
2. Rendering: repaint, reflow/relayout, restyle
希望本文能够有所启发!
30th June 2014