前端性能优化(四)交易报价密度性能优化(纯javascript高频数据处理性能优化)

背景介绍

报价密度是用来展示各档位报价数量的,需要在浏览器端做合并。比如报价2.9511的数量1.0200,报价2.9520的数量是1.0333,按照两位小数合并,如果是卖盘则合并结果为报价2.9600的数量为2.0533,如果是买盘则合并结果为报价2.9500的数量为2.0533。按照什么精度来合并用户可以自己选择。由于交易特别活跃,推送的非常频繁,需要在浏览器端做大量计算,直接的结果就是CPU非常高。

优化

我之前写了一篇前端加载优化的文章《前端性能优化(一)用一张图说明加载优化》 ,
@i5ting
狼叔看了之后说业务梳理更重要,确实是这样的,本次确实做了很多业务梳理方面的优化,原则当然是能不计算就不计算,能少计算就少计算。比如循环外就可以确定的值不要在循环内做计算。比如报价显示20档,当计算获得的数据足够显示时就不要再计算了。

因为场景特殊,这次在纯技术角度的优化所带来的收益也是非常多的,甚至可能超过了业务梳理。

一般来说浏览器端程序出现性能问题都是和DOM操作扯上关系,纯javascript出现严重性能问题是少见的。我至今也就遇到过两次,第一次是在大智慧做行情显示,推送频繁;第二次就是这次的报价密度,推送频繁。本文会主要说一些纯技术方面的优化点,有一些是本次做了的,有的是还没做的,优化无止境,一步一步来。

1. try catch的性能

先看例子

// === try catch
var start = window.performance.now()
for (var i = 0, b = ''; i < 500000; i++) {
var a = b + i;
}
var end = window.performance.now()
console.log(end - start)

var start = window.performance.now()
for (var i = 0, b = ''; i < 500000; i++) {
try {
var a = b + i;
} catch (e) {

}
}
var end = window.performance.now()
console.log(end - start)

// 输出结果
37.59999999991851
58.79999999998836

从测试结果看try catch确实慢,但是慢的不离谱,慢不了一倍。

我们再来看一个例子。

// === try catch
var start = window.performance.now()
for (var i = 0, b = ''; i < 500000; i++) {
var a;
a = (i.toString().split('.')[1] || '').length;
}
var end = window.performance.now()
console.log(end - start)

var start = window.performance.now()
for (var i = 0, b = ''; i < 500000; i++) {
var a;
try {
a = i.toString().split('.')[1].length;
} catch (e) {
a = 0;
}
}
var end = window.performance.now()
console.log(end - start)

// 输出结果
148.00000000011642
3314.400000000256

吓到了吧,慢了20多倍。所以得出结论:try catch要慎用,如果可以把程序写的健壮就不要用try catch来解决问题。

这也是这次优化的大头,之前程序里的加减乘除每个运算使用两个try来做计算小数位的处理,很多走了catch分支,而每一条数据的处理都涉及到多次加减乘除运算,时间耗费巨大。

2. with的性能

先看代码

// === with
var arr = [];
for (var i = 0; i < 50000; i++) {
arr.push('' + Math.random() * 100000)
}
var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = Math.max(i, arr[i])
}
var end = window.performance.now()
console.log(end - start)

var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
with (Math) {
value = max(i, arr[i])
}
}
var end = window.performance.now()
console.log(end - start)

// 输出结果
13.10000000000582
60.20000000001164

性能差四五倍,with这种东西真的不能用,最佳实践早就否定了with。

3. 计算小数精度

先看代码

// === 计算小数精度
var arr = [];
for (var i = 0; i < 50000; i++) {
arr.push('' + Math.random() * 100000)
}
var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = arr[i]
var index = value.indexOf('.');
var len = 0;
if (index > -1) {
len = value.length - 1 - index;
}
}
var end = window.performance.now()
console.log(end - start)

var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = arr[i]
var index = value.indexOf('.');
var len = 0;
if (index > -1) {
len = value.split('.')[1].length;
}
}
var end = window.performance.now()
console.log(end - start)

// 输出结果
3.5000000009313226
20.00000000046566

从测试结果看split的实现方式慢五六倍,也很好理解,对于这个例子split要多创建出来两个字符串和一个数组。这也是本次优化的点之一,结合前文提到的在计算小数精度的时候用到了try catch,所以本次优化在计算小数精度方面比原来提升了几十到上百倍。

4. 将带小数点的字符串分割为整数和小数

先看代码

// === slice vs split
var arr = [];
for (var i = 0; i < 500000; i++) {
arr.push('' + Math.random() * 100000)
}
var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = arr[i]
var index = value.indexOf('.');
var a = value.slice(0, index)
var b = value.slice(index + 1)
}
var end = window.performance.now()
console.log(end - start)

var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = arr[i]
var array = value.split('.')
var a = array[0]
var a = array[1]
}
var end = window.performance.now()
console.log(end - start)

// 测试结果
47.8000000002794
146.8000000002794

从结果看,slice比split还是有优势的,道理应该是操作的内存空间不一样。这是本次没有做优化的,本次优化主要针对于有几倍性能提升的点。

5. 操作Cookie

// === Cookie
var start = window.performance.now()
var cookie = document.cookie
for (var i = 0, value; i < 5000; i++) {
value = cookie;
}
var end = window.performance.now()
console.log(end - start)

var start = window.performance.now()
for (var i = 0, value; i < 5000; i++) {
value = document.cookie;
}
var end = window.performance.now()
console.log(end - start)

// 输出结果
0.7999999997991836
623.7999999993917

又吓到了吧,读取cookie原来可以这样慢,七八百倍,这是5000次的结果,看样子是读取cookie的时候会和硬盘扯上关系,我是ssd,机械硬盘有可能更慢。这个例子只是测试了读document.cookie,实际操作又涉及到按照;来split,之后还要循环按照=来split,前面也说过split性能并不优秀。

所以读取cookie的正确姿势是:如果不易变的cookie只做一次读取,用变量来做缓存,如果是容易变的做一个更新机制。这也是本次优化的点,之前每次数据更新会读一次cookie,而如前文所说的,数据推送特别频繁。

6. 数字转化为字符串

先说结论,这个应该算做无差异,应该做的是能不转就不转,能少转就少转。

// === toString vs plus
var arr = [];
for (var i = 0; i < 500000; i++) {
arr.push(Math.random() * 100000)
}
var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = arr[i].toString()
}
var end = window.performance.now()
console.log(end - start)

var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = '' + arr[i]
}
var end = window.performance.now()
console.log(end - start)

// 输出结果
144.80000000004657
148.19999999948777

7. 字符串转换为数字,new Numbr vs Number

// === new Number vs Number
var arr = [];
for (var i = 0; i < 500000; i++) {
arr.push('' + Math.random() * 100000)
}
var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = new Number(arr[i]) - new Number('1')
}
var end = window.performance.now()
console.log(end - start)
var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = Number(arr[i]) - Number('1');
}
var end = window.performance.now()
console.log(end - start)

// 输出结果
129
74.40000000060536

是不是发现还是有差异的,最佳实践是有道理的。

8. new Array 和 数组字面量

// === new Array vs []
var arr = [];
for (var i = 0; i < 500000; i++) {
arr.push('' + Math.random() * 100000)
}
var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = [];
}
var end = window.performance.now()
console.log(end - start)
var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = new Array()
}
var end = window.performance.now()
console.log(end - start)

// 输出结果为
16.199999999953434
20.199999999953434

最佳实践告诉我们使用字面量是有道理的吧~

9. 用0补位如何生成0

// === 循环拼接0 VS slice
var arr = [];
for (var i = 0; i < 50000; i++) {
arr.push('' + Math.random() * 100000)
}

var start = window.performance.now()
var zero20 = '000000000000000';
for (var i = 0, value; i < arr.length; i++) {
value = zero20.slice(0, 4)
}
var end = window.performance.now()
console.log(end - start)

var start = window.performance.now()
for (var i = 0, value; i < arr.length; i++) {
value = '';
for (var j = 0 ; j < 4; j++) {
value += '0';
}
}
var end = window.performance.now()
console.log(end - start)

// 输出结果
3.3000000003085006
9.599999999409192

从结果可以看出slice还是有不小的优势的,这也是本次做的。我们知道在显示的的时候需要做一次补0占位的操作,用slice这种更为合适。

10. 向上向下截位

这个对比就不贴出来了,之前的代码太长,下面是新实现的一版向下截位,测试了一下比原来的版本性能好7倍左右。向上截位类似。

var zero20 = '00000000000000000000';
// 向下截位(小数部分向下截位)
function floorTruncation(number, len) {
number = '' + number;
var index = number.indexOf('.');
var precision = index > -1 ? number.length - 1 - index : 0;
if (precision == len) {
// 已有精度与目标精度相同
return number;
} else if (precision == 0 || len == 0) {
// 如果是整数,按照精度进行补零。 如果要求的精度是0则用toFixed直接截断。
return (+number).toFixed(len)
}
// 小数的情况
return (number + zero20).slice(0, index + 1 + len);
}

其他

还有其他的一些点,这篇文章就不一一列举了,以后有机会再写。

结论

其实上面说的这些点每个拿出来单独执行都非常快,微秒级别的。不同实现之间的差异当然也是微秒级别的。但是在执行次数巨大的情况下就凸显出来差异了。

推荐文章