使用 Chome Timeline 来优化页面性能 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
Coding.NET 轻量级社交
开源项目广场
使用帮助
意见反馈
CodingNET

使用 Chome Timeline 来优化页面性能

  •  
  •   CodingNET Aug 10, 2016 4173 views
    This topic created in 3552 days ago, the information mentioned may be changed or developed.

    有时候,我们就是会不由自主地写出一些低效的代码,严重影响页面运行的效率。或者我们接手的项目中,前人写出来的代码千奇百怪,比如为了一个 Canvas 特效需要同时绘制 600 个三角形,又比如 Coding.net 的任务中心需要同时 watch 上万个变量的变化等等。那么,如果我们遇到了一个比较低效的页面,应该如何去优化它呢?

    优化前的准备:知己知彼

    在一切开始之前,我们先打开 F12 面板,熟悉一下我们接下来要用到的工具: Timeline :

    2

    嗯没错就是它。下面逐一介绍一下吧。区域 1 是一个缩略图,可以看到除了时间轴以外被上下分成了四块,分别代表 FPS 、 CPU 时间、网络通信时间、堆栈占用;这个缩略图可以横向缩放,白色区域是下面可以看到的时间段(灰色当然是不可见的啦)。区域 2 可以看一些交互事件,例如你滚动了一下页面,那么这里会出现一个 scroll 的线段,线段覆盖的范围就是滚动经过的时间。区域 3 则是具体的事件列表了。

    一开始没有记录的时候,所有的区域都是空的。开始统计和结束统计都很简单,左上角那坨黑色的圆圈就是。它右边那个长得像“禁止通行”的按钮是用来清除现有记录的。当有数据的时候,我们把鼠标滚轮向上滚,可以看到区域被放大了:

    3

    短短的时间里,浏览器做了这么多事情。对于一般的屏幕,原则上来说一秒要往屏幕上绘制 60 帧,所以理论上讲我们一帧内的计算时间不能超过 16 毫秒,然而浏览器除了执行我们的代码以外,还要干点别的(例如计算 CSS ,播放音频……),所以其实我们能用的只有 10~12 毫秒左右。

    差不多熟悉操作了,那么就来一下实战吧!假如有一天,你接手了这样一段代码:

    <!-- 一段小动画:点击按钮之后会有一个爆炸的粒子效果 --> <!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 调试工具自带的):

    1

    只有 10 FPS …… 10 FPS ……坑爹呢这是!

    4

    好吧,打开 Timeline ,按下记录按钮,点一下页面中的“点我”,稍微过一会儿停止记录,就会得到一些数据。放大一些,对 jQuery 比较熟悉的同学可以看出来,这些大部分是 jQuery 的函数。我们点一下那个 updateCircle 的区块,然后看下面:

    5

    这里告诉我们,这个函数运行了多久、函数代码在哪儿。我们点一下那个链接,于是就跳到了 Source 页:

    6

    是不是很震撼,之前这个页面只是用来 Debug 的,没想到现在居然带了精确到行的运行时间统计。当然,这个时间是当前这一行在“刚才我们点击的区块对应的执行时间段”中运行的时间。所以我们就拿最慢的几句话来下手吧!

    优化一:减少 DOM 操作

    看到这几行代码,第一反应是: mdzz 。本来 DOM 操作就慢,还要在字符串和 float 之间转来转去。果断改掉!于是用一个单独的数组来存 xydv 这些属性。

    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; 

    7

    效果显著!我们再来看一下精确到行的数据:

    8

    优化二:减少不必要的运算

    所以最耗时的那句话已经变成了计算 vxvy,毕竟三角函数算法比较复杂嘛,可以理解。至于后面的三角函数为什么那么快,我猜可能是 Chrome 的 V8 引擎将其缓存了(这句话不保证正确性)。然而不知道大家有没有发现,其实计算 d 完全没必要!我们只需要存 vxvy 即可,不需要存 vd

    // 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; 

    9

    只有加减乘除和开平方运算,每次比原来的时间又少了两毫秒。从流畅的角度来说其实已经可以满帧运行了,然而为什么我还是觉得偶尔会有点卡呢?

    优化三:替换 setInterval

    既然偶尔会掉帧,那么就看看是怎么掉的呗~原则上来说,在每一次浏览器进行绘制之前, Timeline 里面应该有一个叫 Paint 的事件,就像这样:

    10

    看到这些绿色的东西了没?就是它们!看上面的时间轴,虽然代码中 setInterval 的长度是 1000/16 毫秒,但是其实根本不能保证!所以我们需要使用 requestAnimationFrame 来代替它。这是浏览器自带的专门为动画服务的函数,浏览器会自动优化这个函数的调用时机。并且如果页面被隐藏,浏览器还会自动暂停调用,有效地减少了 CPU 的开销。

    / 在 updateCircle 最后加一句 requestAnimationFrame(updateCircle); // 去掉全部跟 setInterval 有关的句子,把 showAnimation 最后一句直接改成这个 updateCircle(); 

    我们至少可以保证,我们每算一次,屏幕上就会显示一次,因此不会掉帧(前提是每计算一次的时间小于 12ms )。但是虽然计算时间少了,浏览器重计算样式、绘制图像的时间可是一点都没变。能不能再做优化呢?

    优化四:使用硬件加速、避免反复查找元素

    如果我们用 transform 来代替 lefttop 来对元素进行定位,那么浏览器会为这个元素单独创立一个合成层,专门使用 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)'; 

    11

    看起来是不是很爽了?

    其实,优化是无止境的,例如我在 init 函数中完全可以不用 jQuery ,改用 createDocumentFragment 来拼接元素,这样初始化的时间就可以急剧缩短;调换 updateCircle 中的几个语句的顺序,在 V8 引擎下效率可能会有一定的提升;甚至还可以结合 Profile 面板来分析内存占用,查看浏览器绘图的细节……然而个人感觉并用不到这么极限的优化。对于一个项目来说,如果单纯为了优化而写一些奇怪的代码,是很不合算的。

    P.S. 全部的代码在这里,欢迎吐槽:

    未优化版 | 优化版

    10 replies    2016-08-11 10:52:12 +08:00
    lneoi
        1
    lneoi  
       Aug 10, 2016
    前排
    Nixus
        2
    Nixus  
       Aug 10, 2016
    前排:我算吗?
    感觉楼主好流弊
    lslqtz
        3
    lslqtz  
       Aug 10, 2016
    chome 一脸蒙蔽
    microchang
        4
    microchang  
       Aug 10, 2016
    优化版第二次点击的时候就没有效果了,不过无伤大雅~

    多谢楼主分享!对 timeline 有了一个比较直观的认识。
    fish267
        5
    fish267  
       Aug 10, 2016 via Android
    谢谢分享
    bugmenein
        6
    bugmenein  
       Aug 10, 2016 via iPhone
    Chrome *
    gimp
        7
    gimp  
       Aug 10, 2016
    感觉不错,先收藏了
    benbenzhangqi
        8
    benbenzhangqi  
       Aug 10, 2016
    好厉害的分析 但是还是有点看不明白
    bzw875
        9
    bzw875  
       Aug 10, 2016
    好厉害,就是三角函数计算运动的那一块看不懂
    suduo1987
        10
    suduo1987  
       Aug 11, 2016
    这个介绍不错啊
    About     Help     Advertise     Blog     API     FAQ     Solana     2378 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 49ms UTC 11:02 PVG 19:02 LAX 04:02 JFK 07:02
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86