もっと速くするために(文字列変換コストの抑止 + String.prototype 化による影響)

element.style.xxx にアクセスするには、"css-prop" ではなく "cssProp" の形でアクセスする必要があります。このとき行われる変換は camelize と呼ばれ、JavaScriptライブラリの多くは以下のような実装を行っているようです。
# camelize = らくだのこぶのようにする

function camelize(name) { // "text-align" -> "textAlign"
  return name.replace(/\-(\w)/g, function(m, c){
    return c.toUpperCase();
  });
}

その都度 camelize するのではなく事前生成しておけば速くなるのでしょうか?
比較対照が無いと寂しいので、代表的な JavaScript ライブラリのスコアも一緒に計ってみました。

テスト方法と、事前予想

  • job1 = Hash を事前生成しておいて Hash を検索
    • Hash 検索のみなので軽いハズ
    • Hash が Google Chrome(V8) の隠しクラスとして最適化されればすごく速くなりそう。
  • job2 = オンデマンドで生成
    • 最長ルートだと 組み込み関数x3 + 匿名関数の生成x1 が発生するからやや遅いハズ
  • job3 = Prototype-1.6.1 の String.prototype.camelize()
    • Prototype のコードは具沢山なので遅そう
  • job4 = jQuery-1.3.2 のコードの断片
    • 標準的なコード。Prototype-1.6.1 よりはきっと速いハズ
// ソースコードは概ねこんな感じ
var _TIDY_ALIAS1,
    _TIDY_ALIAS2 = _ie67 ? { "float":    "styleFloat",
                             cssFloat:   "styleFloat" }
                         : { "float":    "cssFloat",
                             styleFloat: "cssFloat" },
    _CAMELIZE = /-([a-z])/g,

mix(_TIDY_ALIAS1, _TIDY_ALIAS2, _uncamelize(document.getElementsByTagName("html")[0].style));

function tidy1() {
  return _TIDY_ALIAS1[this] || this;
}

function _uncamelize(props) {
  var rv = {}, i, v, IGNORE = /^[A-Z-]/, UNCAMELIZE = /([a-z])([A-Z])/g;

  for (i in props) {
    if (!IGNORE.test(i) &&
        typeof props[i] === "string") {
      v = i.replace(UNCAMELIZE, function(m, c, C) {
        return c + "-" + C.toLowerCase();
      });
      if (v !== i) {
        rv[v] = i; // { text-align: "textAlign" }
      }
    }
  }
  return rv;
}


function tidy2() {
  return _TIDY_ALIAS2[this] ||
          ((this.indexOf("-") < 1) ? this :
            this.replace(_CAMELIZE, function(m, c) {
              return c.toUpperCase();
            }));
}

String.prototype.tidy1 = tidy1;
String.prototype.tidy2 = tidy2;

var A = "background-color,text-align,-webkit-box-shadow,align,for,float".split(",");

function job1(A) {
  var v,i=0; 
  while((v=A[i++])){v.tidy1();} 
}
function job2(A) {
  var v,i=0;
  while((v=A[i++])){v.tidy2();} 
}
function job3(A) {
  var v,i=0;
  while((v=A[i++])){v.camelize();} // prototype.js
}
function job4(A) {
  var v,i=0;
  while((v=A[i++])){jQueryCamelCase(v);} // jQuery
  }
}
function jQueryCamelCase(name) {
  return name.replace(/\-(\w)/g, function(all, letter){
    return letter.toUpperCase();
  });
}

結果

job1 job2 速度差(job2 / job1) job3(prototype.js) job4(jQuery)
IE6 1360 2610 2倍 3922 2953
IE8 656 1000 1.5倍 1703 1188
Firefox2 625 1250 2倍 2109 1093
Firefox3 124 299 2.4倍 414 339
Firefox3.5.3(JIT) 138 253 1.8倍 366 232
Opera9.64 140 516 3.7倍 484 1047
Opera10.0 125 609 4.9倍 547 1109
Safari4 26 129 5倍 211 180
Chrome4 23 125 5.4倍 301 169

ほぼ予想通りのスコアが出ました。が… Opera で job3 >> job4 なのは、prototype.js正規表現を使わずに camelize を実装している(↓)からなのでしょうね。Opera正規表現エンジンはかなり遅いので。

  function camelize() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  }

実装方法による差があるのか(String.prototype vs 関数)

String.prototype.tidy1() と 関数の形で実装した functionTidy1() も試してみました。

window.functionTidy1 = function(name) {
  return _TIDY_ALIAS1[name] || name;
}
function job1a(A) {
  var v,i=0;
  while((v=A[i++])){ functionTidy1(v); }
}
function job1b(A) {
  var v,i=0, fn = functionTidy1; // alias
  while((v=A[i++])){ fn(v); }
}
job1 job1a job1b
IE6 1360 1953 719
IE8 656 625 125
Firefox2 625 156 391
Firefox3 124 66 56
Firefox3.5.3(JIT) 138 61 43
Opera9.64 140 79 78
Opera10.0 125 62 78
Safari4 26 28 18
Chrome4 23 24 24

まとめ

camelize はアニメーション等で多用されますが、ブラウザの再描画処理のほうが大幅に遅いので、camelize だけ速くしても大勢に影響は無いような気もします。
描画を速くする方法も色々とあるので、実際のアニメーション処理で体感に変化があるか試してみたいですね。


あれ? UNCAMELIZE? DECAMELIZE? どっちだろ。
あと Firefox3.5 はインストールしなおしたほうがいいんじゃないのか? これFirefox3.5 ⇒ Firefox 3.5.3 に上書インストールしたスコアを載せました。