踏歌

JavaScript 终极优化 / Stoyan's phpied.com

踏歌 · 2017-01-18翻译 · 713阅读 原文链接

2010 更新:

大家好,Web Performance Advent Calendar的网址已经变更了。 12月20日,本文是2009 performance advent calendar实验的一部分。今天的文章是来自Ara Pehlivanian的第二篇文章(这里是第一篇).

谢谢Patricia提供的白俄罗斯语的翻译

Ara Pehlivanian

自1997年以来,Ara Pehlivanian一直在web领域工作。他曾经做过自由职业者,网站管理员。最近他成为了雅虎的一个前端工程师。Ara的经验来自于在他的职业生涯中从事Web开发的每一个方面,但他现在热衷于基于Web标准的前端开发。

在今天的JavaScript世界中有一个奇怪的现象。尽管这个语言在过去的十年几乎没有改变,但其在程序员中却出现了演变。他们使用相同的语言,带给我们滚动状态栏文本,以编写一些非常重的客户端应用程序。虽然这看起来像我们在F1比赛中进入拉达,但在现实中,我们花了过去十年在车道上来回驾驶F1赛车。我们从未使用该语言的全部潜力。Ajax的发现让我们进入了一个新的时代。虽然已经处在新的前端世界里,但还是存在很多棘手的问题待解决。就像赛车一样,并不是每个人都知道以250mph的速度驾驶F1赛车。

我想说的是,你很容易踩油门来驾驶达到60英里/小时。如果你想避免磨损,你将不得不换档。这和在JavaScript中编写大型客户端应用程序也是一样的。快速的处理器可以让我们做任何事情而不用考虑优化。对于较小的程序确实如此,但是写太多的不好的JavaScript代码会很快让你的代码达到令人抓狂的状态。所以就像一个普通司机需要训练驾驶赛车一样,我们需要掌握这种语言的输入和输出以保证大规模的应用程序可以流畅运行。

变量

让我们来看看编程的主要内容之一的变量。一些语言要求你在使用变量之前必须先声明他们,JavaScript却没有这个要求,然而这并不意味着没有要求就不需要那样做。这是因为在JavaScript中,如果一个变量没有明确的用var关键词声明,那么它将被认为是一个全局变量,全局变量都会使速度变慢。为什么?因为编译器需要搜索(作用域 )以知道变量最初声明的位置。请参见以下示例:

function doSomething(val) {
    count += val;
};

变量count会不会在doSomehting的作用域外被赋值?又或者是它仅仅是没有被正确声明?另外,在大型的应用程序中,具有这样的通用全局变量名称使得变量容易发生冲突。

循环

上例中,在作用域链中只搜索一次count变量声明的位置并不是什么大事,但是在大型Web程序中,并不能保证这样的事情只发生一次。尤其是涉及到循环,要记住循环的第一件事,是尽可能多的工作放在循环外,这不仅仅是针对JavaScript。循环中做的越少,循环的速度就越快。话虽如此,让我们来看看JavaScript循环中最常见的可以避免的做法。看看下面的例子,看看你能不能发现:

for (var i = 0; i < arr.length; i++) {
    // some code here
}

你看到了吗?arr数组的长度在每一次循环迭代中都重复计算了。一个简单的方法就像是下面这样讲数组的长度缓存到一个变量中:

for (var i = 0, len = arr.length; i < len; i++) {
    // some code here
}

通过这种方法,数组的长度只会在计算一次,在往后的循环中都会引用缓存的值。

那么我们还能做些什么来提高循环的性能呢?那么,每次迭代还要做什么其他工作?我们会判断i的值是否小于len的值并且每次都将i的值加1。我们可以减少这里的操作次数吗?但是可以的如果循环执行的顺序无关紧要。

for (var i = 100; i--; ) {
    // some code here
}

这个循环执行的速度比上面的要快50%。因为在每次迭代中它仅仅是将i减1直到值不是false,换句话说就是值不为0。当value等于0,循环就会结束。

你也可以用下面的这种循环来达到一样的效果:

while (i--) {
    // some code here
}

和上面一样,i值的判断和减一的操作同时进行,所有的while循环都需要使i得值为false或者0来结束循环。

缓存

在上面将数组的长度缓存到一个变量的时候我们简单的介绍了缓存。同样的原则也适用于其他的JavaScript代码中,基本原则应该避免让编译器做一些重复的事情。例如在作用域链中查找一个全局变量,我们就可以将它的引用保存为局部变量从而避免让编译器每次都去找。这里,让我用下面的代码来说明这个问题。

var aGlobalVar = 1;

function doSomething(val) {
    var i = 1000, agv = aGlobalVar;
    while (i--) {
        agv += val;
    };
    aGlobalVar = agv;
};

doSomething(10);

在这个例子中, aGlobalVar这个变量仅仅取了两次值。第一次我们是取了它的值并赋值给一个局部变量,第二次我们将局部变量的值又赋给了全局变量。如果我们直接在循环中用这个全局变量,那么便会将访问这个变量1000次。事实上,上面那个循环执行avg +=val;会花费3ms,如果用aGlobalVal += val;替换,那么则会10ms.

属性嵌套的深度

嵌套的对象可以方便的使用点操作符来访问,这对于命名空间和组织代码而言是一个非常有效的方法。不幸的是,当考虑性能的时候,这样做可能会存在一些问题。每次在对象中取值,编译器必须遍历这个对象才能取得对应的值,遍历的层级越深,消耗的时间越长。所以即使命名空间对于代码的组织而言确实不错,使对象的层级尽可能浅才能得到更好的性能。YUI库的最新版本已经消除了命名空间的整个嵌套层。例如YAHOO.utl.Anim已经变成了Y.Anim

总结

这只是一些通过了解JavaScript编译器如何工作来提高代码性能的例子。请记住,虽然浏览器不断发展,即使语言不是。例如,今天的浏览器正在引入JIT编译器来加快性能,但这并不意味着我们应该在我们的做法中减少警惕。因为到最后,当你的网络应用程序取得巨大的成功,全世界都在关注的时候,每一毫秒的优化都会带来巨大的价值。

分享到: Facebook, Twitter, Google+

« The new game show: “Will it reflow?” Progressive rendering via multiple flushes »

很抱歉,由于垃圾邮件过多,因此停用和隐藏了评论。正在还原现有评论..

与此同时,你也可以在twitter上关注@stoyanstefanov

相关文章