1.浏览器渲染原理解析
想要提高网页的性能,首要的便是要理解浏览器渲染原理,下面关于浏览器的原理解析,我们以chrome内核webkit为例,其他内核的浏览器原理也基本大同小异,可触类旁通。
如上图所示,浏览器解析页面步骤可分为:
* 解析HTML(HTML Parser)
* 构建DOM树(DOM Tree)
* 构建CSSOM树(Style)
* 构建渲染树(Render Tree)
* 页面布局(Layout)
* 绘制渲染树(Painting)
这一过程可在chrome开发者工具的时间线中观察:
这里我们简要说一下以下四个概念:
* 布局(layout)
布局也称为重排或回流,布局流程输出的是一个“盒模型”,它会精确地捕获每个元素在视口内的精确位置和尺寸,HTML就是采用基于流的布局模型,页面元素的变动往往可能导致回流的发生,而回流的频发发生亦是影响页面性能的重要因素,另外,处于流后置位通常不会影响前置位的几何特征,故对后置位的修改往往比对前置位的修改对页面整体的影响要低。
* 绘制(paint)
绘制即是对DOM所分割的层(layer)进行对应的绘制,页面的回流一般都会伴随着重绘,但重绘行为的出现不一定伴随回流。
* 渲染层
层(layer)的概念对于有设计基础的人来说应该不陌生,我们平面直观所见到的图像是基于空间图层的重叠得到的,一般来说,拥有相同坐标空间的节点属于同一个渲染层。渲染层最初是用来实现层叠上下文,以此来保证页面元素以正确的顺序合成(composite),实现半透明重叠等效果。
创建渲染层的条件:
* 根元素(HTML)
* 有明确的position属性(relative,fixed,sticky,absolute)
* 透明的(opacity小于1)
* 有css滤镜(filter)
* 有css mask 属性
* 当前有对于 opacity,transform,fliter,backdrop-filter 应用动画
* overflow属性不为visible
* 等等......
* 合成层
合成层是特殊的渲染层,每个合成层有单独的绘图层,绘图层中的绘图上下文负责输出该层的位图,位图储存在共享内存中,作为纹理上传到GPU,最后由GPU将多个位图进行合成,最后绘制到屏幕上,而相对于合成层,一般的渲染层是和其第一个拥有绘图层的父层共用一个的绘图层的,提升为合成层后当需要repaint或reflow本身,不影响其它层,另外,合成层的位图会直接交由GPU合成处理,效率比CPU高。
渲染层提升为合成层的触发原因:
* 直接原因
* iframe video canvas flash 元素 有 3D transform
* backface-visibility 为 hidden
* 对 opacity、transform、fliter、backdropfilter 应用了 animation 或 transition
* will-change(设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等))
* 后代原因
* 有合成层后代同时本身有 transform、opactiy(小于 1)、mask、fliter、reflection 属性
* 有合成层后代同时本身 overflow 不为 visible
* 有合成层后代同时本身 fixed 定位
* 有 3D transform 的合成层后代同时本身有 preserves-3d 属性
* 有 3D transform 的合成层后代同时本身有 perspective 属性
* 重叠原因
* 元素的 border box(content + padding + border) 和合成层的有重叠,margin 的重叠无效
* 动画运行期间,元素可能和其他元素有重叠
2.影响页面性能的操作及优化分析
* 频繁操作DOM元素
使用js脚本频繁地操作DOM元素是影响页面性能的一大因素,频繁地对DOM进行操作可能导致页面重绘和回流的频繁发生,从而导致页面卡顿和性能消耗问题,从细节上可按如下方法进行优化:
1)使用文档片段
var fragment = document.createDocumentFragment(); //一些基于fragment的大量DOM操作
...... document.getElementById('myElement').appendChild(fragment);
2)设置DOM元素的display样式为none再操作该元素
var myElement = document.getElementById('myElement');
myElement.style.display = 'none'; //一些基于myElement的大量DOM操作
...... myElement.style.display = 'block';
3)复制DOM元素到内存中再对其进行操作
var old = document.getElementById('myElement');
var clone = old.cloneNode(true); //一些基于clone的大量操作
...... old.parentNode.replaceChild(clone, old);
4)用局部变量缓存样式信息从而避免频繁获取DOM数据
//bad operation for (var i = 0; i < paragraphs.length; i++){
paragraphs[i].style.width = box.offsetWidth + 'px';
} //better operation var width = box.offsetWidth;
for (var i = 0; i < paragraphs.length; i++){
paragraphs[i].style.width = width + 'px';
}
5)合并多次DOM操作
//bad operation var left = 10, top = 10;
el.style.top = top;
el.style.left = left; //better operation el.style.cssText += "; left: " + left + "px; top: " + top + "px;"; //better operation(将样式内容设置于某一类名,再进行元素类名绑定) el.className += " theclassName";
*css动画造成页面不流畅问题分析优化
使用css3动画造成页面的不流畅和卡顿问题,其潜在原因往往还是页面的回流和重绘,减少页面动画元素对其他元素的影响是提高性能的根本方向,而实现可如下:
1)设置动画元素position样式为absolute或fixed,可避免动画的进行对页面其它元素造成影响,导致其重排和重绘的发生;
2)避免使用margin,top,left,width,height等属性执行动画,用transform进行替代;
//bad operation div {
height: 100px;
transition: height 1s linear;
} div:hover {
height: 200px;
} //better operation div {
transform: scale(0.5);
transition: transform 1s linear;
} div:hover {
transform: scale(1.0);
}
总而言之,尽量用transform和opacity完成动画的展示,因为这两个属性可以避免重排和重绘的发生。
页面渲染的流水线其实可简单表示为以下步骤,从性能方面考虑,应该尽量避开layout和paint两个步骤,只触发composite步骤,但目前能做到这一效果的只有transform和opacity两个属性,另外需要注意的是:只有元素提升为合成层的时候transform和opacity才不会触发paint,否则依旧触发。
3)合理的提升合成层,以减少页面不必要的绘制和重排
合成层的好处是不会影响到其他元素的绘制和不被其他层所影响,因此,为了彼此之前的影响造成的性能损失,我们需合理的将动画效果中的元素或固定元素提升为合成层。
提升合成层的最好方式是使用 CSS 的 will-change 属性。将will-change 设置为 opacity、transform、top、left、bottom、right 可以将元素提升为合成层。
#target {
will-change: transform;
}
will-target的兼容性如下:
对于还不兼容该属性的浏览器,我们使用3D transform予以代替
#target {
transform: translateZ(0);
}
对于像页面顶部栏,侧栏等固定不变的位置元素,我们也可将其提升为合成层以避免其被其他元素影响而发生重绘,但要注意,合成层的提升也意味着性能的消耗增加,我们必须通过调试以测出合理的临界值,不能盲目提升合成层,此外,盲目提升合成层也可能造成重叠产生的额外合成层,容易导致层爆炸的出现,即页面连锁出现大量合成层默认提升,建议用google的timeline进行监控调试,避免出现不必要的意外消耗。