前言
先交代一下我的近况,已经入职okcoin了,刚入职,感谢前阿里巴巴技术专家铭承(我老板)。走的匆忙,很多阿里的同学都没来得及打个招呼,好在我现在还会翻看钉钉,可以钉钉我。又到了晋升季,祝大家步步高升。下面进入本文主题,这篇文章应该叫《纯javascript高频数据处理性能优化》。
背景介绍
报价密度是用来展示各档位报价数量的,需要在浏览器端做合并。比如报价2.9511的数量1.0200,报价2.9520的数量是1.0333,按照两位小数合并,如果是卖盘则合并结果为报价2.9600的数量为2.0533,如果是买盘则合并结果为报价2.9500的数量为2.0533。按照什么精度来合并用户可以自己选择。由于交易特别活跃,推送的非常频繁,需要在浏览器端做大量计算,直接的结果就是CPU非常高。
优化
我之前写了一篇前端加载优化的文章,链接为 小姜哥:前端性能优化(一)用一张图说明加载优化 ,
狼叔看了之后说业务梳理更重要,确实是这样的,本次确实做了很多业务梳理方面的优化,原则当然是能不计算就不计算,能少计算就少计算。比如循环外就可以确定的值不要在循环内做计算。比如报价显示20档,当计算获得的数据足够显示时就不要再计算了。
因为场景特殊,这次在纯技术角度的优化所带来的收益也是非常多的,甚至可能超过了业务梳理。
一般来说浏览器端程序出现性能问题都是和DOM操作扯上关系,纯javascript出现严重性能问题是少见的。我至今也就遇到过两次,第一次是在大智慧做行情显示,推送频繁;第二次就是这次在okcoin的报价密度,推送频繁。本文会主要说一些纯技术方面的优化点,有一些是本次做了的,有的是还没做的,优化无止境,一步一步来。
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);
}
其他
还有其他的一些点,这篇文章就不一一列举了,以后有机会再写。
结论
其实上面说的这些点每个拿出来单独执行都非常快,微秒级别的。不同实现之间的差异当然也是微秒级别的。但是在执行次数巨大的情况下就凸显出来差异了。
划重点
前段时间是相对动荡的,有很多人帮助了我,借着这边文章表达一下对他们的感谢。
感谢我的老板铭承。
感谢okcoin的美女HR小姐姐们。
感谢前老板倪欧。
感谢狼叔给我很多新观点。
感谢前公司的前老板们和小伙伴们。
感谢新浪微博的冰哥和吴大师。
感谢美团的斌哥和松哥让我有进美团的机会, 也感谢美团的美女HR小姐姐,美团真的很棒,虽然我没去。
感谢彭老大。
感谢我唐哥
感谢我的好兄弟们和好朋友们。
感谢热情的帮我推荐工作机会的小姐姐们,虽然我基本都没去看。
第二波重点
如果你正在寻找工作我可以帮忙推荐,直接推荐到用人部门的leader手里。可以推荐的公司包括:okcoin、新浪微博、美团、阿里大文娱、猎豹、快手、滴滴、头条等。 请把简历发送到我的邮箱 linus.wang.i77@gmail.com
,请注明自己的基本信息以及想去的公司。也可加我微信lw20170313 ,工作忙,不保证一定回复,不要发简历到微信,见谅。
结束
最后祝大家工作顺利,写出高性能代码,少写bug。
本文有任何疏漏欢迎指正。
有哪些可优化点欢迎指点一下,如果能来okcoin我们一起来做事是极好的,欢迎发简历过来。