1. 1. 前言
  2. 2. 部分译文
    1. 2.1. ECMAScript
      1. 2.1.1. Avoid using eval or the Function constructor(避免使用 eval 和 Function 构造器)
      2. 2.1.2. Rewrite that eval(改写 eval)
      3. 2.1.3. Avoid using with(避免使用 with)
      4. 2.1.4. Don’t use try-catch-finally inside performance-critical functions
      5. 2.1.5. (在对性能要求比较高的函数中尽量不要使用 try-catch-finally)
      6. 2.1.6. Isolate uses of eval and with()
      7. 2.1.7. Avoid using global variables(避免使用全局变量)
      8. 2.1.8. Beware of implicit object conversion(注意隐形对象转化)
      9. 2.1.9. Avoid for-in in performance-critical functions(在对性能要求较高的函数中避免使用 for-in)
      10. 2.1.10. Use strings accumulator-style(使用字符串相加样式)
      11. 2.1.11. Primitive operations can be faster than function calls()
      12. 2.1.12. Pass functions, not strings, to setTimeout() and setInterval()()
    2. 2.2. DOM
      1. 2.2.1. repaint and reflow(重绘与重流)
      2. 2.2.2. Document tree modification(修改 DOM 树)
      3. 2.2.3. Modifying an invisible element(修改一个不可见的元素)
      4. 2.2.4. Taking measurements(测量)
      5. 2.2.5. Making several style changes at once(一次执行多次样式改变)
      6. 2.2.6. Trading smoothness for speed(牺牲流畅来换取速度)
      7. 2.2.7. Avoid inspecting large numbers of nodes(避免检查大量的节点)
      8. 2.2.8. Improve speed with XPath(用 XPath 提高速度)
      9. 2.2.9. Avoid modifications while traversing the DOM(遍历 DOM 的时候避免修改)
      10. 2.2.10. Cache DOM values in script variables(在脚本变量中缓存 DOM 值)
    3. 2.3. Document loading(文档载入)
      1. 2.3.1. Avoid keeping alive references from one document to another()
      2. 2.3.2. Fast history navigation()
      3. 2.3.3. Use XMLHttpRequest()
      4. 2.3.4. Create \ elements dynamically(动态创建 script 标签)
      5. 2.3.5. location.replace() keeps the history under control()

前言

一直打算手写一个轮播焦点图,但是发现在动画效果选择的方面有很多种方式可供选择:可以通过调整 margin-left 值,也可以通过 position: absolute, left: ..px 的方式,还可以通过 transform: translate(-..px, 0px)的方式来实现。网上也有各种各样的方式,都可以实现轮播的效果,但是到底哪一种是最优的方式呢?我问了一些人也看了一些博客,最后的结论是在不考虑兼容性问题的情况下, transform: translate() 的方式是性能最佳的,因为 transform: translate()不会导致重流。

在探索最佳方式的过程中,找到了一篇 segment 的问答,回复中有一个 id 为 bf 的作者提到了 efficient JavaScript 这篇文章。看了下这篇文章,是 2006.11.02 发表的,虽然有些古老,但是依然有很多内容非常有参考价值。

这里就挑一部分比较有参考价值的内容进行翻译。

原文地址:https://dev.opera.com/articles/efficient-javascript/?page=3#reflow

部分译文

ECMAScript
Avoid using eval or the Function constructor(避免使用 eval 和 Function 构造器)

每次在一个代表源代码的字符串上调用 eval 或者 Function 构造器的时候,脚本引擎就会启动一个将源码转化成可执行代码的机器。这对于性能而言是非常昂贵的——例如,很容易就比普通函数的调用昂贵上百倍。

eval 函数尤其糟糕,传入 eval 的字符串内容无法提前预知。…

Function 构造器没有 eval 那么糟糕,…,但是运行起来也很慢。

Rewrite that eval(改写 eval)

Avoid using with(避免使用 with)

Don’t use try-catch-finally inside performance-critical functions
(在对性能要求比较高的函数中尽量不要使用 try-catch-finally)

Isolate uses of eval and with()

Avoid using global variables(避免使用全局变量)

在全局中创建变量是非常诱人的,仅仅因为这样做很简单。然而,这样可以让脚本运行更加缓慢。

首先,如果某个函数或者另外一个作用域中的代码引用了这个变量,脚本引擎必须依次进入到作用域链中的每个作用域中,直到到达全局作用域。本地作用域中的变量可以更加迅速的被找到。

全局变量也会在脚本的生命周期中一直存在。在局部作用域,当局部作用域丢失的时候,局部变量就会被摧毁。变量所使用的内存就会被垃圾回收器释放。

最后,全局作用域被 window 对象共享,也就意味着它实际上处于两个作用域中,而不仅仅是一个。在全局作用域中,变量总是可以通过他们的名字被定位,而不是像在局部作用域中一样,使用一个优化的预定义索引。最终结果就是,全局变量需要脚本引擎花费更多的时间去寻找。

函数也会经常在全局作用域中被创建。这就意味着调用其他函数的函数,以及这些函数再去调用其他函数,就会增加脚本引擎必须去遍历到全局作用域中定位这些全局函数的次数。

看看下面这个简单的例子,例子中 i 和 s 处于全局作用域中,函数使用了这两个全局变量:

var i, s = '';
function testfunction() {
    for(i = 0; i < 20; i++) {
        s += i;
    }
}
testfunction();

这个代替版本会执行的更加迅速。在大多数的现代浏览器中,包括 Opera 9 和最新版本的 IE ,FF,Konqueror and Safari,执行速度都会比前面的代码快 30%。

Beware of implicit object conversion(注意隐形对象转化)

字面量,比如 字符串,数字,布尔值等,在 ECMAScript 中都有两个代表。每种类型都可以或者通过值的形式或者通过对象的形式创建。例如,一个字符串值可以仅仅通过 var oString = ‘some content’; 的方式创建,而另外一种同样等价的字符串对象也可以通过 var oString = new String(‘some content’); 的方式创建。

任何属性和方法都是定义在字符串对象上的,而不是字符串值上。当你引用一个字符串值得属性或者方法时,在正式运行方法之前,ECMAScript引擎必须隐形地创建一个和你的字符串值有相同值的字符串对象。这个对象仅仅因为这一个请求而被使用,而当你下次要使用这个字符串值的方法的时候还要被创建。

这个例子让脚本引擎创建了 21 个新的字符串对象,每次获取长度属性的时候都会创建,每次调用 charAt 方法的时候也会创建:

var s = "0123456789";
for (var i = 0; i < s.length; i++) {
    s.charAt(i);
}

下面的例子可以实现同样的效果,但是只创建了一个对象,性能结果会更好一些:

var s = new String("0123456789");
for(var i = 0; i < s.length; i++) {
    s.charAt(i);
}

如果你的代码需要经常调用字面量值的方法,你可以考虑像前面这个例子一样将其转化成对象。

注意,尽管这篇文章中的大多数观点都和所有的浏览器相关,但是这些观点主要是针对于 Opera 的优化。It may also affect some other browsers, but can be a little slower in Internet Explorer and Firefox.

Avoid for-in in performance-critical functions(在对性能要求较高的函数中避免使用 for-in)

for-in 循环有其独特的作用,但是经常被误用在一些 for 循环更合适的场景中。for-in 循环要求脚本引擎建立一个所有可列举属性的列表,在可以开始列举之前,在列表中会复制。??

很多时候,脚本自己已经知道了什么属性必须被列举。在很多情况下,一个简单的 for 循环可以用来遍历这些属性,尤其是如果属性是按照数字顺序命名的时候,比如数组,还有很多类似数组的对象(DOM 创建的 NodeList 对象就是一个很好的例子)。

下例是一个没有正确使用 for-in 循环的例子:

var oSum = 0;
for(var i in oArray) {
    oSum += oArray[i];
}

for 循环会更有效率:

var oSum = 0;
var oLength = oArray.length;
for(var i = 0; i < oLength; i++) {
    oSum += oArray[i];
}
Use strings accumulator-style(使用字符串相加样式)

字符串相连可能会是一个非常昂贵的过程。使用 + 操作符不会等待结果指定给变量。而是,在内存中创建一个新的字符串,将结果指定给那个字符串,是由这个新的字符串可能会赋值给一个变量。下面的代码展示了一个通常赋值一个字符串连接的方式:

a += 'x' + 'y';

That code would be evaluated by firstly creating a temporary string in memory, assigning the concatenated value of xy, then concatenating that with the current value of a, and finally assigning the resulting value of that to a. The following code uses two separate commands, but because it assigns directly to a each time, the temporary string is not used. The resulting code is around 20% faster in many current browsers, and potentially requires less memory, as it does not need to temporarily store the concatenated string:

a += 'x';
a += 'y';
Primitive operations can be faster than function calls()

Pass functions, not strings, to setTimeout() and setInterval()()

DOM
repaint and reflow(重绘与重流)

重绘(repaint)——也叫重画(redraw)——就是,当之前没有显示的内容被显示出来的时候会发生,重绘并不会改变文档布局。例如,添加一个元素的轮廓线,改变背景色,或者改变 visibility 样式。重绘在性能方面是非常昂贵的,因为它需要引擎去挨个搜索一遍所有的元素,来决定什么是可见的(visible),而什么是要被展示的(display)。

而重流(reflow)则是一种更为明显的变化。当 DOM 树被操作的时候,或者影响布局的某个样式被改变的时候,或者某个元素的 className 属性发生变化的时候,再或者当浏览器窗口的尺寸变化的时候,都会发生重流。这种情况下,引擎必须重流相关元素,以便于了解重流的部分应当被展示在哪里。子元素也将会重流以便于将父元素的新布局也考虑进去。重流元素后面的元素也需要重流,以计算其新的布局,祖先元素也会重流,最终所有的元素都会被重绘(repaint)。

重流在性能方面是非常昂贵的,也是导致 DOM 脚本变慢的一个主要原因,尤其是在那些处理能力较弱的设备上,比如手机。在很多时候,这几乎等同于重新布局整个页面。

尽量减少重流

很多时候,我们的脚本都需要触发一个重绘或者重流。动画就是基于重流的,但是我们依然渴望动画。所以,重流是网页开发的一个要素,为了让脚本能够运行的更快,他们需要在保证整体效果的情况下,尽量减少。

浏览器会一直等待脚本线程结束,然后再进行重流来展示变化。…

Document tree modification(修改 DOM 树)

DOM 树的修改将会触发重流。给 DOM 添加新的元素,改变文本节点的值,或者修改元素的一些属性都足以导致一次重流的发生。一次又一次的制造变化,会导致不只一次的重流,所以通常来讲,最好以不会被显示的 DOM 树碎片来执行变化。然后将这些变化一次性地通过操作添加到真实的文档 DOM 中:

var docFrag = document.createDocumentFragment();
var elem, contents;
for (var i = 0; i < textlist.length; i++) {
    elem = document.createElement('p');
    content = document.createTextNode(textlist[i]);
    elem.appendChild(content);
    docFrag.appendChild(elem);
}
document.body.appendChild(docFrag);

DOM 树的修改也可以通过元素克隆来完成,当修改完成后再移除原来的真实元素,这样只会导致一次重流。如果元素包含任何表格控件的时候,不能使用这种方法,因为任何用户对这些控件值的修改都不会反映在主要的 DOM 树中。如果你需要依赖元素或者其子元素的事件处理器时,也不能使用这种方法,因为理论上事件处理器是无法被克隆的。

var original = document.getElementById('container');
var cloned = original.cloneNode(true);
var elem, contents;
for (var i = 0; i < textlist.length; i++) {
    elem = document.createElement('p');
    contents = document.createTextNode(textlist[i]);
    elem.appendChild(contens);
    cloned.appendChild(elem);
}
original.parentNode.replaceChild(cloned, original);
Modifying an invisible element(修改一个不可见的元素)

当一个元素的 display 样式设置为 none 的时候,它无需重绘,即使它的内容发生了改变,因为这些改变都不会被显示出来。我们可以利用这个特点。如果某个元素或者其内容需要发生多次改变,并且这些改变无法结合成为一次重绘时,就可以先将元素设置为 display:none,然后执行这些改变(多次改变),最后再将这个元素的 display 设置回原来的值。

这会导致两次额外的重流,第一次是当元素被隐藏的时候,另一次当它再次重新显示的时候,但是总的效果可能会非常快。如果元素自己影响了滚动偏移的话,这也可能会出现我们不希望出现的滚动条的跳跃。但是,对于一个已经定好位置的元素而言,这非常容易应用,而且不会导致不好的视觉效果。

var posElem = document.getElementById('animation');
posElem.style.display = 'none';
posElem.appendChild(newNodes);
posElem.style.width = '10em';
//other changes...
posElme.style.display = 'block';
Taking measurements(测量)

如前所述,浏览器也许会为你缓存一些变化,当这些变化都发生的时候,就会重绘一次。但是要注意,测量元素的大小会进行强制重流,以保证测量结果的准确性。变化也是是一次很明显的重绘,也许不是,但是在后台重流依然一定会被触发。

当使用诸如 offsetWidth 这样的属性进行测量,或者 getComputedStyle 这样的方法进行测量时,就会导致上面所说的效果。即使没有使用测量数据,而尽管浏览器也在缓存变化,使用上述方法中的任意一种也足以触发一次隐形的重流。如果这些测量会不断地发生,你就应当考虑将这些测量合成一次,并将结果保存起来,用于后续的使用。

var posElem = document.getElementById('animation');
var calcWidth = posElem.offsetWidth;
posElem.style.fontSize = (calcWidth/10) + 'px';
posElem.firstChildNode.style.marginLeft = (calcWidth/20) + 'px';
posElem.style.left = ((-1*calcWidth)/2) + 'px';
//other changes
Making several style changes at once(一次执行多次样式改变)

和 DOM 树修改一样,为了减少重绘或者重流的次数,可以一次修改多个样式的变化,一般情况下我们都会一次修改一个样式:

var toChange = document.getElementById('mainelement');
toChange.style.backgroud = '#333';
toChange.style.color = '#fff';
toChange.style.border = '1px solid #00f';

上述方法意味着多次的重绘和重流。有两种主要的方式来更好的处理上述情况。如果元素自身需要采用多次样式变化,而这些样式的值又都可以提前知道,那么可以通过改变元素的 class 来进行。它就会采用这个 class 的新样式:

div {
    background: #ddd;
    color: #000;
    border: 1px solid #000;
}
div.highlight {
    background: #333;
    color: #fff;
    border: 1px solid #00f;
}
…
document.getElementById('mainelement').className = 'highlight';

第二种方式则是为元素定义一个新的样式属性,而不是一个接一个的指定样式。大多数情况下,这种方式适合动画类的动态改变,这种情况下新的样式是无法预知的。通过样式对象(style object) 的 cssText property,或者使用 setAttribute 方法都可以实现。 IE 不支持第二种方式,而只能采用第一种方式。一些更老的浏览器,包括 opera 8 ,只能采用第二种方式,而不支持第一种方式。所以,最佳的方法就是检测第一个版本是否支持,如果支持就使用第一个版本,如果不支持就使用第二个版本。

var posElem = document.getElmentById('mainelement');
var newStyle = "backgroud: " + newBack + ";" +
                "color: " + newColor + ";" +
                "border: " + newBorder + ";";
if (typeof(posElem.style.cssText) != 'undefied') {
    posElem.style.cssText = newStyle;
} else {
    posElem.setAttribute('style', newStyle);
}
Trading smoothness for speed(牺牲流畅来换取速度)

作为一个开发者,通过使用更短的延迟,更细小的变化,让动画尽可能的精细是一件非常有诱惑力的事情。例如,动画效果可以使用 10ms 的间隔,每次移动 1 像素来实现。如此快速运转的动画在一些 PC 或者浏览器上运转会非常流畅。然而,10ms 几乎是浏览器不使用100% 的 CPU 来获得的最短间隔。一些浏览器甚至无法执行这样的效果——要求每秒执行 100 次重流,对于大多数浏览器而言是非常难的。低能力的电脑,或者设备浏览器,完全无法执行这样的速度,动画就会看起来很慢而且很卡顿。

这会吞噬开发者的信心,我么可以考虑牺牲一些动画的顺畅换来速度的提升。将时间间隔改为 50ms ,动画移动改为 5像素,就会需要更少的处理能力,并且可以让动画在低能力处理器上运行地更快。

Avoid inspecting large numbers of nodes(避免检查大量的节点)

当尝试去定位某个节点或者某个子节点时,尽量使用内置的方法和 DOM 集合来收缩节点的搜索范围。例如,如果你需要定位一个文档中未知的元素,它拥有某个属性,你也许可以这样:

var allElements = document.getElementsByTagName("*");
for(var i = 0; i < allElements.length; i++) {
    if(allElements[i].hasAttribute("someAttr")) {
        //...
    }
}

即使我们忽略类似 XPath 这样的高级技术,上面的例子依然有两个问题导致运行缓慢。第一,它搜索了所有的元素,而完全没有尝试去缩小搜索范围。第二,当它找到想要找的元素之后依然在搜索。例如,假设,我们要找的这个未知元素在 id 为 inhere 的一个 div 中,修改后的代码可以执行的更快:

var allElements = document.getElementById('inhere').getElementsByTagName('*');
for(var i = 0; i < allElements.length; i++) {
    if(allElements[i].hasAttribute('someattr')) {
        // …
        break;
    }
}

如果这个位置的元素是某个 div 的直接子元素,这个方法就会比上述方法还要快,速度取决于 div 的后代元素的数量,和子元素集合的长度比较:

var allChildren = document.getElementById('inhere')h3 id=h3 id=.childNodes;
for(var i = 0; i < allChildren.length; i++) {
    if(allChildren[i].nodeType == 1 && allChildren[i].hasAttribute('someattr'))         {
        // …
        break;
    }
}

根本目的就是尽可能地去避免一步一步遍历整个 DOM。在不同的情况下,DOM 有很多执行更快的可替代方式,例如 DOM 2 Traversal TreeWalker, 而不是一步步递归去遍历 childNodes collections。

Improve speed with XPath(用 XPath 提高速度)

一个简单的例子就是,基于 H2-H4 元素,在 HTML 文档中回执表格内容。在 HTML 中,在很多地方都可能出现,不需要什么合适的层级关系,所以一个递归函数可以用来以正确的顺序获取这些元素。传统的 DOM 会采用类似下面所示的方式:

var allElements = document.getElementsByTagName("*");
for(var i = 0; i < allElements.length; i++) {
    if(allElements[i].tagName.match(/^h[2-4]$/i)) {
        // …
    }
}

在一个可能拥有 2000 个元素的文档中,这可能会导致非常严重的迟缓,因为每一个都要分开检查。XPath,当被原生支持的时候,因为 XPath 查询引擎可以被优化,比解释后的 JS 更有效率。在某些情况下,……

Avoid modifications while traversing the DOM(遍历 DOM 的时候避免修改)

某些 DOM 集合是实时更新的,这就意味着,如果当你的脚本查看集合的时候,相关元素会发生变化,不等你的脚本运行结束,集合就会发生改变。这包括 childNodes 集合和由 getElementsByTagName 返回的节点集合。

如果脚本正在遍历上述类似集合,并且同时,还会给它添加元素,你很可能冒着进入无限循环的风险,在这种情况下,在你到达终点之前,你会不断地添加入口。然而,这并不是唯一的问题。这些集合在表现方面可以优化。他们可以记住长度,你的脚本引用的最后一个索引,以便于当你递增索引的时候,他们可以快速的引用下一个节点。

如果你修改了 DOM 树中的任何一部分,即使它并没有包含在集合中,集合也必须被重新测试寻找新的入口。这样的话,它就无法记住最后的索引和长度,因为他们可能会发生变化,优化就会失效:

var allPara = document.getElementsByTagName('p');
for (var i = 0; i < allPara.length; i++) {
    allPara[i].appendChild(document.createTextNode(i));
}

在 Opera 和一些其他现代浏览器中,下面这段同样功能的代码是上面代码的 10 倍。它通过首先建立一个要修改的静态元素列表,接着通过依次遍历静态列表来执行修改,而不是通过遍历 getElementByTagName 返回的节点列表,实现功能:

var allPara = document.getElementsByTagName('p');
var collectTemp = [];
for (var i = 0; i < allPara.length; i++) {
    collectTemp[collectTemp.length] = allPara[i];
}
for (var i = 0; i < collectTemp.length; i++) {
    collectTemp[i].appendChild(document.createTextNode(i));
}
collectTemp = null
Cache DOM values in script variables(在脚本变量中缓存 DOM 值)

一些 DOM 的缓存值是无法缓存的,每次被调用时都会重新评估。getElementById 方法就是一个例子。下面就是一个非常浪费的代码的例子:

document.getElementById('test').property1 = 'value1';
document.getElementById('test').property2 = 'value2';
document.getElementById('test').property3 = 'value3';
document.getElementById('test').property4 = 'value4';

上述代码在定位同一个对象时执行了 4 次请求。下面的代码只执行一次请求然后将其存起来,对于这一次请求而言,速度几乎是相同的,在执行赋值的时候可能会有一点点慢。但是,之后每一次缓存值被使用的时候,在大多数浏览器中,执行速度会是上面例子中代码执行速度的 5-10 倍。

var sample = document.getElementById('test');
sample.property1 = 'value1';
sample.property2 = 'value2';
sample.property3 = 'value3';
sample.property4 = 'value4';
Document loading(文档载入)
Avoid keeping alive references from one document to another()

Fast history navigation()

Use XMLHttpRequest()

Create \ elements dynamically(动态创建 script 标签)

载入和处理脚本需要花费时间,但是有些时候,某个载入的脚本可能从来不会被使用到。载入这样的脚本只会浪费时间和资源,并且拖延当前脚本的运行,这种情况下,如果不会被使用的话,最好根本就不要载入它。通过一个简单的载入脚本就可以实现这样的功能,这个载入脚本可以检查其他脚本是否需要,只会创建那些真的会被使用到的脚本元素。

理论上来讲,其余的脚本可能通过创建 SCRIPT 标签的方式在页面载入之后被添加。当前所有的主流浏览器都可以实现这个效果,但是…另外,也许有些脚本在页面在入职前就会需要,所以最好在页面载入的时候进行检查,

location.replace() keeps the history under control()