読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

JavaScriptパフォーマンスベストプラクティスその4

Nokia Developerより

Document Object Model (DOM) の難解さ

ソース: Efficient JavaScript - DOM

DOMパフォーマンスの遅さは以下の3つの主な原因に遡ることができる:

大規模なDOM操作 DOM APIの大量の使用は遅さのよく知られた原因である。
スクリプトが多すぎるリフローや再描画を引き起こす DOM操作の結果として、レイアウトのリフローと再描画はとても高価である。
DOMの中にノードを置く遅いやり方 DOMに望みのノードを置くことはDOMがサイズ変更可能だったり複雑だったりすると、潜在的なボトルネックとなる。

Slow DOM performance can be traced back into the following three main causes:

Extensive DOM manipulation Extensive use of DOM API is a well-known cause of slowness.
Script triggers too many reflows or repaints As a consequence of DOM manipulation, reflowing the layout and repainting are very expensive.
Slow approach to locating nodes in the DOM Locating a desired node(s) in the DOM is potential bottleneck if the DOM is sizable and/or complex.
DOMのサイズを最小化すること
  • DOMのサイズはリフロー、横断、DOM操作など関連したすべての操作を遅くする。
  • プログラムを速くするもっとも効果のある方法はnを小さくすることである。それはDOMがいつも可能な限り小さくあるべきということを意味する。
  • nを最小化すること。ページ内の要素の数を以下によって追跡できる:
    • {{{1}}}
    • {{{1}}}
  • The size of DOM slows down all the operation related to it such as reflowing, traversal and DOM manipulation.
  • The most effective way to make programs faster is to make n smaller means that the DOM should be as small as possible at all times.
  • Minimize n, you can track the number of elements in a page by:
    • {{{1}}}
    • {{{1}}}
再利用のためドキュメント断片テンプレートを使うこと
  • 動的にDOMの中に要素を挿入したり更新したりすることは高価だ。これに取り組む効率的な方法は、ダイヤログや他のUIウィジェットのようなDOMの再利用可能な部品を複製して再利用できるHTMLテンプレートを活用することだ。
  • 実際には、このアプローチは、生のDOMに触れることなく、JavaScriptですべてのノードを修正、複製、追加し、完成したドキュメント部分をDOMに一度に追加したりすることだ。DOM APIを使ってこれをすることができるし、代わりに文字列テンプレートに基づいて追加するHTML断片の文字列表現を構成して、それをDOMにinnerHTML割り当てを一度使って挿入することもできる。どちらの場合も、レンダリングエンジンはレイアウトのリフローや再描画を複数回する必要はない。次に、この振る舞いを達成するためのテクニックをいくつか紹介する。
  • Dynamically inserting and updating elements into the DOM is expensive. An efficient way to tackle this is to use HTML templates which can be cloned and re-used for re-usable parts of the DOM such as dialogs and other UI widgets.
  • In practice the approach is to modify, clone and append all nodes in JavaScript without touching the live DOM and append the completed document fragment to the DOM at once. One can do this with DOM API or alternatively construct a string representation of the HTML fragment to append based on a string template and push that into the DOM with a one innerHTML assignment. On both cases the rendering engine does not have to reflow and repaint the layout multiple times. Next we introduce some techniques to achieve this behavior.
リフローと再描画の回数を最小化すること
再描画 再描画はドキュメントのレイアウトを変えることなく何かの表示/非表示したりすると起こる。例えば、要素に外枠を追加すると、その可視性が変わるので背景色が変わる。再描画は高価な操作だ。 (描画イベントデモ)
リフロー リフローはレイアウトに影響が出るようなやり方でDOMが操作された時はいつでも起こる。例えば、レイアウトに影響するようにスタイルを変えたり、classNameプロパティを変えたり、ブラウザのウィンドウサイズが変わったり。一旦要素がリフローを必要とすると、子要素もリフローされ、要素の後に現れるどんな要素も対象となる。最後に、すべてが再描画される。リフローは再描画よりさらに高価な操作だ。多くの場合、リフローはページ全体を再レイアウトするのと同じである。(リフローデモビデオ)
Repaint Repainting happens when something is made visible or hidden without altering the layout of the document. For example if an outline is added to an element, its background color is changed of its visibility is changed. Repainting is an expensive operation (paint events demo).
Reflow Reflow happens whenever the DOM is manipulated in a way it affects the layout. For example, style is changed to affect the layout, className property is changed or browser window size is changed. Once an element needs to be reflown, its children will also be reflown and any elements appearing after the element in the DOM. Finally, everything is repainted. Reflows are even more expensive operations, than repainting. In many cases reflowing is comparable to layout out the entire page again (reflow demo videos).
  • リフローの引き金となる操作は頻繁に使うべきでない。
  • テーブル要素のリフローはblock displayの等価な要素をリフローするよりも高価。
  • absoluteやfixedで位置された要素はメインのドキュメントレイアウトに影響しない。だから、それらはメインのドキュメントのリフローの引き金にならないので、それらのリフローは安価。これはアニメーションを必要とする要素への推奨アプローチだ。
  • DOM修正はリフローの引き金となる。これは、新しい要素を加えたり、テキストノードの値を変えたり、要素の属性やプロパティを加えたりするような操作はリフローを引き起こすことを意味する。
  • この制限に打ち勝つよい戦略は次に詳しく述べる。
  • もっと知るには
  • ツール
    • XUL Profiler
    • Mac OS X Quartz Debug (in developer tools part of every OS X) Autoflush drawing setting illustrates how the page is reflown in step-by-step detail. (more)
  • Operation that trigger reflows should be used sparsely.
  • Reflowing a table element is more expensive that reflowing equivalent element with block display.
  • Elements that are positioned absolutely or fixed do not affect the main document layout, so their reflowing is cheaper as they do not trigger main document reflowing. This is recommended approach for element that need to be animated.
  • DOM modifications trigger reflow. This means that operations such as adding new elements, changing the value of text nodes or adding element attributes and their properties cause reflow.
  • Good strategies to overcome this limitation are elaborated next.
  • Further reading
    • Repaint and reflow at Opera Developer Network
    • Notes on HTML Reflow - more detailed information on the reflow process (archived)
    • Reflows & Repaints: CSS Performance making your JavaScript slow
    • Go With The Reflow
    • Rendering: repaint, reflow/relayout, restyle
  • Tools
    • XUL Profiler
    • Mac OS X Quartz Debug (in developer tools part of every OS X) Autoflush drawing setting illustrates how the page is reflown in step-by-step detail. (more)
createDocumentFragment()を使うこと
  • DOMDocumentFragmentの中で複数の変更を行い、断片をDOMに一回の操作で加えること。これは1度だけリフローの引き金になる。
  • Make multiple changes in a DOMDocumentFragment and add the fragment into the DOM in a single operation. This triggers only one reflow.

遅い:

var list = ['foo', 'bar', 'baz'],
    elem,
    contents;
for (var i = 0; i < list.length; i++) {
    elem = document.createElement('div');
    content = document.createTextNode(list[i]);
    elem.appendChild(content);
    document.body.appendChild(elem);
}

速い:

var fragment = document.createDocumentFragment(),
    list = ['foo', 'bar', 'baz'],
    elem,
    contents;
for (var i = 0; i < list.length; i++) {
    elem = document.createElement('div');
    content = document.createTextNode(list[i]);
    fragment.appendChild(content);
}
document.body.appendChild(fragment);
cloneNode()を使うこと
  • フォーム要素やイベントハンドラを含まない要素上で作業しているのであれば、変更する要素をクローンし、変更がすべて終わった後で、それを入れ替えることができる。それは1回だけリフローを起こす。
  • 上の遅いアプローチのより速い代替を以下に示す。
  • If you're not working on elements that do not contain form elements or event handlers, you can clone the element to modify and swap it in place after all the changes have been done resulting in a one reflow only.
  • A faster alternative to above slow approach is presented below.

速い:

var orig = document.getElementById('container'),
    clone = orig.cloneNode(true),
    list = ['foo', 'bar', 'baz'],
    elem,
    contents;
clone.setAttribute('width', '50%');
for (var i = 0; i < list.length; i++) {
    elem = document.createElement('div');
    content = document.createTextNode(list[i]);
    elem.appendChild(content);
    clone.appendChild(elem);
}
original.parentNode.replaceChild(clone, original);
HTMLテンプレートとinnerHTMLを使うこと
  • テンプレートシステムを実装する1つの方法は、データモデルとして振る舞う軽量JavaScriptオブジェクトに基づいたテンプレートコンテンツを使うことだ。データモデルはJSONシリアル化として、例えばS60 WRTのsetPreferenceForKey() メモリやHTTPクッキーやXHRでサーバサイドに保持することができる。
  • One way to implement a templating system is to populate template content based on a light-weight JavaScript object acting as a date model. The data model may be persisted as JSON serialization into e.g. setPreferenceForKey() store in S60 WRT, in a HTTP cookie or XHR'd to the server-side.
<nowiki>
var model = { title: 'My Test Page'; },
    template = [];
template.push('<h1>' + model.title + '<h1>');
template.push('<div>Another Test Element<div>');
document.getElementById(containerId).innerHTML = template.join(''); 
// alternatively you can use concat() -- see string concatenation test results
</nowiki>
非表示要素を修正すること
  • 要素のdisplayプロパティがnoneに設定されているなら、それは再描画されない。
  • displayをnoneに設定し、変更し、blockに設定するとリフローが2回だけ起こる。
  • If the display property of an element is set to none it will not be repainted.
  • By setting display to none, do the modifications and then set it to block causes only two reflows

遅い:

var subElem = document.createElement('div'),
    elem = document.getElementById('animated');
elem.appendChild(subElem);
elem.style.width = '320px';

速い:

var subElem = document.createElement('div'),
    elem = document.getElementById('animated');
elem.style.display = 'none';
elem.appendChild(subElem);
elem.style.width = '320px';
elem.style.display = 'block';
要素のサイズや位置を決定する演算の使用を最小にすること
  • getComputedStyle, offsetWidth, scrollWidth, clientWidthプロパティを使って要素のサイズや位置を決定するとリフローが起こる。
  • 測定を繰り返し行っているなら、それらを一度だけすることを検討すること。
  • Dave Hyattによると、この問題はWebkitでの遅さの主な原因である。
  • Determining dimensions or location of elements via getComputedStyle, offsetWidth, scrollWidth and clientWidth properties will force reflow.
  • If you take the measurements repeatedly, consider taking them only once.
  • This issue is the main cause of slowness in WebKit according to Dave Hyatt

遅い:

var elem = document.getElementById('animated');
elem.style.fontSize = (elem.offsetWidth / 10) + 'px';
elem.firstChild.style.marginleft = (elem.offsetWidth / 20) + 'px';

速い:

var elem = document.getElementById('animated'),
    elemWidth = elem.offsetWidth;
elem.style.fontSize = (elemWidth / 10) + 'px';
elem.firstChild.style.marginleft = (elemWidth / 20) + 'px';
複数の事前定義されたスタイルの変更をclassNameを使って一度に行うこと
  • DOM操作に関連して、いくつかのスタイルの変更を同時にすることができる。
  • そうではなく、スタイルを1つ1つ変えると、複数のリフローと再描画が引き起こされる可能性がある。
  • As with DOM manipulation, several style changes can be done at the same time.
  • Instead if you set the styles one by one, multiple reflows and repaints can be triggered.

遅い:

var elem = document.getElementById('styled');
elem.style.background = 'blue';
elem.style.color = 'white';


速い:

<code html4strict>
<style type="text/css">
div { background: white; color: black; }
div.active { background: blue; color: white; }
</style>
...
var elem = document.getElementById('styled').className = 'active';
動的なスタイルの変更をsetAttributeを使って1度に行うこと
  • 動的なアニメーションには、事前定義されたスタイルを使うことはうまくいかない。この場合、setAttributeオブジェクトを使うことができる。(IEではstyle.cssText propertyを使う)
  • For dynamic animation, using predefined styles does not work. In this case setAttribute object can be used (for IE, use style.cssText property)

速い:

var elem = document.getElementById('styled');
elemStyle = 'background: blue; color: white;';
elem.setAttribute('style', elemStyle);
CSSクラス名 vs. スタイルプロパティ変更

要素のクラス名の変更は要素を動的に変更するためにJavaScriptを使う素敵な方法だ。パフォーマンスはブラウザによって様々だが、一般に要素の見え方をJavaScriptのスタイル属性を介して直接変更することは、要素のクラス名を変更するより速い。

Changing the class name of an element is a nice way to use JavaScript to dynamically change elements. Performance varies from browser to browser, but generally it is faster to change an element's visual appearance directly via the Javascript style attribute, rather than to change a class name on that element.

遅い:

div.active { border: 1px solid red; }

速い: (1つの要素に対して):

var container = document.getElementById('container');
container.style.border = '1px solid red';

上の方法は特定の数の項目を変更する時にはもっと効率的に見える。しかし、時々、1つのクラス名変更が有効だ。例えば、与えられたコンテナの下のすべての要素を変更する必要があるなら、影響の出る要素を持つ親コンテナのクラス名を変更し、CSSにベストを尽くさせるのがより効率的だ。

The above method appears to be more efficient when changing a specific number of items. Sometimes a single class name change is effective however. If you need to change all elements under a given container for example, it is more efficient to change the class name of a parent container which holds the affected elements and let CSS do what it does best.

速い (コンテナ内の複数の子要素を変更する必要がある場合):

// コンテナのクラス名を変えることで、子どものdiv要素すべてが更新される。
#container.active div { border: 1px solid red; }

目下の特定な場合に依存して、(外部に定義されたCSSの是非の分離にあまり多く犠牲にすることなく)最高のパフォーマンスが得られる方法を使うべきだ。

Depending on the specific case at hand you should use the method which gives you the best performance (without sacrificing too much of the separation of concerns benefits of externally defined CSS).

ソース:

たくさんのノードを横断することを避けること
  • 常にノードの可能な最少数で探索を狭めるようにDOMの組み込みメソッドやコレクションを使うようにすること
  • 手動で再帰的にDOM中を歩くことを可能な限り避けようとすること
  • Always try to use inbuilt methods and collections of the DOM to narrow down the search to smallest number of nodes possible.
  • Try to avoid manually recursively stepping through the DOM as much as possible.

遅い:

var elements = document.getElementsByTagName('*'); // searches every element, slow
for (i = 0; i < elements.length; i++) {
    if (element[i].hasAttribute('selected')) { // continues even through element was found
        ...
    }
}

速い:

var elements = document.getElementById('parent').childNodes; // we know the element is a child of parent
for (i = 0; i < elements.length; i++) {
    if (element[i].nodeType == 1 && element[i].hasAttribute('selected') { // first test for valid node type
        ...
        break; // break out of the loop if we found what we were looking for
    }
}
横断中の変更を避けること
  • getElementsByTagName()が返すchildNodesやNodeListは生だ。これは、これらのコレクションが最初に完了する実行を待たずに変更される可能性を意味する。
  • 横断中にコレクションに新しい要素が追加されると、無限ループが起こるかもしれない。
  • コレクション自身の外側に新しい要素が追加されるときでさえ、コレクションは潜在的な新しいエンティティを探さなければいけない。このせいで、再計算が必要となる最後の位置や長さを記憶できない。
  • childNodes and NodeList returned by getElementsByTagName() are live. This means that these collections may change without waiting for the execution to finish first.
  • If new elements are added to the collections while they are traversed, an infinite loop may occur.
  • If new elements are added even outside of collection itself, the collection must look for potential new entries. Due to this it cannot remember its last position or length which need to be recalculated.

遅い:

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

速い:

var elems = document.getElementsByTagName('div'),
    temp = [];
for (var i = 0; i < elems.length; i++) {
    temp[i] = elems[i]; // first a build static list of elements to modify
}
for (var i = 0; i < temp.length; i++) {
    temp[i].appendChild(document.createTextNode(i)); // perform modifications on static list instead of live NodeList
}
temp = null;
DOM値を変数にキャッシュすること
  • 頻繁にアクセスするDOMの値を変数にキャッシュすること。
  • Cache frequently accessed DOM values into variables.

遅い:

document.getElementById('elem').propertyOne = 'value of first property';
document.getElementById('elem').propertyTwo = 'value of second property';
document.getElementById('elem').propertyThree = 'value of third property';

速い:

>|javascript|
var elem = document.getElementById('elem').propertyOne = 'value of first property';
elem.propertyTwo = 'value of second property';
elem.propertyThree = 'value of third property'
|

閉じたドキュメントへの参照を取り除くこと

  • フレームやiframeやオブジェクトへの参照がグローバル変数やプロパティに保存んされているなら、それにnullを設定するか削除してクリアすること。
  • 例えば、閉じて、グローバル変数への参照を持つポップアップウィンドウは、もしも手動で削除されなければ、ドキュメントそのものはもはやロードされないけれどもメモリに保たれる。
  • If a reference to frame, iframe or object is stored in a global variable or a property, clear it by setting it to null or deleting it.
  • For example, a popup window that is closed and has a reference to global variable, will be kept in memory although the document itself is no longer loaded, if it is not deleted manually.

遅い:

var frame = parent.frames['frameId'].document,
    container = frame.getElementById('contentId'),
    content = frame.createElement('div');
content.appendChild(frame.createTextNode('Some content'));
container.appendChild(content);

速い:

var frame = parent.frames['frameId'].document,
    container = frame.getElementById('contentId'),
    content = frame.createElement('div');
content.appendChild(frame.createTextNode('Some content'));
container.appendChild(content);
// nullify references to frame
frame = null;
container = null;
content = null;