现在的位置: 首页 > 综合 > 正文

转载—-【译】编写高性能JavaScript

2012年04月19日 ⁄ 综合 ⁄ 共 9765字 ⁄ 字号 评论关闭
文章目录

转载原文:【译】编写高性能JavaScript

========================转载文分割线===========================

原文:http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/

作者:

译者按:本人第一次翻译外文,言语难免有些晦涩,但尽量表达了作者的原意,未经过多的润色,欢迎批评指正。另本文篇幅较长、信息量大,可能难以消化,欢迎留言探讨细节问题。本文主要关注V8的性能优化,部分内容并不适用于所有JS引擎。最后,转载请注明出处: )

========================译文分割线===========================

很多JavaScript引擎,如Google的V8引擎(被Chrome和Node所用),是专门为需要快速执行的大型JavaScript应用所设计的。如果你是一个开发者,并且关心内存使用情况与页面性能,你应该了解用户浏览器中的JavaScript引擎是如何运作的。无论是V8,SpiderMonkey的(Firefox)的Carakan(Opera),Chakra(IE)或其他引擎,这样做可以帮助你更好地优化你的应用程序。这并不是说应该专门为某一浏览器或引擎做优化,千万别这么做。

但是,你应该问自己几个问题:

  • 在我的代码里,是否可以使代码更高效一些
  • 主流的JavaScript引擎都做了哪些优化
  • 什么是引擎无法优化的,垃圾回收器(GC)是否能回收我所期望的东西

Dashboard Speedometer

加载快速的网站就像是一辆快速的跑车,需要用到特别定制的零件. 图片来源: dHybridcars.

编写高性能代码时有一些常见的陷阱,在这篇文章中,我们将展示一些经过验证的、更好的编写代码方式。

那么,JavaScript在V8里是如何工作的?

如果你对JS引擎没有较深的了解,开发一个大型Web应用也没啥问题,就好比会开车的人也只是看过引擎盖而没有看过车盖内的引擎一样。鉴于Chrome是我的浏览器首选,所以谈一下它的JavaScript引擎。V8是由以下几个核心部分组成:

  • 一个基本的编译器,它会在代码执行前解析JavaScript代码并生成本地机器码,而不是执行字节码或简单地解释它。这些代码最开始并不是高度优化的。
  • V8将对象构建为对象模型。在JavaScript中对象表现为关联数组,但是在V8中对象被看作是隐藏的类,一个为了优化查询的内部类型系统。
  • 运行时分析器监视正在运行的系统,并标识了“hot”的函数(例如花费很长时间运行的代码)。
  • 优化编译器重新编译和优化那些被运行时分析器标识为“hot”的代码,并进行“内联”等优化(例如用被调用者的主体替换函数调用的位置)。
  • V8支持去优化,这意味着优化编译器如果发现对于代码优化的假设过于乐观,它会舍弃优化过的代码。
  • V8有个垃圾收集器,了解它是如何工作的和优化JavaScript一样重要。

垃圾回收

垃圾回收是内存管理的一种形式,其实就是一个收集器的概念,尝试回收不再被使用的对象所占用的内存。在JavaScript这种垃圾回收语言中,应用程序中仍在被引用的对象不会被清除。

手动消除对象引用在大多数情况下是没有必要的。通过简单地把变量放在需要它们的地方(理想情况下,尽可能是局部作用域,即它们被使用的函数里而不是函数外层),一切将运作地很好。

Garbage collection attempts to reclaim memory.

垃圾回收器尝试回收内存. 图片来源: Valtteri Mäki.

在JavaScript中,是不可能强制进行垃圾回收的。你不应该这么做,因为垃圾收集过程是由运行时控制的,它知道什么是最好的清理时机。

“消除引用”的误解

网上有许多关于JavaScript内存回收的讨论都谈到delete这个关键字,虽然它可以被用来删除对象(map)中的属性(key),但有部分开发者认为它可以用来强制“消除引用”。建议尽可能避免使用delete,在下面的例子中delete o.x 的弊大于利,因为它改变了o的隐藏类,并使它成为一个"慢对象"。

JavaScript
1
2
3
var o = { x: 1 };
delete o.x; // true
o.x; // undefined

你会很容易地在流行的JS库中找到引用删除——这是具有语言目的性的。这里需要注意的是避免在运行时修改”hot”对象的结构。JavaScript引擎可以检测出这种“hot”的对象,并尝试对其进行优化。如果对象在生命周期中其结构没有较大的改变,引擎将会更容易优化对象,而delete操作实际上会触发这种较大的结构改变,因此不利于引擎的优化。

对于null是如何工作也是有误解的。将一个对象引用设置为null,并没有使对象变“空”,只是将它的引用设置为空而已。使用o.x= null比使用delete会更好些,但可能也不是很必要。

JavaScript
1
2
3
4
var o = { x: 1 };
o = null;
o; // null
o.x // TypeError

如果此引用是当前对象的最后引用,那么该对象将被作为垃圾回收。如果此引用不是当前对象的最后引用,则该对象是可访问的且不会被垃圾回收。

另外需要注意的是,全局变量在页面的生命周期里是不被垃圾回收器清理的。无论页面打开多久,JavaScript运行时全局对象作用域中的变量会一直存在。

JavaScript
1
var myGlobalNamespace = {};

全局对象只会在刷新页面、导航到其他页面、关闭标签页或退出浏览器时才会被清理。函数作用域的变量将在超出作用域时被清理,即退出函数时,已经没有任何引用,这样的变量就被清理了。

经验法则

为了使垃圾回收器尽早收集尽可能多的对象,不要hold着不再使用的对象。这里有几件事需要记住:

  • 正如前面提到的,在合适的范围内使用变量是手动消除引用的更好选择。即一个变量只在一个函数作用域中使用,就不要在全局作用域声明它。这意味着更干净省心的代码。
  • 确保解绑那些不再需要的事件监听器,尤其是那些即将被销毁的DOM对象所绑定的事件监听器。
  • 如果使用的数据缓存在本地,确保清理一下缓存或使用老化机制,以避免大量不被重用的数据被存储。

函数

接下来,我们谈谈函数。正如我们已经说过,垃圾收集的工作原理,是通过回收不再是访问的内存块(对象)。为了更好地说明这一点,这里有一些例子。

JavaScript
1
2
3
4
function foo() {
    var bar = new LargeObject();
    bar.someCall();
}

当foo返回时,bar指向的对象将会被垃圾收集器自动回收,因为它已没有任何存在的引用了。

对比一下:

JavaScript
1
2
3
4
5
6
7
8
function foo() {
    var bar = new LargeObject();
    bar.someCall();
    return bar;
}
 
// somewhere else
var b = foo();

现在我们有一个引用指向bar对象,这样bar对象的生存周期就从foo的调用一直持续到调用者指定别的变量b(或b超出范围)。

闭包(CLOSURES)

当你看到一个函数,返回一个内部函数,该内部函数将获得范围外的访问权,即使在外部函数执行之后。这是一个基本的闭包 —— 可以在特定的上下文中设置的变量的表达式。例如:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
function sum (x) {
    function sumIt(y) {
        return x + y;
    };
    return sumIt;
}
 
// Usage
var sumA = sum(4);
var sumB = sumA(3);
console.log(sumB); // Returns 7

在sum调用上下文中生成的函数对象(sumIt)是无法被回收的,它被全局变量(sumA)所引用,并且可以通过sumA(n)调用。

让我们来看看另外一个例子,这里我们可以访问变量largeStr吗?

JavaScript
1
2
3
4
5
6
var a = function () {
    var largeStr = new Array(1000000).join('x');
    return function () {
        return largeStr;
    };
}();

是的,我们可以通过a()访问largeStr,所以它没有被回收。下面这个呢?

JavaScript
1
2
3
4
5
6
7
var a = function () {
    var smallStr = 'x';
    var largeStr = new Array(1000000).join('x');
    return function (n) {
        return smallStr;
    };
}();

我们不能再访问largeStr了,它已经是垃圾回收候选人了。【译者注:因为largeStr已不存在外部引用了】

定时器

最糟的内存泄漏地方之一是在循环中,或者在setTimeout()/ setInterval()中,但这是相当常见的。思考下面的例子:

JavaScript
1
2
3
4
5
6
7
8
9
var myObj = {
    callMeMaybe: function () {
        var myRef = this;
        var val = setTimeout(function () {
            console.log('Time is running out!');
            myRef.callMeMaybe();
        }, 1000);
    }
};

如果我们运行myObj.callMeMaybe();来启动定时器,可以看到控制台每秒打印出“Time is running out!”。如果接着运行myObj = null,定时器依旧处于激活状态。为了能够持续执行,闭包将myObj传递给setTimeout,这样myObj是无法被回收的。相反,它引用到myObj的因为它捕获了myRef。这跟我们为了保持引用将闭包传给其他的函数是一样的。

同样值得牢记的是,setTimeout/setInterval调用(如函数)中的引用,将需要执行和完成,才可以被垃圾收集。

当心性能陷阱

永远不要优化代码,直到你真正需要。现在经常可以看到一些基准测试,显示N比M在V8中更为优化,但是在模块代码或应用中测试一下会发现,这些优化真正的效果比你期望的要小的多。

Speed trap.

做的过多还不如什么都不做. 图片来源: Tim Sheerman-Chase.

比如我们想要创建这样一个模块:

  • 需要一个本地的数据源包含数字ID
  • 绘制包含这些数据的表格
  • 添加事件处理程序,当用户点击的任何单元格时切换单元格的css class

这个问题有几个不同的因素,虽然也很容易解决。我们如何存储数据,如何高效地绘制表格并且append到DOM中,如何更优地处理表格事件?

面对这些问题最开始(天真)的做法是使用对象存储数据并放入数组中,使用jQuery遍历数据绘制表格并append到DOM中,最后使用事件绑定我们期望地点击行为。

注意:这不是你应该做的

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var moduleA = function () {
 
    return {
 
        data: dataArrayObject,
 
        init: function () {
            this.addTable();
            this.addEvents();
        },
 
        addTable: function () {
 
            for (var i = 0; i < rows; i++) {
                $tr = $('<tr></tr>');
                for (var j = 0; j < this.data.length; j++) {
                    $tr.append('<td>' + this.data[j]['id'] + '</td>');
                }
                $tr.appendTo($tbody);
            }
 
        },
        addEvents: function () {
            $('table td').on('click', function () {
                $(this).toggleClass('active');
            });
        }
 
    };
}();

这段代码简单有效地完成了任务。

但在这种情况下,我们遍历的数据只是本应该简单地存放在数组中的数字型属性ID。有趣的是,直接使用DocumentFragment和本地DOM方法比使用jQuery(以这种方式)来生成表格是更优的选择,当然,事件代理比单独绑定每个td具有更高的性能。

要注意虽然jQuery在内部使用DocumentFragment,但是在我们的例子中,代码在循环内调用append并且这些调用涉及到一些其他的小知识,因此在这里起到的优化作用不大。希望这不会是一个痛点,但请务必进行基准测试,以确保自己代码ok。

对于我们的例子,上述的做法带来了(期望的)性能提升。事件代理对简单的绑定是一种改进,可选的DocumentFragment也起到了助推作用。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var moduleD = function () {
 
    return {
 
        data: dataArray,
 
        init: function () {
            this.addTable();
            this.addEvents();
        },
        addTable: function () {
            var td, tr;
            var frag = document.createDocumentFragment();
            var frag2 = document.createDocumentFragment();
 
            for (var i = 0; i < rows; i++) {
                tr = document.createElement('tr');
                for (var j = 0; j < this.data.length; j++) {
                    td = document.createElement('td');
                    td.appendChild(document.createTextNode(this.data[j]));
 
                    frag2.appendChild(td);
                }
                tr.appendChild(frag2);
                frag.appendChild(tr);
            }
            tbody.appendChild(frag);
        },
        addEvents: function () {
            $('table').on('click', 'td', function () {
                $(this).toggleClass('active');
            });
        }
 
    };
 
}();

接下来看看其他提升性能的方式。你也许曾经在哪读到过使用原型模式比模块模式更优,或听说过使用JS模版框架性能更好。有时的确如此,不过使用它们其实是为了代码更具可读性。对了,还有预编译!让我们看看在实践中表现的如何?

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
moduleG = function () {};
 
moduleG.prototype.data = dataArray;
moduleG.prototype.init = function () {
    this.addTable();
    this.addEvents();
};
moduleG.prototype.addTable = function () {
    var template = _.template($('#template').text());
    var html = template({'data' : this.data});
    $tbody.append(html);
};
moduleG.prototype.addEvents = function () {
   $('table').on('click', 'td', function () {
       $(this).toggleClass('active');
   });
};
 
var modG = new moduleG();

事实证明,在这种情况下的带来的性能提升可以忽略不计。模板和原型的选择并没有真正提供更多的东西。也就是说,性能并不是开发者使用它们的原因,给代码带来的可读性、继承模型和可维护性才是真正的原因。

更复杂的问题包括高效地在canvas上绘制图片和操作带或不带类型数组的像素数据。

在将一些方法用在你自己的应用之前,一定要多了解这些方案的基准测试。也许有人还记得JS模版的shoot-off随后的扩展版。你要搞清楚基准测试不是存在于你看不到的那些虚拟应用,而是应该在你的实际代码中去测试带来的优化。

V8优化技巧

详细介绍了每个V8引擎的优化点在本文讨论范围之外,当然这里也有许多值得一提的技巧。记住这些技巧你就能减少那些性能低下的代码了。

  • 特定模式可以使V8摆脱优化的困境,比如说try-catch。欲了解更多有关哪些函数能或不能进行优化,你可以在V8的脚本工具d8中使用–trace-opt file.js命令。
  • 如果你关心速度,尽量使你的函数职责单一,即确保变量(包括属性,数组,函数参数)只使用相同隐藏类包含的对象。举个例子,别这么干:
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    function add(x, y) {
       return x+y;
    }
     
    add(1, 2);
    add('a','b');
    add(my_custom_object, undefined);
  • 不要加载未初始化或已删除的元素。如果这么做也不会出现什么错误,但是这样会使速度变慢。
  • 不要使函数体过大,这样会使得优化更加困难。

更多内容可以去看Daniel Clifford在Google I/O的分享 Breaking the JavaScript Speed Limit with V8。 Optimizing For V8 — A Series也非常值得一读。

对象VS数组:我应该用哪个?

  • 如果你想存储一串数字,或者一些相同类型的对象,使用一个数组。
  • 如果你语义上需要的是一堆的对象的属性(不同类型的),使用一个对象和属性。这在内存方面非常高效,速度也相当快。
  • 整数索引的元素,无论存储在一个数组或对象中,都要比遍历对象的属性快得多
  • 对象的属性比较复杂:它们可以被setter们创建,具有不同的枚举性和可写性。数组中则不具有如此的定制性,而只存在有和无这两种状态。在引擎层面,这允许更多存储结构方面的优化。特别是当数组中存在数字时,例如当你需要容器时,不用定义具有x,y,z属性的类,而只用数组就可以了。

JavaScript中对象和数组之间只有一个的主要区别,那就是数组神奇的length属性。如果你自己来维护这个属性,那么V8中对象和数组的速度是一样快的。

使用对象时的技巧

  • 使用一个构造函数来创建对象。这将确保它创建的所有对象具有相同的隐藏类,并有助于避免更改这些类。作为一个额外的好处,它也略快于Object.create()
  • 你的应用中,对于使用不同类型的对象和其复杂度(在合理的范围内:长原型链往往是有害的,呈现只有一个极少数属性的对象比大对象会快一点)是有没限制的。对于“hot”对象,尽量保持短原型链,并且少属性。

对象克隆

对于应用程序开发人员,对象克隆是一个常见的问题。虽然各种基准测试可以证明V8对这个问题处理得很好,但仍要小心。复制大的东西通常是较慢的——不要这么做。JS中的for..in循环尤其糟糕,因为它有着恶魔般的规范,并且无论是在哪个引擎中,都可能永远不会比任何对象快。

当你一定要在关键性能代码路径上复制对象时,使用数组或一个自定义的“拷贝构造函数”功能明确地复制每个属性。这可能是最快的方式:

JavaScript
1
2
3
4
5
function clone(original) {
  this.foo = original.foo;
  this.bar = original.bar;
}
var copy = new clone(original);

模块模式中缓存函数

使用模块模式时缓存函数,可能会导致性能方面的提升。参阅下面的例子,因为它总是创建成员函数的新副本,你看到的变化可能会比较慢。

另外请注意,使用这种方法明显更优,不仅仅是依靠原型模式(经过jsPerf测试确认)。

Performance improvements

使用模块模式或原型模式时的性能提升

这是一个原型模式与模块模式的性能对比测试

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
  // Prototypal pattern
  Klass1 = function () {}
  Klass1.prototype.foo = function () {
      log('foo');
  }
  Klass1.prototype.bar = function () {
      log('bar');
  }
 
  // Module pattern
  Klass2 = function () {
      var foo = function () {
          log('foo');
      },
      bar = function () {
          log('bar');
      };
 
      return {
          foo: foo,
          bar: bar
      }
  }
 
  // Module pattern with cached functions
  var FooFunction = function () {
      log('foo');
  };
  var BarFunction = function () {
      log('bar');
  };
 
  Klass3 = function () {
      return {
          foo: FooFunction,
          bar: BarFunction
      }
  }
 
  // Iteration tests
 
  // Prototypal
  var i = 1000,
      objs = [];
  while (i--) {
      var o = new Klass1()
      objs.push(new Klass1());
      o.bar;
      o.foo;
  }
 
  // Module pattern
  var i = 1000,
      objs = [];
  while (i--) {

抱歉!评论已关闭.