V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Coding.NET 轻量级社交
开源项目广场
使用帮助
意见反馈
CodingNET
V2EX  ›  Coding

使用 Chome Timeline 来优化页面性能

  •  
  •   CodingNET · 2016-08-10 11:31:08 +08:00 · 3657 次点击
    这是一个创建于 3026 天前的主题,其中的信息可能已经有所发展或是发生改变。

    有时候,我们就是会不由自主地写出一些低效的代码,严重影响页面运行的效率。或者我们接手的项目中,前人写出来的代码千奇百怪,比如为了一个 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 条回复    2016-08-11 10:52:12 +08:00
    lneoi
        1
    lneoi  
       2016-08-10 11:52:25 +08:00
    前排
    Nixus
        2
    Nixus  
       2016-08-10 11:56:01 +08:00
    前排:我算吗?
    感觉楼主好流弊
    lslqtz
        3
    lslqtz  
       2016-08-10 11:56:16 +08:00
    chome 一脸蒙蔽
    microchang
        4
    microchang  
       2016-08-10 12:00:19 +08:00
    优化版第二次点击的时候就没有效果了,不过无伤大雅~

    多谢楼主分享!对 timeline 有了一个比较直观的认识。
    fish267
        5
    fish267  
       2016-08-10 12:13:56 +08:00 via Android
    谢谢分享
    bugmenein
        6
    bugmenein  
       2016-08-10 12:20:55 +08:00 via iPhone
    Chrome *
    gimp
        7
    gimp  
       2016-08-10 12:22:42 +08:00
    感觉不错,先收藏了
    benbenzhangqi
        8
    benbenzhangqi  
       2016-08-10 12:39:12 +08:00
    好厉害的分析 但是还是有点看不明白
    bzw875
        9
    bzw875  
       2016-08-10 14:43:45 +08:00
    好厉害,就是三角函数计算运动的那一块看不懂
    suduo1987
        10
    suduo1987  
       2016-08-11 10:52:12 +08:00
    这个介绍不错啊
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   924 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 20:30 · PVG 04:30 · LAX 12:30 · JFK 15:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.