随着网络的普及和浏览器技术的日益进步,Web页面的呈现样式变得越来越丰富多彩,带着各种酷炫效果和富交互的网站层出不穷,Web页面再也不是一个简单信息罗列的文档了。

可是,随着web页面内容的逐渐丰富,浏览器渲染页面的性能瓶颈逐渐显现。所以,当web页面的动画效果从最开始的个别按钮和图标的交互动画逐渐演变为配合Ajax技术实现页面主体内容改变的过场动画,甚至到由复杂JavaScript实现的web在线游戏。做前端开发时对性能的优化和调校也变得越来越重要。

好吧,前面废话了一堆,现在进入正题。

考虑下图所示的一个场景:

页面分为6个部分,除最上方的Banner区域外,其他区域均会显示各种不同类型的动画效果(注:这里的“动画”指广义的动画,即只要呈现内容随时间发生变化就算是“动画”,如,一段文字从左向右缓慢移动算“动画”,一段文字内容由“AAA”变为“BBB”也算“动画”)。

如果这4个动画区域的动画的频度相对较小,也许不会有什么问题,但,如果动画内容频度非常高,就很有可能会引起严重的性能问题。

我们用如下所示的页面来模拟这种情况:

点击start按钮后,这个页面会在2~3秒时间内每一毫秒左右收到一条从server端发来的消息,一共3000条消息,每条消息包含cmd和data两部分内容,cmd表示该条消息在哪个方框内显示,data表示显示什么内容(这个内容是server端自行生成的一个0到1000的随机数)。

客户端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
<link href="css/reset.css" rel="stylesheet" type="text/css" />
<script src="js/jquery.js" type="text/javascript"></script>
<script src="js/json2.js" type="text/javascript"></script>
<script src="js/config.js" type="text/javascript"></script>
<script src="js/SocketMessage.js" type="text/javascript"></script>
<script src="js/WebSocketClient.js" type="text/javascript"></script>
<style type="text/css">
#receivers div{float:left;height:300px;width:80px;border:solid 1px black;display:inline-block;overflow:auto;margin-left:5px}
</style>
</head>
<body>
<input type="button" onclick="start()" value="start"/>
<input type="button" onclick="showResult()" value="show result"/>
<span id="resultContainer">frame count:<span id="fc"></span>  
time cost:<span id="tc"></span>  
fps:<span id="fps"></span></span><br />
<div id="receivers">
</div>
<script type="text/javascript">


var frameCount = 0; //统计从动画开始到结束,页面的刷新次数
var started = false; //标记动画过程是否已经开始
var rs = []; //消息接收模块集合,用来接收server发来的消息并显示在页面上
var t1, t2; //记录动画开始和结束的时间
var useUFC = true; //标记是否使用统一帧管理方法

function countFrame() {
if (!started) return;
frameCount++;
webkitRequestAnimationFrame(countFrame);
}

function refresh() { //刷新页面内容并计数
if (!started) return;
if (useUFC) { //如果用统一帧管理方法就绘制每一个消息接收类的实例
for (var i in rs) {
if (rs[i].needPaint) {
rs[i].draw();
rs[i].needPaint = false;
}
}
}
setTimeout(refresh, 16);
}

//创建到服务器的链接,用以接收消息
var ws = CreateWebSocketConnection(systemconfig.wsLication);

function start() {
//发送消息到服务器通知服务器开始发消息
ws.send(JSON.stringify(new SocketMessage('', 'UFCT', { cmd: 'start' })));
frameCount = 0;
t1 = new Date();
started = true;
countFrame();
refresh();
}

//显示统计结果,包括页面刷新的次数和总共所用的时间
function showResult() {
fc.innerHTML = frameCount;
tc.innerHTML = t2 - t1;
fps.innerHTML = frameCount / (t2 - t1) * 1000;
}

//消息接收模块类
function Receiver(id) {
this.id = id;
this.msgContent = '';
this.needPaint = false;
var that = this;
$('#receivers').append('<div id="r' + this.id + '"></div>');
this.onmessage = function (msg) {
msg = eval('(' + msg.data + ')');
if (msg.Type != 'UFCT' || msg.Data.cmd != that.id) return;
that.msgContent += msg.Data.data + '<br/>';
//如果用统一帧管理方法就标记自己需要被重绘,否则立即绘制
if (useUFC) {
that.needPaint = true;
}
else{
that.draw()
}
}
this.draw = function () {
var c = document.getElementById('r' + that.id);
c.innerHTML = that.msgContent;
c.scrollTop = c.scrollHeight;
}
}

//生成10个消息接收类的实例
for (var i = 0; i < 10; i++) {
var r = new Receiver(i);
ws.addEventListener('message', r.onmessage);
rs.push(r);
}

//收到结束消息时,停止相应活动并显示结果
ws.addEventListener('message', function (msg) {
msg = eval('(' + msg.data + ')');
if (msg.Type != 'UFCT' || msg.Data != 'end') return;
started = false;
t2 = new Date();
showResult();
});
</script>
</body>
</html>

当变量useUFC标记为true时,页面使用统一帧管理方法来管理页面的刷新渲染过程,当标记为false时,则使用传统方式刷新页面,即页面收到消息后立即用新消息更新页面。

我们先看看两种情况下的测试结果:

useUFC = false

No. Frame count Time cost(ms) FPS
1 13 14371 0.904599540
2 14 14668 0.954393619
3 15 14288 1.049832026
4 14 14212 0.985083028
5 16 14437 1.108263489

useUFC = true

No. Frame count Time cost(ms) FPS
1 48 2420 19.834710743
2 47 2451 19.175846593
3 49 2318 21.138912855
4 47 2315 20.302375809
5 49 2410 20.331950207

由上面的两张表大家可以看出,使用了统一帧管理方法后,页面的性能有质的飞跃。

为什么会有如此显著的效果呢?这就要说到浏览器的工作原理了。

如下图所示,浏览器从HTML文档下载完成到将页面内容显示到浏览器界面上,大概要经历下面4个步骤。

然后,当页面的内容因为一些原因,比如执行js脚本,而发生改变时,就可能导致上面4个步骤中的一个或多个重新执行。

比如在我们前面所设计的测试案例中,当useUFC变量标记为false时,浏览器每次收到从Server端发来的消息,js脚本都会将消息的内容作为一个新的文本节点添加到页面中去。这样做,会立刻导致HTML文档的部分DOM树重构,部分渲染树重构,部分渲染树重排,已经部分渲染树重绘。也就是说,每当浏览器收到一条消息,都会执行上面的4个步骤。这样一来,处理每条消息时,浏览器画在更新渲染页面上的时间大大超出了js脚本运行所需的时间。于是我们看到的结果就是页面变得非常卡,用户体验极差。

而当我们将useUFC变量标记为true时,浏览器每次收到Server端发来的消息,都只是将消息内容写入到一个变量中记录下来,并不会立即去请求更新渲染页面,这样浏览器就省下了大量的计算资源。

那么如果我们没有在收到消息时更新页面,那页面是什么时候更新的呢?关键就在这一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function refresh() {
//刷新页面内容并计数
if (!started) return;
if (useUFC) {
//如果用统一帧管理方法就绘制每一个消息接收类的实例
for (var i in rs) {
if (rs[i].needPaint) {
rs[i].draw();
rs[i].needPaint = false;
}
}
}
setTimeout(refresh, 16);
}

refresh这个函数会通过setTimeout递归调用自己,并且是每隔16ms调用一次,大约一秒60次。每次被调用的时候,该函数都会挨个检查页面上的动画元素是否需要更新页面,如果需要就一次将所有需要更新的内容全部更新。这样一来,浏览器每秒钟进行更新渲染页面的次数大大降低,反而提供了更好的用户体验。

那么,是不是在任何时候我们都需要使用类似这样的统一帧管理方法呢?其实也不尽然。

从上面的分析可以看出,这种统一帧管理方法之所以适用于我们前面所用到的测试案例,是因为在这个场景中,浏览器如果不进行统一帧管理,其更新页面的速度就会超过了浏览器的承受范围(收到每条消息都更新页面,大约每秒更新一千多次)。如果上面的案例中Server发送消息的频率很低,比如每秒20次,那么使用如此复杂的方法显然是多余的,浏览器自能应付自如。

如有不妥之处敬请不吝赐教。