作者 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
非効率であるだけでなく、ほとんどの場合、使用する必要がまったくありません。多くの場合、情報が文字列形式で提供されるため eval が使用されますが、開発者はこの情報を eval だけが使用できると誤って信じています。次の例は典型的なエラーです。
以下のコードは、 eval
を使用せずにまったく同じ機能を実行します。
後者は、Opera 9、Firefox、Internet Explorer では前者より 95%、Safari では 85% 高速です。 (この比較には関数の呼び出し時間が含まれていないことに注意してください。)
以下は一般的な Function
コンストラクターの使用法です:
以下のコードは Function
コンストラクターを使用していませんが、匿名関数を作成することで同じ機能を提供します:
with
の使用を避ける
非常に非効率的です。 with
構造体は、変数を使用するときにスクリプト エンジンが検索するための別のスコープを作成します。これ自体はパフォーマンスにわずかな影響を与えるだけです。しかし、深刻なのは、このスコープの内容はコンパイル時にはわからないため、コンパイラは他のスコープ (関数によって生成されたスコープなど) のように最適化することができないことです。 with
次のコードを考えてみましょう:
<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
ステートメントの先頭で作成され、最後に破棄されます。 catch
catch
この関数は特殊であり、実行時に動的に作成および破棄されるため、一部のブラウザでは効率的に処理されません。 catch ステートメントを重要なループに配置すると、パフォーマンスに大きな影響を与えます。
可能であれば、頻繁に呼び出されないスクリプトで例外を処理するか、アクションがサポートされているかどうかを確認して例外を回避します。次の例では、必要なプロパティが存在しない場合、ループ ステートメントで多くの例外がスローされます:
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); }
下面的代码和上面相同,但只创建了一个对象,因此其效率更高:
コードがリテラル値のメソッドを頻繁に呼び出す場合は、上記の例のようなオブジェクトを作成することを検討する必要があります。
この記事のヒントのほとんどはすべてのブラウザで機能しますが、このヒントは Opera に固有のものであることに注意してください。この最適化のヒントは、Internet Explorer と Firefox では Opera ほど改善されません。
for-in
for-in
は、特に単純な for
ループの方が適切な場合に誤用されることがよくあります。 for-in
このループでは、スクリプト エンジンがすべての列挙可能なプロパティのリストを作成し、重複をチェックする必要があります。
場合によっては、列挙可能なプロパティがスクリプトに認識されます。この時点で、特に配列などで連続した数値列挙を使用する場合、単純な for
ループですべてのプロパティを反復処理できます。
次は間違っていますfor-in
ループの使用法:
for
ループは間違いなくより効率的になります:
演算子は結果を変数に保存しません。新しい文字列オブジェクトを作成し、その結果をこのオブジェクトに割り当てます。おそらく、新しいオブジェクトは変数に割り当てられます。以下は一般的な文字列結合ステートメントです:
変数とマージし、最後に結果を a に割り当てます。以下のコードは 2 つの別々のコマンドを使用しますが、毎回 a に直接代入するため、一時的な文字列オブジェクトを作成する必要はありません。その結果、ほとんどのブラウザでは、後者の方が前者より 20% 高速で、消費メモリも少なくなります。
メソッドが挙げられますが、これは配列の末尾に直接代入するよりも効率が低くなります。もう 1 つの例は、 Math オブジェクト メソッドです。ほとんどの場合、単純な数学演算子の方が効率的で適切です。
setTimeout()
および setInterval()
メソッドは のように実行されますが、その非効率性は setTimeout()
と同じになります。 setInterval()
eval
しかし、これらのメソッドは最初のパラメータとして関数を受け入れることもできます。この関数はしばらくしてから呼び出されますが、この関数はコンパイル時に解釈して最適化できるため、パフォーマンスが向上します。文字列をパラメーターとして使用する一般的な例は次のとおりです。 eval
eval
第一个语句可以直接传递函数名。第二个语句中,可以使用匿名函数封装代码:
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 需要使用第二种形式,不支持第一种形式。最简单的方式是测试看是否支持第一种形式,如果支持就使用,如果不支持则使用第二种形式。
開発者としては、当然のことながら、通常はより短い時間間隔またはより小さな変更を使用して、アニメーションをできるだけスムーズに実行したいと考えます。たとえば、アニメーションを 10 ミリ秒ごとに更新するか、毎回 1 ピクセルずつ移動します。このアニメーションは、デスクトップ コンピュータまたは一部のブラウザでは完全に動作する可能性があります。ただし、10 ミリ秒間隔は、ブラウザが CPU を 100% 使用して達成できる最小値である可能性があります。一部のブラウザではそれさえできません。ほとんどのブラウザにとって、1 秒あたり 100 回のリフローを要求するのは簡単ではありません。低パフォーマンスのコンピュータやその他のデバイスではこの速度を達成できない場合があり、これらのデバイスではアニメーションが非常に遅くなったり、応答しなくなったりすることがあります。
したがって、当面は開発者のプライドを脇に置き、速度のために滑らかさを犠牲にすることが最善です。時間間隔を 50 ミリ秒に変更するか、アニメーション ステップを 5 ピクセルに設定すると、コンピューティング リソースの消費が減り、低パフォーマンスのデバイスでも正常に実行されます。
ノードを検索する必要がある場合は、DOM 組み込みメソッドとコレクションを使用して検索範囲を狭めてみてください。特定の属性を含む要素を見つけたい場合は、次のコードを使用できます:
XPath のような高度なテクニックについて聞いたことがなくても、上記のコードには速度を低下させる 2 つの問題があることがわかります。まず、検索を絞り込むのではなく、すべての要素を検索します。次に、必要な要素が見つかった後でも検索が続行されます。探している要素が inhere という ID を持つ div 内にあることがわかっている場合は、次のコードを使用するのが最善です:
探している要素が div の直接の子ノードであることがわかっている場合は、次のコードの方が高速です:
基本的な考え方は、DOM ノードを 1 つずつ確認することを避けようとすることです。 DOM には、DOM 2 Traversal TreeWalker など、より優れた高速メソッドが多数あり、childNodes コレクションを再帰的に検索するよりも効率的です。
H2 ~ H4 要素に基づいて HTML Web ページに目次を作成する必要があるとします。タイトル要素は HTML のさまざまな場所に出現する可能性があるため、再帰関数を使用してタイトル要素を取得する方法はありません。従来の DOM は次のメソッドを使用する場合があります:
Web ページに 2000 を超える要素がある場合、この方法は非常に遅くなります。 XPath がサポートされている場合は、XPath クエリ エンジンを解釈する必要がある JavaScript よりも最適化できるため、より高速な方法を使用できます。場合によっては、XPath の方が 2 桁以上高速になることがあります。次のコードは上記と同じ機能を実行しますが、XPath を使用するため高速です:
下面版本代码融合上述两种方法;在支持 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 (および他の多くのブラウザ) は、デフォルトで高速履歴参照を使用します。ユーザーが「戻る」または「進む」をクリックすると、現在のページのステータスとページ内のスクリプトが記録されます。ユーザーが前のページに戻ると、このページから離れなかったかのように、すぐに前のページが表示されます。ページをリロードしたり、再初期化したりする必要はありません。スクリプトは引き続き実行され、DOM はこのページを離れる前とまったく同じになります。これにより、ユーザーへの応答性が向上し、読み込みの遅い Web アプリケーションのパフォーマンスが向上します。
Opera は、開発者がこの動作を制御する 方法を提供しています が、可能な限り高速な履歴参照機能を維持することが最善です。これは、フォーム送信時のフォーム コントロールの無効化や、ページ コンテンツを透明または非表示にするフェードアウト効果など、この機能を妨げる操作を避けることが最善であることを意味します。
簡単な解決策は、onunload リスナーを使用してフェードアウト効果をリセットするか、フォーム コントロールを再度有効にすることです。 Firefox や Safari などの一部のブラウザでは、unload イベントのリスナーを追加すると履歴の閲覧が無効になることに注意してください。また、Opera の送信ボタンを無効にすると、履歴の閲覧が無効になります。
この方法でも問題が発生する可能性があることに注意してください。まず、この方法では履歴の閲覧が完全に破壊されます。この問題はインライン フレームに情報を格納することで解決できますが、これでは明らかに XMLHttpRequest を使用する本来の目的が損なわれます。したがって、前のコンテンツに戻る必要がない場合にのみ、慎重に使用してください。このアプローチは、DOM が変更されたことを認識しないため、支援デバイスの使用にも影響します。そのため、問題が発生しない XMLHttpRequest を使用するのが最善です。
このトリックは、JavaScript が利用できない場合、または XMLHttpRequest をサポートしていない場合にも失敗します。この問題を回避する最も簡単な方法は、通常のリンクを使用して新しいページをポイントすることです。リンクがアクティブ化されているかどうかを検出するイベント ハンドラーを追加します。ハンドラーは、XMLHttpRequest がサポートされているかどうかを検出し、サポートされている場合は、新しいデータをロードして、リンクのデフォルトのアクションを防止します。新しいデータをロードし、ページの一部をそのデータで置き換えた後、リクエスト オブジェクトを破棄し、ガベージ コレクターによってメモリ リソースを再利用できます。
[wW]*$/g , '' ); '). innerHTML = useResponse; request.open('get', this.href, true );
SCRIPT 要素を動的に作成する
スクリプトの読み込みと処理には時間がかかりますが、一部のスクリプトは読み込み後に使用されません。このようなスクリプトをロードすると、時間とリソースが無駄になり、現在のスクリプトの実行に影響を与えるため、そのような未使用のスクリプトを参照しないことが最善です。スクリプトをロードするだけでどのスクリプトが必要かを判断し、後で必要なスクリプトのみにスクリプト要素を作成できます。
理論的には、ページの読み込み後に SCRIPT 要素を作成することで、この読み込みスクリプトを DOM に追加できます。これはすべての主要なブラウザで正常に機能しますが、スクリプト自体をロードするよりも多くの要求がブラウザに課される可能性があります。また、ページが読み込まれる前にスクリプトが必要になる場合があるため、ページの読み込みプロセス中に
を通じてスクリプト タグを作成するのが最善です。現在のスクリプトが終了しないように、必ず「/」文字をエスケープしてください: document.write
if( document.createElement && document.childNodes ) { document.write(''); window.XMLHttpRequest ) { document.write(''); }
location.replace()
履歴項目の制御場合によっては、スクリプトを使用してページのアドレスを変更する必要があります。一般的な方法は、 location.href
に新しいアドレスを割り当てることです。これにより、新しいリンクを開くのと同じように、新しい履歴項目が追加され、新しいページが読み込まれます。
ユーザーが前のページに戻る必要がないため、新しい履歴項目を追加したくない場合があります。これは、メモリ リソースが限られているデバイスで役立ちます。履歴アイテムを置き換えることにより、現在のページで使用されているメモリを回復します。これは、 location.replace()
メソッドを通じて実現できます。