页面渲染:性能分析

11,043 阅读11分钟

CSS属性的渲染影响列表>>


performance介绍


Chrome DevTools的performance面板可以记录和分析页面在运行时的所有活动。

配合无痕模式,可以避免chrome插件的影响。

帧率图

1. 录制查看

上面部分是帧数信息:

  • 1312.3ms:一帧的时间
  • 1fps: 每秒的帧数

下面部分是网页快照,浏览器按照一定时间间隔截取。

2. 实时查看

快捷键ctrl + shift + p可以实时查看帧率

还可以进行其它配置

  • Paint Flashing 高亮显示网页中需要被重绘的部分。
  • Layer Borders 显示Layer边界。
  • FPS Meter 每一秒的帧细节,帧速率的分布信息和GPU的内存使用情况。
  • Scrolling Performance Issues 分析鼠标滚动时的性能问题,会显示使屏幕滚动变慢的区域。
  • Emulate CSS Media 仿真CSS媒体类型,查看不同的设备上CSS样式效果,可能的媒体类型选项有print、screen

火焰图

  • Loading:网络通信和HTML解析
  • Scripting:Javascript执行
  • Rendering:样式计算和布局,即重排
  • Painting:重绘 对应的详细事件 >>>

怎么看出性能问题?

1. 红色小三角

常见的原因为:

  • handler took xxx ms 操作消耗太多时间
  • forced reflow is likely performance bottleneck “强制同步布局”可能会导致性能问题,通常是因为修改样式后读取属性,导致了浏览器必须重新渲染以获取最新的属性值。

2. 布局抖动

“布局抖动”是指反复出现“强制同步布局”情况。 这种情况会在 JavaScript 从 DOM反复地写入和读取时出现,将会强制浏览器反复重新计算布局。布局抖动也会导致长帧,使页面卡顿。

3. 长帧

长帧表示一帧的时间过长,会影响页面的加载速度与动画的流畅性,这时会感受到页面加载慢或动画卡顿。

动画播放时每秒的帧数最好能够达到60帧,也就是每帧16.6ms。


实例


1. 解析html(不包含js css外部文件)

  • readystatechange(第一个)(文档已加载和解析) 此时状态为interactive,表示文档已加载和解析但资源仍在加载,该状态通常紧接着会触发DomContentLoaded。
  • DOMContentLoaded (DOM树构建完成) html文档被加载和解析成功,DOM树构建完成时会触发。
  • Recalculate Style(CSSOM构建完成) 通过添加和删除元素,更改属性、类或通过动画来更改 DOM,全都会导致浏览器重新计算元素样式,在很多情况下还会对页面或页面的一部分进行布局(即自动重排)。重新计算样式的步骤可以分为两步:
  1. 浏览器计算出给指定元素应用哪些类、伪选择器和 ID。
  2. 从匹配选择器中获取所有样式规则,并计算出此元素的最终样式。
  • readystatechange(第二个)(文档已加载和解析,且资源也加载完成) 此时状态为complete,表示文档和资源都已加载完成,该状态通常紧接着会触发load。
  • load事件 文档和资源都已加载完成时会触发。更多页面加载完成事件>>
  • Layout 布局几乎总是作用在整个文档,但还是主要看影响的节点个数。

2. 解析html(包含js css外部文件)

  • Evaluate Script (执行js)
  • layout变为不在ParseHtml中执行 可能是因为CSS文件或JS文件的加载阻塞了整个页面的渲染过程,因为js和css都可能对标签进行样式的设置。如果不存在文件,就不会存在等待加载的问题。

3. 改变背景色(重绘)

4. 改变高度(重排)

相对于重绘多了个Layout

5. 图片资源加载(img或bg)

如果图片标签尺寸不变,则会触发一次重绘


遵循的原则


1. 关于阻塞

  • css加载 不会阻塞 DOM树的解析;CSS解析 会阻塞;
  • css加载和解析 会阻塞 js(所以内联样式不用加载性能较高,适用于第一屏)
  • js 会阻塞 DOM树的解析 (因为js会改变DOM树内容)
  • css引入的字体文件加载 也会阻塞 js , 页面渲染

2. 关于与页面渲染过程的对应

  1. js执行时:这时应该只是构建了前面部分的dom树和CSSOM树,因为js需要通过dom api和CSSOM api操作前面部分的标签的内容和样式。
  2. DOM树构建完成:DomContentLoaded事件
  3. CSSOM构建完成、Render Tree构建完成:Recalculate Style
  4. Layout:Layout事件
  5. paint:Paint(图片层绘制) 和 Composite Layers(图片层合并),除了transform 或 opacity属性之外,更改任何属性始终都会触发绘制Paint
  6. reflow重排:3 4 5步走一遍
  7. repaint重绘:3 5步走一遍
  8. 更改一个既不要布局也不要绘制的属性:3步 + Composite Layers,此行为在678重新渲染步骤中开销最小,适合动画或滚动,具体比如transfrom opacity。

3. 关于chrom浏览器的一些行为

  • 渲染队列:浏览器存在一个渲染队列,用于将多次连续的重排和重绘操作变成一次。当你进行DOM的读操作时,如果队列不为空,chrome会清空队列,立即进行重排或重绘;如果为空,chrome不会做出多余的操作。
  • 布局:布局或重排中浏览器需要计算元素要占据的空间大小及其在屏幕的位置,网页的布局模式意味着一个元素可能影响其他元素,例如 <body> 元素的宽度一般会影响其子元素的宽度以及树中各处的节点。
  • 绘制与合成:绘制一般是在多个表面(通常称为层)上完成的,因此浏览器需要将它们按正确顺序绘制到屏幕上,以便正确渲染页面。
  • css选择器:对于复杂的css选择器,浏览器需要花更多时间来确定元素的样式,因此以类为中心的css编写原则,老外比较推崇,比如:nth-last-child伪类可以用独立的类替代(不然它怎么叫伪类( ̄▽ ̄)")。

性能优化


阻塞优化

  • js存在问题: 1)js加载和执行都会阻塞DOM解析和页面渲染 2)如果引用第三方脚本,当第三方服务商请求延迟时,页面会白屏; js解决办法: 页面可以通过添加关键字defer和async来异步加载js。

  1. 两者都是异步加载,不同的是: async(参考ajax):异步加载时不会阻塞DOM解析和页面渲染;执行时间为加载完成时;执行时会阻塞,但这时可能DOM已经解析完成,甚至页面已经渲染;另外会影响js文件执行顺序。 defer:异步加载时不会阻塞DOM解析和页面渲染;执行时间为DOM解析完成之后、DOMContentLoaded事件之前(所以会阻塞DCLoad事件和jquery的ready事件);执行时DOM已经解析完成,只会阻塞页面渲染。
  2. js文件加载顺序 同步 > 异步 同是async 按加载完成顺序 同是defer 按引入顺序 async defer 正常不一起用

减少重新渲染

关于CSS

  • 使用简单的样式表。样式表越简单,重排和重绘就越快。具体为:
  1. 减低选择器的复杂性,少用伪类;使用以类为中心的方法,例如BEM;
  2. 减少必须计算其样式的元素的数量,应当尽可能减少声明为无效的元素的数量。
  • 减少DOM元素层级。重排和重绘的DOM元素层级越高,成本就越高。
  • 多利用display:none。display:none的元素没有在渲染树,因而也不会进行重排和重绘
  • 使用CSS动画而不是JS动画。CSS动画优于JS动画,是由于CSS改变的是translate的值,不会引起offsetLeft、offsetTop等位置值的改变。
  • 使用absolute而不是float。position属性为absolute或fixed的元素在重排的开销比float少,因为不用考虑它对其他元素的影响。
  • 使用div而不是table。因为一个很小的改动可能都会引起整个table的重新布局,比如说td内容改变。

关于JS

  • DOM的多个读操作(或多个写操作)应该放在一起,不要穿插进行。因为连续地设置元素样式(写操作),浏览器会一次性执行,即只触发一次重排或重绘,但如果在几个写操作间插入读取样式的操作,浏览器则不得不立即重排或重绘。
  • 一次性改变样式。不要一条条地改变样式,而要通过改变class,或者el.style.csstext属性。
  • 使用离线DOM来改变元素样式。比如cloneNode()克隆节点,然后再替换掉元素节点 或者 display:none → 改动 → 显示
  • 尽量修改层级较低的DOM。
  • 不要在循环中重复读取DOM节点属性值。
  • 使用 window.requestAnimationFrame()、window.requestIdleCallback() 这两个方法控制重新渲染。

提高fps(frame per second)

网页动画的每一帧(frame)都是一次重新渲染,将一帧送到屏幕会采用如下顺序:

帧数与动画流畅度的关系如下:

  • < 24帧 :人眼能感受到停顿
  • 30 - 60 帧数:比较流畅
  • 70 - 80:德芙,纵享新丝滑 考虑到多数显示器的刷新频率为60Hz,即每秒刷新60次,如果fps高于60,每秒将输出高于60张的画面,那么假如fps > 刷新频率,多输出的画面将是无效的帧数,流畅度没有任何提升,但也没啥坏处(手动滑稽d:)。

这里想说地是fps最好能达到浏览器刷新频率,因而在播放动画的时候,注意不要执行太多耗时耗性能的操作。

手动控制重新渲染

window.requestAnimationFrame() 方法可以将某些代码统一放到下一次重新渲染时执行。具体是将js代码放在下一帧开始时执行。如果使用setTimeout 或 setInterval 来执行动画之类的视觉变化,其回调可能在帧的某个时间点执行,可能在末尾,这会使我们丢失帧,导致卡顿。

  1. 处理“布局抖动” 反复读写属性会导致布局抖动,导致长帧。
function doubleHeight(element) {
    var currentHeight = element.clientWidth;
    element.style.width = (currentHeight / 2) + 'px';
    element.style.height = '80px';
}
var elements = document.getElementsByTagName('tr');
for (var i = 0; i < elements.length; i++) {
    doubleHeight(elements[i]);
}

将doubleHeight函数改成下面这样:

function doubleHeight(element) {
    var currentHeight = element.clientHeight;
    window.requestAnimationFrame(function () {
      element.style.height = (currentHeight * 2) + 'px';
    });
}

2)页面滚动事件(scroll)

$(window).on('scroll', function() {
   window.requestAnimationFrame(scrollHandler);
});

3)最适合用于动画

结合项目

现在项目中,页面(以“任务”页面为例)在加载时都会请求一些ajax数据,比如datagrid,tree数据等等,还有些ajax数据只是预加载。如果这些ajax在页面渲染前完成请求,则会阻塞页面渲染。所以同一个页面不同网速下会有两种渲染顺序:

  • 在渲染之后执行

  • 在渲染之前执行

解决办法

  1. 在所有资源加载完后进行ajax请求。将datagrid等控件的数据加载放在$(window).load()事件中。
  2. 延迟初始化modal中的内容

效果


问题


问:请求js文件时,请求和执行顺序是什么?

答:请求会一起发出;执行顺序按引入的顺序,不会因为后一个先返回数据而先执行。

问:页面加载时提前ajax请求一些数据,会不会影响性能?

答:可能会,可能不会。ajax请求时不影响性能,请求后执行回调函数会影响。ajax回调函数会在请求完成且js主程序运行完后执行,等到我们能看到页面,需要经历页面解析和渲染这两个过程,如果页面渲染前ajax回调执行了,那将阻塞渲染过程。

问:为什么jquery通常在ready方法中执行代码?

答:ready 监测的是 DOMContentLoaded事件,也就是监测DOM树构建完成。

附录


loading 事件

事件描述
Parse HTML 浏览器执行HTML文件解析
Parse Stylesheet 浏览器执行CSS文件解析(单指外部CSS文件)
Finish Loading 网络请求完毕事件
Receive Data 请求的响应数据到达事件,如果响应数据很大(拆包),可能会多次触发该事件
Receive Response 响应头报文到达时触发
Send Request 发送网络请求时触发

Scripting事件

事件描述
Animation Frame Fired 一个定义好的动画帧发生并开始回调处理时触发
Cancel Animation Frame 取消一个动画帧时触发
GC Event 垃圾回收时触发
DOMContentLoaded 当页面中的DOM内容加载并解析完毕时触发
Evaluate Script A script was evaluated.
Event js事件
Function Call 只有当浏览器进入到js引擎中时触发
Install Timer 创建计时器(调用setTimeout()和setInterval())时触发
Request Animation Frame A requestAnimationFrame() call scheduled a new frame
Remove Timer 当清除一个计时器时触发
Time 调用console.time()触发
Time End 调用console.timeEnd()触发
Timer Fired 定时器激活回调后触发
XHR Ready State Change 当一个异步请求为就绪状态后触发
XHR Load 当一个异步请求完成加载后触发

Rendering事件

事件描述
Invalidate layout 当DOM更改导致页面布局失效时触发
Layout 页面布局计算执行时触发
Recalculate style Chrome重新计算元素样式时触发
Scroll 内嵌的视窗滚动时触发

Painting事件

事件描述
Composite Layers Chrome的渲染引擎完成图片层合并时触发
Image Decode 一个图片资源完成解码后触发
Image Resize 一个图片被修改尺寸后触发
Paint 合并后的层被绘制到对应显示区域后触发