有时候,咱们就是会不禁自主地写出一些低效的代码,严重影响页面运行的效率。或者咱们接手的项目中,前人写出来的代码千奇百怪,好比为了一个 Canvas 特效须要同时绘制 600 个三角形,又好比 Coding.net 的任务中心须要同时 watch 上万个变量的变化等等。那么,若是咱们遇到了一个比较低效的页面,应该如何去优化它呢?javascript
在一切开始以前,咱们先打开 F12 面板,熟悉一下咱们接下来要用到的工具:Timeline:css
嗯没错就是它。下面逐一介绍一下吧。区域 1 是一个缩略图,能够看到除了时间轴之外被上下分红了四块,分别表明 FPS、CPU 时间、网络通讯时间、堆栈占用;这个缩略图能够横向缩放,白色区域是下面能够看到的时间段(灰色固然是不可见的啦)。区域 2 能够看一些交互事件,例如你滚动了一下页面,那么这里会出现一个 scroll 的线段,线段覆盖的范围就是滚动通过的时间。区域 3 则是具体的事件列表了。html
一开始没有记录的时候,全部的区域都是空的。开始统计和结束统计都很简单,左上角那坨黑色的圆圈就是。它右边那个长得像“禁止通行”的按钮是用来清除现有记录的。当有数据的时候,咱们把鼠标滚轮向上滚,能够看到区域被放大了:java
短短的时间里,浏览器作了这么多事情。对于通常的屏幕,原则上来讲一秒要往屏幕上绘制 60 帧,因此理论上讲咱们一帧内的计算时间不能超过 16 毫秒,然而浏览器除了执行咱们的代码之外,还要干点别的(例如计算 CSS,播放音频……),因此其实咱们能用的只有 10~12 毫秒左右。jquery
差很少熟悉操做了,那么就来一下实战吧!假若有一天,你接手了这样一段代码:算法
<!-- 一段小动画:点击按钮以后会有一个爆炸的粒子效果 --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Test</title> <style> .main { position: relative; width: 500px; height: 500px; background: #000; overflow: hidden; } .circle { position: absolute; border-radius: 50%; border: 1px solid #FFF; width: 8px; height: 8px; } </style> </head> <body> <div class="main"></div> <hr> <button onclick="showAnimation()">点我</button> <script src="jquery.min.js"></script> <script src="animation.js"></script> </body> </html>
// animation.js // 粒子总数 var COUNT = 500; // 重力 var G = -0.1; // 摩擦力 var F = -0.04; function init() { for (var i = 0; i < COUNT; i++) { var d = Math.random() * 2 * Math.PI; var v = Math.random() * 5; var circle = $('<div id="circle-' + i + '" class="circle" data-x="250" data-y="250" data-d="' + d + '" data-v="' + v + '"></div>'); circle.appendTo($('.main')); } } function updateCircle() { for (var i = 0; i < COUNT; i++) { var x = parseFloat($('#circle-' + i).attr('data-x')); var y = parseFloat($('#circle-' + i).attr('data-y')); var d = parseFloat($('#circle-' + i).attr('data-d')); var v = parseFloat($('#circle-' + i).attr('data-v')); var vx = v * Math.cos(d); var vy = v * Math.sin(d); if (Math.abs(vx) < 1e-9) vx = 0; // 速度份量改变 vx += F * Math.cos(d); vy += F * Math.sin(d) + G; // 计算新速度 v = Math.sqrt(vx * vx + vy * vy); if (vy > 0) d = Math.acos(vx / v); else d = -Math.acos(vx / v); // 位移份量改变 x += vx; y += vy; $('#circle-' + i).attr('data-x', x); $('#circle-' + i).attr('data-y', y); $('#circle-' + i).attr('data-d', d); $('#circle-' + i).attr('data-v', v); $('#circle-' + i).css({'top': 400 - y, 'left': x}); } } var interval = null; function showAnimation() { if (interval) clearInterval(interval); $('.main').html(''); init(); interval = setInterval(updateCircle, 1000 / 60); }
效果以下(右上角的 FPS 计数器是 Chrome 调试工具自带的):数组
只有 10 FPS……10 FPS……坑爹呢这是!浏览器
好吧,打开 Timeline,按下记录按钮,点一下页面中的“点我”,稍微过一下子中止记录,就会获得一些数据。放大一些,对 jQuery 比较熟悉的同窗能够看出来,这些大部分是 jQuery 的函数。咱们点一下那个 updateCircle
的区块,而后看下面:缓存
这里告诉咱们,这个函数运行了多久、函数代码在哪儿。咱们点一下那个连接,因而就跳到了 Source 页:网络
是否是很震撼,以前这个页面只是用来 Debug 的,没想到如今竟然带了精确到行的运行时间统计。固然,这个时间是当前这一行在“刚才咱们点击的区块对应的执行时间段”中运行的时间。因此咱们就拿最慢的几句话来下手吧!
看到这几行代码,第一反应是:mdzz。原本 DOM 操做就慢,还要在字符串和 float 之间转来转去。果断改掉!因而用一个单独的数组来存 x
、y
、d
、v
这些属性。
var objects = []; // 在 init 函数中 objects.push({ x: 250, y: 250, d: d, v: v }); // 在 updateCircle 函数中 var x = objects[i].x; var y = objects[i].y; var d = objects[i].d; var v = objects[i].v; // …. objects[i].x = x; objects[i].y = y; objects[i].d = d; objects[i].v = v;
效果显著!咱们再来看一下精确到行的数据:
因此最耗时的那句话已经变成了计算 vx
和 vy
,毕竟三角函数算法比较复杂嘛,能够理解。至于后面的三角函数为何那么快,我猜多是 Chrome 的 V8 引擎将其缓存了(这句话不保证正确性)。然而不知道你们有没有发现,其实计算 d
彻底不必!咱们只须要存 vx
和 vy
便可,不须要存 v
和 d
!
// init var vx = v * Math.cos(d); var vy = v * Math.sin(d); objects.push({ x: 250, y: 250, vx: vx, vy: vy }); // updateCircle var vx = objects[i].vx; var vy = objects[i].vy; // 计算新速度 var v = Math.sqrt(vx * vx + vy * vy); if (Math.abs(vx) < 1e-9) vx = 0; // 速度份量改变 vx += F * vx / v; vy += F * vy / v + G; // …. objects[i].vx = vx; objects[i].vy = vy;
只有加减乘除和开平方运算,每次比原来的时间又少了两毫秒。从流畅的角度来讲其实已经能够满帧运行了,然而为何我仍是以为偶尔会有点卡呢?
既然偶尔会掉帧,那么就看看是怎么掉的呗~原则上来讲,在每一次浏览器进行绘制以前,Timeline 里面应该有一个叫 Paint 的事件,就像这样:
看到这些绿色的东西了没?就是它们!看上面的时间轴,虽然代码中 setInterval 的长度是 1000/16 毫秒,可是其实根本不能保证!因此咱们须要使用 requestAnimationFrame
来代替它。这是浏览器自带的专门为动画服务的函数,浏览器会自动优化这个函数的调用时机。而且若是页面被隐藏,浏览器还会自动暂停调用,有效地减小了 CPU 的开销。
// 在 updateCircle 最后加一句 requestAnimationFrame(updateCircle); // 去掉所有跟 setInterval 有关的句子,把 showAnimation 最后一句直接改为这个 updateCircle();
咱们至少能够保证,咱们每算一次,屏幕上就会显示一次,所以不会掉帧(前提是每计算一次的时间小于 12ms)。可是虽然计算时间少了,浏览器重计算样式、绘制图像的时间但是一点都没变。能不能再作优化呢?
若是咱们用 transform
来代替 left
和 top
来对元素进行定位,那么浏览器会为这个元素单首创立一个合成层,专门使用 GPU 进行渲染,这样能够把重计算的代价降到最低。有兴趣的同窗能够研究一下“CSS 硬件加速”的机制。同时,咱们能够缓存一下 jQuery 的元素(或者 DOM 元素),这样不用每次都从新查找,也能稍微提升一点效率。若是把元素缓存在 objects
数组中,那么连 id 都不用写了!
// init var circle = $('<div class="circle"></div>'); objects.push({ x: 250, y: 250, vx: vx, vy: vy, // 其实能够只存 DOM,不存 jQuery 对象 circle: circle[0] }); // updateCircle 里面 for 循环的最后一句话替换掉 objects[i].circle.style.transform = 'translate(' + x + 'px, ' + (400 - y) + 'px)';
看起来是否是很爽了?
其实,优化是无止境的,例如我在 init
函数中彻底能够不用 jQuery,改用 createDocumentFragment
来拼接元素,这样初始化的时间就能够急剧缩短;调换 updateCircle
中的几个语句的顺序,在 V8 引擎下效率可能会有必定的提高;甚至还能够结合 Profile 面板来分析内存占用,查看浏览器绘图的细节……然而我的感受并用不到这么极限的优化。对于一个项目来讲,若是单纯为了优化而写一些奇怪的代码,是很不合算的。
—
P.S. 所有的代码在这里,欢迎吐槽: