2.3 Web运行时性能
正如我们已经讨论过的,Web性能跟踪的是内容传递到用户的耗时。现在来看看Web运行时性能,它跟踪的是用户与应用交互时应用的行为。
对于传统的编译类型应用,运行时性能是有关内存管理、垃圾回收以及线程等各个方面的。这是因为编译类的应用运行在内核之上,直接使用系统资源。
在客户端运行Web应用与运行编译类应用是大不相同的。这是因为Web应用运行在沙盒中,或者,说得更具体点,是运行在浏览器中。当它们运行的时候,用的是浏览器的资源。而浏览器是运行在它事先从内核中分配的内存资源中的。所以,当我们提到Web运行时性能,我们实际上说的是应用是怎样在客户端的浏览器运行,以及让浏览器在虚拟内存中其自身的内存里执行。图2-12是Web应用在常驻内存中的浏览器内存里运行的情况。
图2-12 一个Web应用运行在常驻内存中的浏览器预分配内存中
下面是我们需要考虑到的影响Web运行时性能的因素。
内存管理与垃圾回收
首先要看的是,我们有没有因为太多无用的对象以及创建更多对象时却仍保留这些无用对象而导致浏览器的内存分配被阻塞。随着时间推移,我们是否有什么机制限制JavaScript中的对象创建,或应用用的越多越久时,内存消耗是否也越多?是否存在内存泄露?
回收无用对象可能会导致浏览器在渲染或播放动画的时候暂停,容易在用户体验上出现锯齿现象。我们可以通过减少创建的对象数量以及尽可能重用已有对象来将垃圾回收次数降到最少。
布局
我们更新DOM的时候是否引发了页面重绘?这一般是由于大范围的样式变化,需要渲染引擎重新计算页面元素的大小与位置。
高代价的绘制
当用户滚动页面时,我们有没有因为绘制一些区域而加重浏览器的负担?动画效果或是更新除了位置、缩放、旋转或透明度之外的任意元素属性,都将引起渲染引擎重绘对应元素并消耗时间。位置、缩放、旋转以及透明度是渲染引擎最后配置的元素属性,所以,更新这些属性只需极小的开销。
如果我们在宽度、高度、背景或者其他属性上使用动画,渲染引擎就需要重新考虑页面的布局并且重绘那个元素,这就会在渲染和动画过程中消耗更多的时间。更糟的是,如果我们引起了父元素的重绘,渲染引擎就需要重绘所有的子元素,严重影响运行时性能。
同步调用
我们会在等待同步调用返回的时候阻塞用户的动作吗?当在操作复选框或其他方式接受输入后更新服务端的状态,再等待确认对应的更新操作已完成时,就会经常发生这样的事情。这会让页面感觉起来有些卡顿。
CPU占用率
浏览器渲染页面和执行客户端代码需要多大负载?
我们要查看的Web运行时性能指标是每秒的帧数和CPU的占用率。
每秒帧数
每秒帧数(FPS)是动画师、游戏开发者以及电影摄影师常用的一种度量单位。它是系统重绘屏幕的速率。按照Paul Bakaus的博客帖子“The Illusion of Motion”(http://bit.ly/1ou97Zn)中的说法,人类感觉动作平滑、逼真的理想帧率是60 FPS。
也有一个Web应用,叫每秒帧数 (http://frames-per-second.appspot.com),在浏览器中以不同的帧率演示动画效果。看这个演示,感受下自己的眼睛对同一个动画在不同帧率下的反应,很有意思。
FPS还是浏览器的一个重要性能指标,因为其反映出了动画运行以及窗口滚动的平滑程度。滚动时出现锯齿(卡顿)已经是Web性能问题的一个明显标志。
在Google Chrome中监控FPS
Google在创建浏览器内置工具追踪运行时性能方面在当前已是领头羊。其内置开发工具中已经包含了追踪FPS的能力。点击Rendering选项卡,然后选中“Show FPS meter”复选框,就可以看到(见图2-13)。
图2-13 在Chrome开发者工具中启用FPS meter
在浏览器的右上方会出现一个小的时间数列图,显示了当前FPS以及每秒帧数的趋势,如图2-14所示。使用这个工具,可以显式地追踪你的页面在实际使用过程中的执行情况。
图2-14 Chrome的FPS meter,位于Web页面的右上角
虽然FPS meter是很好的追踪每秒帧数的工具,但迄今为止,对在帧率方面体验下降进行调试的最有用的工具还是Timeline工具,这个也是Chrome开发者工具中的一员(见图2-15)。
图2-15 Frames模式下的Chrome Timeline工具
用Timeline工具,可以追踪以及分析浏览器运行的时候都干了些什么。它提供了3个操作模式:Frames、Events以及Memory。我们来看一下Frames模式。
Frames模式
这个模式下,Timeline工具展示了Web应用的渲染性能。图2-15是Frames模式的屏幕布局。
在Timeline工具中可以看到两个不同的窗格。顶部窗格展示的是活动模式(位于左手边),里面包含了一系列代表帧的竖条。底部窗格是Frames视图,这里展现的是形似瀑布的水平条状,标示了某个给定动作在帧里耗费的时长。在左边有对应动作的描述。在Frames视图的最右边是一个饼图,展示了在给定帧中最耗时动作的分类。所包含的动作如下所示。
Layout
Paint Setup
Paint
Recalculate Style
Timer Fired
Composite Layers
图2-15展现出运行JavaScript耗去了将近一半的时间,1.02秒中占了525毫秒。
使用Timeline工具,在Frames模式下,通过在Frames视图下找到最长的条,就能轻易地确定对帧率影响最大的动作。
内存分析
内存分析是监控我们应用所用到的内存消耗模式的一种方法。这对检测内存泄露与不会销毁的对象创建非常有用——JavaScript中,当我们用程序为DOM对象指定事件处理器,而后又忘记将事件处理器移除时尤为常见。更进一步,内存分析对优化内存占用也甚为有用。对象的创建、销毁与重用应当是智能的,要时刻注意,不要让剖析图中不断增加的一系列峰值呈上升趋势。图2-16描绘的是JavaScript堆。
虽然浏览器内置的功能远比之前强大,但这仍是一个需要扩大和规范化的领域。迄今为止,Google已经做了很多,让开发者可以用上浏览器内置的内存管理工具。
MemoryInfo对象
在Chrome已有的内存管理工具中,首先我们要看的是MemoryInfo对象,它存在于Performance对象中。图2-17的截图中展示了一个控制台视图。
可以像这样访问MemoryInfo对象。
performance.memory
MemoryInfo {jsHeapSizeLimit: 793000000, usedJSHeapSize:
37300000, totalJSHeapSize: 56800000}
表2-2展示出了与MemoryInfo相关的堆属性。
这些属性指出了可用和已用的JavaScript堆。堆是解释器保存在驻留内存中的JavaScript对象集合。在堆中,每个对象都是互有关联的节点,它们是通过诸如原型链或组合对象等属性连接起来的。浏览器中运行的JavaScript是通过对象引用来使用堆中对象的。当要销毁JavaScript中的一个对象,实际要做的就是销毁那个对象的引用。当解释器发现堆中对象不再有对象引用时,垃圾收集器将会从堆中移除这些对象。
用MemoryInfo对象,我们可以获取用户群与内存消耗相关的RUM数据,也可以在实验室里追踪这些指标,好在代码产品化之前发现潜在的内存问题。
Timeline工具
除了提供Frames模式来调试Web应用帧率之外,Chrome的Timeline工具还有Memory模式(如图2-18所示),能可视化地观察随时间变化的应用内存使用情况,并且会显示文档、DOM节点以及留在内存中的事件监听器的数量。
图2-18 Memory模式下的Chrome Timeline工具
顶部窗口展示的是内存剖析图,最底部的窗口展示了文档、节点以及监听器的数量。注意看,蓝色阴影区显示了内存使用率,可视化地展示了堆内存使用量。随着更多对象被创建,内存使用率也一直上升;当这些对象被销毁并被垃圾收集掉后,内存使用率就下降了。
可以在Mozilla开发网http://mzl.la/1r1RzOG找到一篇有关内存管理的很好的文章。
Firefox也开始开放内存使用数据,可以通过“about:memory”页面看到,Firefox的实现更多的还是通过静态信息页的方式而不是暴露一组API。正因为如此,它无法容易地插入程序中生成经验数据,about:memory页面更像是为Firefox用户(尽管是高级用户)设计的,而不是作为运行时性能管理的开发者工具集的一部分。
要在Firefox中访问“about:memory”页面,在浏览器的地址栏里键入about:memory。图2-19展示了这个页面的样子。
如图2-19所示,可以看到以操作系统级别展示的浏览器分配的内存,以及浏览器打开的每个页面的堆分配情况。