作者 Mark 'Tarquin' Wilton-Jones · 2006年11月2日
本文翻译自 Efficient JavaScript
原译文地址 http://kb.operachina.com/node/207
传统上,网页中不会有大量的脚本,至少脚本很少会影响网页的性能。但随着网页越来越像 Web 应用程序,脚本的效率对网页性能影响越来越大。而且使用 Web 技术开发的应用程序现在越来越多,因此提高脚本的性能变得很重要。
对于桌面应用程序,通常使用编译器将源代码转换为二进制程序。编译器可以花费大量时间优化最终二进制程序的效率。Web 应用程序则不同。因为Web应用程序需要运行在不同的浏览器、平台和架构中,不可能事先完全编译。浏览器在获得脚本后要执行解释和编译工作。用户要求不仅要求网页能快速的载入,而且要求最终 Web 应用程序执行的效果要和桌面应用程序的一样流畅。Web 应用程序应能运行在多种设备上,从普通的桌面电脑到手机。
浏览器并不很擅长此项工作。虽然 Opera 有着当前最快的脚本引擎,但浏览器有不可避免的局限性,这时就需要 Web 开发者的帮助。Web开发者提高 Web 应用程序的性能的方法很多而且也很简单,如只需要将一种循环变成另一种、将组合样式分解成三个或者只添加实际需要的脚本。
本文从 ECMAScript/JavaScript, DOM, 和页面载入方面分别介绍几种简单的能提高 Web 应用程序性能的方法。
eval
或 Function
构造函数
with
try-catch-finally
eval
和 with
for-in
setTimeout()
和 setInterval()
传送函数名,而不要传送字符串 eval
或 Function
构造函数每次 eval
或 Function
构造函数作用于字符串表示的源代码时,脚本引擎都需要将源代码转换成可执行代码。这是很消耗资源的操作 —— 通常比简单的函数调用慢100倍以上。
eval
函数效率特别低,由于事先无法知晓传给 eval
的字符串中的内容,eval
在其上下文中解释要处理的代码,也就是说编译器无法优化上下文,因此只能有浏览器在运行时解释代码。这对性能影响很大。
Function
构造函数比 eval
略好,因为使用此代码不会影响周围代码;但其速度仍很慢。
eval
eval
Not only is it inefficient, but it is also completely unnecessary to use in most cases. In many cases, eval is used because the information is provided in string form, and developers mistakenly believe that only eval can use this information. The following example is a typical error:
The code below performs the exact same function, but without using eval
:
The latter is 95% faster than the former in Opera 9, Firefox, and Internet Explorer, and 85% faster in Safari. (Note that this comparison does not include the function call time.)
The following is the common Function
constructor usage:
The code below does not use the Function
constructor, but provides the same functionality: by creating an anonymous function:
with
Although it seems convenient, with
it is very inefficient. The with
structure creates another scope for the script engine to search when using variables. This in itself only slightly affects performance. But the serious thing is that the contents of this scope are not known at compile time, so the compiler cannot optimize it like it can for other scopes (such as scopes generated by functions).
Another efficient and convenient method is to use variables to reference objects, and then use variables to access object properties. But this only applies if the property is not a literal type, such as a string or a boolean.
Consider the following code:
The following code is more efficient:
<pre class="brush:javascript">var testObject = test.information.settings.files; testObject.primary = 'names'; testObject.secondary = 'roles'; testObject.tertiary = 'references';
try-catch-finally
try-catch-finally
The structure is rather special. Unlike other syntax constructs, it creates new variables in the current scope of the runtime. Whenever catch
is executed, the captured exception object is assigned to a variable. This variable does not belong to any script. It is created at the beginning of the catch
statement and destroyed at the end.
Because this function is special and is dynamically created and destroyed at runtime, some browsers do not process it efficiently. Placing catch statements in critical loops will greatly impact performance.
If possible, handle exceptions in scripts where they are not called frequently, or avoid them by checking whether an action is supported. In the following example, if the required property does not exist, many exceptions will be thrown in the loop statement:
try-catch-finally
结构移到循环外部。这样做稍微改变了程序语义,因为如果抛出异常,将停止整个循环:var oProperties = ['first','second','third',...,'nth'], i; try { for( i = 0; i < oProperties.length; i++ ) { test[oProperties[i]].someproperty = somevalue; } } catch(e) { ... }
有时可用属性检测或其他检测代替 try-catch-finally
结构:
eval
和 with
因为 eval 和 with 结构严重影响性能,应该尽量避免使用这些结构。但如不得不使用时, 避免在频繁被调用的函数中或循环中使用这些结构。最好将这些结构放在只运行一次,或少量几次的代码中,并不要将其放在对性能要求较高的代码中。
如果可能,尽量将这些结构和其他代码分隔开,这样他们就不会影响脚本性能。如将其放在顶级函数中,或只执行一次然后保存运行结果,避免再次使用。
try-catch-finally
结构在一些浏览器中也会影响性能,包括 Opera ,因此最好也将其分隔。
全局变量使用简单,因此很容易禁不住诱惑在脚本中使用全局变量。但有时全局变量也会影响脚本性能。
首先,如果函数或其他作用域内引用了全局变量,则脚本引擎不得不一级一级查看作用域直到搜索到全局作用域。查询本地作用域变量更快。
其次,全局变量将始终存在在脚本生命周期中。而本地变量在本地作用域结束后就将被销毁,其所使用的内存也会被垃圾收集器回收。
最后,window 对象也共享全局作用域,也就是说本质上是两个作用域而不是一个。使用全局变量不能像使用本地变量那样使用前缀,因此脚本引擎要花更多时间查找全局变量。
也可在全局作用域中创建全局函数。函数中可以调用其他函数,随着函数调用级数增加,脚本引擎需要花更多时间才能找到全局变量以找到全局变量。
考虑下面的简单例子,i 和 s 是全局作用域且函数使用这两个全局变量:
下面的函数效率更高。在大多数浏览器中,包括 Opera 9、最新版 Internet Explorer, Firefox, Konqueror 和 Safari,后者执行速度比上面代码快30%。
function testfunction() { var i, s = ''; for( i = 0; i < 20; i++ ) { s += i; } } testfunction();
Literal,如字符串、数字和布尔值在 ECMAScript 中有两种表示方法。 每个类型都可以创建变量值或对象。如 var oString = 'some content';
, 创建了字符串值,而 var oString = new String('some content');
创建了字符串对象。
所有的属性和方法都定义在 string 对象中,而不是 string 值中。每次使用 string 值的方法或属性,ECMAScript 引擎都会隐式的用相同 string 值创建新的 string 对象。此对象只用于此请求,以后每次视图调用 string值方法是都会重新创建。
下面的代码将要求脚本引擎创建21个新 string 对象,每次使用 length 属性时都会产生一个,每一个 charAt 方法也会产生一个:
var s = '0123456789'; for( var i = 0; i < s.length; i++ ) { s.charAt(i); }
下面的代码和上面相同,但只创建了一个对象,因此其效率更高:
If your code often calls methods on literal values, you should consider creating objects like the example above.
Note that most of the tips in this article work on all browsers, but this tip is specific to Opera. This optimization tip doesn't improve as much in Internet Explorer and Firefox as it does in Opera.
for-in
for-in
is often misused, especially when a simple for
loop is more appropriate. for-in
The loop requires the script engine to create a list of all enumerable properties and then check for duplicates.
Sometimes enumerable properties are known to the script. At this point a simple for
loop can iterate over all properties, especially when using sequential numeric enumerations, such as in arrays.
The following is incorrectfor-in
Loop usage:
for
The loop will undoubtedly be more efficient:
String merging is relatively slow. The
operator does not store the result in a variable. It creates a new string object and assigns the result to this object; perhaps the new object is assigned to a variable. The following is a common string merging statement:
This code first creates a temporary string object to save the merged 'xy' value, then merges it with the a variable, and finally assigns the result to a. The code below uses two separate commands, but assigns directly to a each time, so there is no need to create a temporary string object. As a result, in most browsers, the latter is 20% faster than the former and consumes less memory:
Although the effect is not obvious when used alone, script performance may be improved if basic operators are used instead of function calls in critical loops and functions that require high performance. Examples include the push method of an array, which is less efficient than assigning directly to the end of the array. Another example is the Math object method. In most cases, simple mathematical operators are more efficient and more suitable.
The following code implements the same function, but is more efficient:
setTimeout()
and setInterval()
instead of the string setTimeout()
and setInterval()
methods are approximate to eval
. If the passed parameter is a string, after a period of time, the string value will be executed like eval
. Of course, its inefficiency will be the same as eval
.
But these methods can also accept a function as the first parameter. This function will be called after some time, but this function can be interpreted and optimized at compile time, which means better performance. A typical example of using string as a parameter is as follows:
第一个语句可以直接传递函数名。第二个语句中,可以使用匿名函数封装代码:
setInterval(updateResults,1000); setTimeout(function () { x += 3; prepareResult(); if( !hasCancelled ) { runmore(); } },500);
需要注意的是 timeout或时间延迟可能并不准确。通常浏览器会花比要求更多的时间。有些浏览器会稍微提早完成下一个延迟以补偿。有些浏览器每次可能都会等待准确时间。很多因素,如 CPU 速度、线程状态和 JavaScript负载都会影响时间延迟的精度。大多数浏览器无法提供1ms以下的延迟,可能会设置最小可能延迟,通常在10 和 100 ms之间。
通常主要有三种情况引起 DOM 运行速度变慢。第一就是执行大量 DOM 操作的脚本,如从获取的数据中建造新的 DOM 树。第二种情况是脚本引起太多的 reflow 或重绘。第三种情况是使用较慢的 DOM 节点定位方法。
第二种和第三种情况比较常见且对性能影响比较严重,因此先介绍前两种情况。
重绘也被称为重画,每当以前不可见的元素变得可见(或反之)时就需要重绘操作;重绘不会改变页面布局。如给元素添加轮廓、改变背景颜色、改变样式。重绘对性能影响很大,因为需要脚本引擎搜索所有元素以确定哪些是可见的及哪些是应被显示的。
Reflow 是更大规模的变化。当 DOM 数被改变时、影响布局的样式被修改时、当元素的 className属性被修改时或当浏览器窗口大小变化时都会引起 reflow。脚本引擎必须 reflow 相关元素以确定哪些部分不应被现实。其子节点也会被reflow 以考虑其父节点的新布局。DOM 中此元素之后出现的元素也被 reflow以计算新布局,因为它们的位置可能已被移动了。祖先节点也需要 reflow 以适应子节点大小的改变。总之,所有元素都需被重绘。
Reflow 从性能角度来说是非常耗时的操作,是导致 DOM 脚本较慢的主要原因之一,特别在手机等处理能力较弱的设备上。很多情况下,reflow 和重新布局整个网页耗时相近。
很多情况下脚本需要进行会引起 reflow 或重绘的操作,如动画就需要 reflow 操作,因此 reflow 是 Web 开发不可或缺的特性。为了让脚本能快速运行,应在不影响整体视觉效果的情况下尽量减少 reflow 次数。
浏览器可以选择缓存 reflow 操作,如可以等到脚本线程结束后才 reflow 以呈现变化。Opera 可以等待足够数量的改变后才reflow、或等待足够长时间后才 reflow、或等待脚本线程结束后才reflow。也就是说如果一个脚本线程中的发生很多间隔很小的改变时,可能只引起一个 reflow 。但开发者不能依赖此特性,特别是考虑到运行Opera 的不同设备的运算速度有很大差异。
注意不同元素的 reflow 消耗时间不同。Reflow 表格元素消耗的时间最多是 Reflow 块元素时间的3倍。
正常的 reflow 可能影响整个页面。reflow 的页面内容越多,则 reflow 操作的时间也越长。Reflow的页面内容越多,需要的时间也就越长。位置固定的元素不影响页面的布局,因此如果它们 reflow 则只需 reflow其本身。其背后的网页需要被重绘,但这比 reflow 整个页面要快得多。
所以动画不应该被用于整个页面,最好用于固定位置元素。大部分动画符合此要求。
修改 DOM 树会导致 reflow 。向 DOM 中添加新元素、修改 text 节点值或修改属性都可能导致 reflow。顺序执行多个修改会引起超过一个 reflow,因此最好将多个修改放在不可见的 DOM 树 fragment 中。这样就只需要一次 DOM 修改操作:
也可以在元素的克隆版本中进行多个 DOM 树修改操作,在修改结束后用克隆版本替换原版本即可,这样只需要一个 reflow操作。注意如果元素中包含表单控件,则不能使用此技巧,因为用户所做修改将无法反映在 DOM树种。此技巧也不应该用于绑定事件处理器的元素,因为理论上不应该克隆这些元素。
如果一个元素的 display 样式被设置为 none,即使其内容变化也不再需要重绘此元素,因为根本就不会显示此元素。可以利用这一点。如果需要对一个元素或其内容做出多个修改,又无法将这些更改放在一个重绘中,则可以先将元素设置为 display
:none ,做出修改后,在把元素改回原来状态。
上面方法将导致两个额外的 reflow,一个是隐藏元素时另一个是重新显示此元素时,但此方法的总体效率仍较高。如果隐藏的元素影响滚动条位置,上面的方法也有可能会引起滚动条跳动。但此技术也被用于固定位置元素而不会引起任何不好看的影响。
如上面所述,浏览器可能会缓存多个修改一起执行,并只执行一次 reflow 。但注意为保证结果正确,测量元素大小也会引起 reflow 。尽管这不会造成任何重绘,但仍会在后台进行 reflow 操作。
使用 offsetWidth 这样的属性或 getComputedStyle 这样的方法都会引起 reflow 。即使不使用返回的结果,上述操作也会引起立即 reflow。如果重复需要测量结果,可以考虑只测量一次但用变量保存结果。
与 DOM 树修改相似,可将多个样式修改一次进行,以尽量减少重绘或 reflow数目。常见设置样式方法是逐个设置:
上面代码可能引起多次 reflow 和重绘。有两种改进方法。如果元素采用了多个样式,而且这些样式值事先知道,可以通过修改元素 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 对象的 cssText 属性,或者通过 setAttribute. 可以实现此技巧。Internet Explorer 不允许第二种形式,支持第一种形式。有些较老的浏览器,包括 Opera 8 需要使用第二种形式,不支持第一种形式。最简单的方式是测试看是否支持第一种形式,如果支持就使用,如果不支持则使用第二种形式。
As a developer, of course you want the animation to run as smoothly as possible, usually using smaller time intervals or smaller changes. For example, update the animation every 10ms, or move 1 pixel each time. This animation may work perfectly on a desktop computer or in some browsers. But the 10ms interval may be the minimum that the browser can achieve by using 100% of the CPU. Some browsers can't even do it - requiring 100 reflows per second is not easy for most browsers. Low-performance computers or other devices may not be able to achieve this speed, and animations may be very slow or even unresponsive on these devices.
So it’s best to put developer pride aside for the time being and sacrifice smoothness for speed. Changing the time interval to 50ms or setting the animation step to 5 pixels will consume less computing resources and run normally on low-performance devices.
When you need to find a node, try to use DOM built-in methods and collections to narrow the search scope. If you want to locate an element that contains a certain attribute, you can use the following code:
Even if you haven’t heard of advanced techniques like XPath, you can see that the above code has two problems that slow it down. First it searches every element instead of trying to narrow down the search. Secondly, the search continues even after the required element has been found. If it is known that the element you are looking for is in a div with the id inhere, it is best to use the following code:
If it is known that the element you are looking for is a direct child node of the div, the following code is faster:
The basic idea is to try to avoid looking at DOM nodes one by one. DOM has many better and faster methods, such as DOM 2 Traversal TreeWalker, which is more efficient than recursively searching the childNodes collection.
Suppose you need to create a table of contents in an HTML web page based on H2-H4 elements. Title elements can appear in many places in HTML, so there is no way to get them using recursive functions. Traditional DOM may use the following method:
If the web page has more than 2000 elements, this method will be very slow. If XPath is supported, a much faster method can be used because the XPath query engine can be better optimized than the JavaScript that needs to be interpreted. In some cases, XPath can be more than 2 orders of magnitude faster. The following code performs the same function as the above, but uses XPath and is therefore faster:
下面版本代码融合上述两种方法;在支持 XPath 的地方使用快速方法,在不支持时使用传统 DOM 方法:
if( document.evaluate ) { var headings = document.evaluate( '//h2|//h3|//h4', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null ); var oneheading; while( oneheading = headings.iterateNext() ) { ... } } else { var allElements = document.getElementsByTagName('*'); for( var i = 0; i < allElements.length; i++ ) { if( allElements[i].tagName.match(/^h[2-4]$/i) ) { ... } } }
有些 DOM 集合是实时的,如果在你的脚本遍历列表时相关元素产生变化,则此集合会立刻变化而不需要等待脚本遍历结束。childNodes 集合和 getElementsByTagName 返回的节点列表都是这样的实时集合。
如果在遍历这样的集合的同时向其中添加元素,则可能会遇到无限循环,因为你不停的向列表中添加元素,永远也不会碰到列表结束。这不是唯一的问题。为提高性能,可能会对这些集合做出优化,如记住其长度、记住脚本中上一个访问元素序号,这样在你访问下一个元素时可快速定位。
如果你此时修改 DOM 树,即使修改的元素不在此集合中,集合还是会重新搜索以查看是否有新元素。这样就无法记住上一个访问元素序号或记住集合长度,因为集合本身可能已经变了,这样就无法使用优化:
var allPara = document.getElementsByTagName('p'); for( var i = 0; i < allPara.length; i++ ) { allPara[i].appendChild(document.createTextNode(i)); }
下面的代码在 Opera 和 Internet Explorer 等主流浏览器中比上面代码快10倍以上。先创建一个要修改元素的静态列表,然后遍历静态列表并作出相应修改,而不是遍历 getElementsByTagName 返回的节点列表:
var allPara = document.getElementsByTagName('p'); var collectTemp = []; for( var i = 0; i < allPara.length; i++ ) { collectTemp[collectTemp.length] = allPara[i]; } for( i = 0; i < collectTemp.length; i++ ) { collectTemp[i].appendChild(document.createTextNode(i)); } collectTemp = null;
有些 DOM 返回值无法缓存,每次调用时都会重新调用函数。如 getElementById 方法。下面是一个低效率代码的例子:
此代码为定位同一个对象调用了四次 getElementById 方法。下面的代码只调用了一次并将结果保存在变量中,单看这一个操作可能比上面单个操作要略慢,因为需要执行赋值语句。但后面不再需要调用 getElementById 方法!下面的代码比上面的代码要快5-10倍:
如果文档访问过其他文档中的节点或对象,在脚本结束后避免保留这些引用。如果在全局变量或对象属性中保存过这些引用,通过设置为 null 清除之或者直接删除之。
原因是另一个文档被销毁后,如弹出窗口被关闭,尽管那个文档已经不再了,所有对那个文档中对象的引用都会在内存中保存整个 DOM 树和脚本环境。这也适用那些包含在frame,内联 frame,或 OBJECT 元素中的网页。.
Opera (and many other browsers) uses fast history browsing by default. When the user clicks back or forward, the status of the current page and the script in the page will be recorded. When the user returns to the previous page, the previous page will be displayed immediately, as if they had never left this page. There is no need to reload the page or re-initialize. The script continues to run, and the DOM is exactly the same as before leaving this page. This is more responsive to users and will result in better performance for slower-loading web applications.
Although Opera provides ways for developers to control this behavior, it is best to keep fast history browsing functionality as much as possible. This means it's best to avoid actions that would interfere with this functionality, including disabling form controls when submitting a form or fade-out effects that make page content transparent or invisible.
The simple solution is to use the onunload listener to reset the fade-out effect or re-enable the form control. Note that for some browsers, such as Firefox and Safari, adding a listener for the unload event will disable historical browsing. And disabling the submit button in Opera disables historical browsing.
This technique may not be suitable for every project, but it can significantly reduce the amount of data downloaded from the server and avoid the overhead of destroying and creating script environments when reloading the page. Start by loading the page normally, then use XMLHttpRequest to download a minimal amount of new content. This way the JavaScript environment will always exist.
Note that this method may also cause problems. First this method completely destroys historical browsing. Although this problem can be solved by storing information in an inline frame, this obviously defeats the original purpose of using XMLHttpRequest. So use it sparingly and only when you don't need to go back to previous content. This approach also affects the use of assistive devices because it will not be aware that the DOM has been changed, so it is best to use XMLHttpRequest where it will not cause problems.
This trick will also fail if JavaScript is not available or does not support XMLHttpRequest. The easiest way to avoid this problem is to use normal links to point to the new page. Add an event handler to detect whether the link is activated. The handler can detect whether XMLHttpRequest is supported, and if so, load the new data and prevent the link's default action. After loading the new data and replacing part of the page with it, the request object can be destroyed and the memory resources can be reclaimed by the garbage collector.
[wW]*$/g , '' ); document.getElementById('container'). innerHTML = useResponse; request.onreadystatechange = null; request = null; }; request.open( 'GET', this.href, true ); request.send(null); return false; }
Loading and processing scripts takes time, but some scripts are never used after loading. Loading such scripts wastes time and resources and affects current script execution, so it is best not to reference such unused scripts. You can determine which scripts are needed by simply loading the script, and create script elements only for the scripts you need later.
Theoretically, this loading script can be added to the DOM by creating a SCRIPT element after the page loads. This will work fine in all major browsers, but it may place more demands on the browser than the script itself to load. And the script may be needed before the page is loaded, so it is best to create a script tag through document.write
during the page loading process. Remember to escape the '/' character to prevent terminating the current script:
location.replace()
Control history itemsSometimes it is necessary to modify the page address through script. A common method is to assign a new address to location.href
. This will add a new history item and load a new page just like opening a new link.
Sometimes you don't want to add new history items because the user doesn't need to go back to previous pages. This is useful in devices with limited memory resources. Recover the memory used by the current page by replacing historical items. This can be achieved through the location.replace()
method.
Note that the page is still saved in the cache and still takes up memory, but it is much less than what is saved in the history.