Books - High Performance JavaScript

Yahoo! PRESS から出版されている High Perfomance JavaScript を読んだメモ書きです。

Chapter 1: Loading and Execution

JavaScript のコードを読み込むときは、ブラウザは <script> タグの中で DOM を書き換えるかどうかを予め判断できないので、別スレッドにはできない。 IE だと <script> タグに "deferred" 属性を付けることで読み込みを遅延させられる。 loadScript(url, callback) 関数だけを先に送り出してあげて、アプリケーションコードの本体は後から送信するようにする。YUI().use() など。

Chapter 2: Data Access

Literal values, Variables, Array items, Object members へのアクセスを比較する。局所変数を使うように心がける。

ECMA-262 第三版で [[Scope]] が定義されている。 スコープチェーンには Activation object と Global object がある。Activation object には "arguments" という局所変数があり、引数などを保持している。 setTimeout() で呼び出しを繰り返すには arguments.callee を使える。 with, try-catch, eval を使うとスコープチェーンが長くなるため、データアクセスが遅くなる傾向にある。Closure を使うとややこしいのでよく考える。 Object__proto__ プロパティを持っていてプロトタイプチェーンを構成する。__proto__ を辿るかどうかは hasOwnProperty() で確認できる。 ArrayObject のメンバーへのアクセスは遅いので、複数回使用する場合は局所変数にキャッシュする。window.location.href など。

スクリプトの実行速度を計測する場合は Firebug のプロファイラを使う。console.time() など。 自前で簡易版を実装する場合は var start = +new Date() で数値を比較すれば良い。

Chapter 3: DOM Scripting

ひとくちに「JavaScript」といっても、DOM と ECMA がある。双方の仕様を行き来するとコストがかかる。 Windows の場合は "jscript.dll" と "mshtml.dll" で別々のライブラリになっている。 たとえば、表 <table> をつくるときには三つの方法がある。

  1. HTML 文字列を生成して、innerHTML を使う。
  2. createElement でノードを生成してから親ノードに append する。
  3. createElement して、ループの中でそのノードを cloneNode して使う。

HTML の document オブジェクトは画像、リンク、フォームをプロパティに持っている。これらは HTML Collections と呼ばれるもので、Array とは異なる。

  • document.images
  • document.links
  • document.forms

DOM で子要素にアクセスするときに childNodes プロパティを使うと、空白やコメントも含まれてしまうので自力でフィルタリングしないといけない。 children プロパティを使える場合はそれを使った方が良い。 ノードを選択する場合は querySelectorAll() を使うと良い。NodeList を返してくれる。 NodeList は HTML Collections とは異なり、"live" ではない。"live" な要素は reflow を考えなくてはいけない。 reflow の頻度を抑えるために、オペレーションをキューイングしておく。スタイルやサイズを変更するときは注意する。

Chapter 4: Algorithms and Flow Control

for-in はオブジェクトの探索でプロトタイプチェーンを辿ってしまうので遅くなる。意図しない動作だと誤解する人も出てしまう。

配列のループをまわすときは、インクリメンタルに扱うよりデクリメンタルに扱う方が、条件文での比較ステップが少ないから高速化できる。 ループで Duff's device を使う、というやり方もある。 ECMA-262 の第四版で Array の forEach が定義されている。各種ライブラリでも類似の機能を提供しているが、引数が少しずつ異なる。

複数の条件分岐がある場合は、if-elseswitch よりも Array からルックアップした方が速い。要素数が7個くらいから差が出てくる。 ブラウザごとに call stack の大きさが異なるので再帰呼び出しには気をつける。memoization でキャッシュできる場合は活用する。

Chapter 5: Strings and Regular Expressions

文字列を連結する方法は四種類 (+, +=, array.join(), string.concat()) ある。普通は "+" を使うが、IE の 7 より前のバージョンでは array.join() を使う。コピーが発生しないため。

正規表現でアトミックなグループを作るときは (?=(pattern to make atomic))\1 のようにする。 バックトラックの可能性を抑制できるので、マッチしないときに遅くなりにくい。 単純に後ろから探した方が高速な場合は正規表現を使わない。length をデクリメントして探すのがベター。 文字列を trim するときに安直な正規表現を二つつなげるのはダメ。よく考える。ネイティブ実装を使えるならそれがベスト。

ダメ:string.replace(^\s+/, "").replace(/\s+$/, "")

noncapturing group を使うと速くなる処理系もある。(\s+\S+)(?:\s+\S+)

Without a firm understanding of backtracking, you won't be able to anticipate and identify backtracking-related problems.

長い文字列、部分的に一致する部分がある文字列、完全に一致しない文字列を確認する。 これらの意味と使い方を確認する

  • (?:{some regular expressions})
  • (?=({some regular expressions}))\1
  • /<p>.*<\/p>/i --> greedy quantifier
  • /<p>.*?<\/p>/i --> lazy quantifier

Chapter 6: Responsive Interfaces

ブラウザごとにスクリプトに対する実行制限の機構が組み込まれている。call stack size limit と long-running script limit。 スクリプトの実行時間を 100ms 以下にしないと、ユーザは自分が切断された状態にいるのではないかと感じる。Robert Miller や Jakob Nielsen の研究による。 ループ処理に長時間かかってしまう場合は、setTimeout でタイマーを生成する。 ループの中の処理が複雑な場合と、ループの回数が巨大な場合がある。 プロセスに同期が不要だったり、データの順番に意味がない場合は非同期処理させた方が良い。 再帰処理は setTimeout(arguments.callee, 25); でも実現できる。タイムアウト時間は 25 が良いらしい。研究成果か経験則。

関数オブジェクトを配列にして、非同期にタスクを実行させることもできる。このときは apply() メソッドを使う。 時間を計測しながら do-while を実行することで、一回のタイマーで複数回の処理を実行できる。

Chapter 7: Ajax

データを受け取る場合と投げる場合。

  • Retrieval: XHR, Dynamic script tag insertion, iframes, comet, MXHR (Multipart XHR)
  • Post: XHR, Beacons (Beacons では Image オブジェクトの src プロパティでサーバサイドのスクリプトを呼び出す。レスポンスはいらない。)

レスポンスを XML にする場合は、クライアント側で三種類のコード (XPath, IE8, 古いもの) を準備する、 子要素を入れ子にするより、属性で持たせた方がパースが簡単なので速い可能性が高い。 JSON は、名前付きの辞書をパースするより、Array をパースして手元でループを回して辞書にする方が速い。 evalJSON.parse に与えるデータを極力小さくするのがポイント。

HTTP のヘッダで Cache を指定するなり、ローカルに保存するなりで、とにかく無駄な HTTP コネクションを使わない。 ライブラリでラップされてると readyState にアクセスできないので、MXHR を使うときは厄介かも。

Chapter 8: Programming Practices

work avoidance:

  • don't do work that isn't required
  • don't repeat work that has already been completed

Lazy Loading と Conditional Advance Loading がある。 Integer オブジェクトの toString() に 2 を渡すとバイナリ表現を得られる。ビット演算は高速。 表を作るときに odd/even を計算する場合は剰余を計算するよりもビット演算の AND の方が高速。

Chapter 9: Building and Deploying High-Performance JavaScript Applications

YUI Compressor でデータ量を4割くらい圧縮できる。 Closure Compiler だとさらに圧縮できるが、出力結果が入力結果と等価にならないこともあるので動作確認が大変。デバッグには Closure Inspector を使うしかない。ブラウザ依存の問題になるともう大変。

Apache の設定で一年間キャッシュさせる。 一年まで、というのは RFC 2616 の Section 14.21 で規定されている。

ExpiresActive on
ExpiresDefault "access plus 1 year"

Chapter 10: Tools

Y.Profiler はレポートを収集するときにフィルターを定義できるので、値が0のデータをスキップできる。 無名関数だとレポートを出力したときに何のコトか分からないので、何らかの方法で名前を付ける。 Safari の場合は関数オブジェクトの "displayName" プロパティに文字列を指定しておくと、プロファイラに表示される。

まとめ

Although conventional wisdom says to minimize the number of HTTP requests, deferring scripts whenever possible allows the page to render more quickly, providing users with a better overall experience.