IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

はてなブックマークのコンテンツの JavaScript を高速化する

はじめに

「新はてなブックマーク」になったということで、とっても便利になったのですが、ブックマーク一覧ページ*1が若干 JavaScript に時間が掛かっているみたいです。

というわけで

調査してみたいと思います。調査して、改善できそうなところは後で纏めて「はてなアイデア」にでも登録しようと思います。
この日記は調査しながら、過程を書いていくつもりです。

準備

まずは、人のサイトの JavaScript を書き換えて試してみるための環境を作ります。

作業用ディレクトリを作る

とりあえず、ホームに HatenaJS というディレクトリを作ります。

$ mkdir HatenaJS
$ cd HatenaJS
CocProxy をダウンロードしてくる

以下から CocProxy というツールをダウンロードしてきます。
http://coderepos.org/share/wiki/CocProxy

$ wget http://svn.coderepos.org/share/lang/ruby/cocproxy/proxy.rb

CocProxy は id:cho45 が作った超絶便利ツールです。
ローカルに Proxy サーバーを立ち上げて、 Web サーバからの応答をローカルのファイルの内容に差し替えることができます。

CocProxy で差し替えるファイル用のディレクトリを作る

CocProxy は、 proxy.rb と同じディレクトリ内にある files サブディレクトリ内にファイルを置いておけば、自動で応答を差し替えてくれるようになります。

$ mkdir files
はてなブックマークで使っている以下の JavaScript をダウンロード

はてなブックマーク一覧ページで使っている JavaScript をダウンロードして、 files ディレクトリ内に入れます。

$ cd files
$ wget http://s.hatena.ne.jp/js/HatenaStar.js
$ wget http://b.hatena.ne.jp/js/DropDownSelector.js
$ wget http://b.hatena.ne.jp/js/CSSChanger.js
$ wget http://b.hatena.ne.jp/js/Hatena/Bookmark.js
$ cd ..

今のところ作業ディレクトリ内は以下のような感じです。

$ tree
.
|-- files
|   |-- Bookmark.js
|   |-- CSSChanger.js
|   |-- DropDownSelector.js
|   `-- HatenaStar.js
`-- proxy.rb

1 directory, 5 files
CocProxy を起動して、ブラウザのプロキシを設定する

ruby で proxy.rb を起動します。以下のように、表示されます。

$ ruby proxy.rb 
Use default configuration.
Port : 5432
Dir  : files/
Cache: true
Rules:
    1. #{File.basename(req.path_info)}
    2. #{req.host}#{req.path_info}
    3. #{req.host}/#{File.basename(req.path_info)}
    4. .#{req.path_info}

これが完了したら localhost:5432 にプロキシサーバが立ち上がっているので、ブラウザに設定します。

準備完了

これで準備完了です。
あとは files ディレクトリ内のファイルを、書き換えればその結果をブラウザ上で確認できるようになります。
というわけで、実際に書き換えていきましょう。

まずは、 Firebug でプロファイリングする

とりあえず、 Firefox 3.0 を最初のターゲットにします。
JavaScript のパフォーマンスチューニングをする際に一番最初にやるべきことは、プロファイリングです。
手順は以下の通りです。

  1. Firebug のコンソールを開いて、上のほうにあるプロファイルボタンを押して、ページをリロード
  2. ページが読み込まれ、しばらくしてプロファイルボタンをもう一度押す
  3. 結果が表示される
プロファイリングの結果

結果が出ました

  • 「時間」は、その関数の開始から終わりまでの時間の合計
  • 「所有時間」は、その関数の開始から終わりまでの時間の合計から、自分の呼び出した関数の「時間」を引いたもの
  • たいていの場合は「所有時間」から重い場所を特定できる
結果の考察

はてなブックマークJavaScript の実行時間のほとんどが HatenaStar.jsということが分かりました。
まずは、 HatenaStar.js をカスタマイズしていきましょう。

重い箇所1:HatenaStar.js 1380 行目

    makeTextNodes: function(c) {
        if (c.textNodes || c.textNodePositions || c.documentText) return;
        if (Ten.Highlight.highlighted) Ten.Highlight.highlighted.hide();
        c.textNodes = [];
        c.textNodePositions = [];
        var isIE = navigator.userAgent.indexOf('MSIE') != -1;
        var texts = [];
        var pos = 0; 
        (function(node, parent) {
            if (isIE && parent && parent != node.parentNode) return;
            if (node.nodeType == 3) { 
                c.textNodes.push(node);
                texts.push(node.nodeValue);
                c.textNodePositions.push(pos);
                pos += node.nodeValue.length;
            } else {
                var childNodes = node.childNodes;
                for (var i = 0; i < childNodes.length; i++) {
                    arguments.callee(childNodes[i], node);
                }    
            }    
        })(document.body);
        c.documentText = texts.join('');
        c.loaded = true;
    },   
コードを読む

このコードは、

  • コンテンツ内の TextNode をすべて走査して、全体の textContent における TextNode の出現位置をキャッシュするためのもの
  • はてなスター」の「引用部分」を高速に探してハイライトするために使っている
  • Ten.Highlight の最初のオブジェクトが作られた時に一度だけ全実行される
  • 二個目のオブジェクト以降は、最初の条件文でリターンする

って感じですね。

改善してみる

まず、 DOM ツリーの走査がかなり重そうです。
XPath を使えるブラウザでは高速化できそうですね。
という訳で、まず以下のような判定を入れます。

/* Ten.Browser */
Ten.Browser = {

    // XPath が使えるかどうかのフラグ
    supportsXPath: !!document.evaluate,

    isIE: navigator.userAgent.indexOf('MSIE') != -1,
    isMozilla: navigator.userAgent.indexOf('Mozilla') != -1 && !/compatible|WebKit/.test(navigator.userAgent),
    isOpera: !!window.opera,
    isSafari: navigator.userAgent.indexOf('WebKit') != -1,
    version: {
        string: (/(?:Firefox\/|MSIE |Opera\/|Version\/)([\d.]+)/.exec(navigator.userAgent) || []).pop(),
        valueOf: function() { return parseFloat(this.string) },
        toString: function() { return this.string }
    }
};

で、件の箇所を以下のように XPath を使うように修正します。

    makeTextNodes: function(c) {
        if (c.textNodes || c.textNodePositions || c.documentText) return;
        if (Ten.Highlight.highlighted) Ten.Highlight.highlighted.hide();
        c.textNodes = [];
        c.textNodePositions = [];
        var isIE = navigator.userAgent.indexOf('MSIE') != -1;
        var texts = [];
        var pos = 0; 

        // XPath をサポートしていたら
        if (Ten.Browser.supportsXPath) {

            // XPath で全てのテキストノードを取得する
            var result = document.evaluate('descendant::text()', document.body, null, 7, null);

            // テキストノードの走査
            for (var i = 0; i < result.snapshotLength; i ++) {
                var node = result.snapshotItem(i);
                c.textNodes.push(node);
                c.textNodePositions.push(pos);
                pos += node.length;
            }

            // textContent は一発で 
            c.documentText = document.body.textContent || document.body.innerText;
            c.loaded = true;

            return;
        }

        (function(node, parent) {
            if (isIE && parent && parent != node.parentNode) return;
            if (node.nodeType == 3) { 
                c.textNodes.push(node);
                texts.push(node.nodeValue);
                c.textNodePositions.push(pos);
                pos += node.nodeValue.length;
            } else {
                var childNodes = node.childNodes;
                for (var i = 0; i < childNodes.length; i++) {
                    arguments.callee(childNodes[i], node);
                }    
            }    
        })(document.body);
        c.documentText = texts.join('');
        c.loaded = true;
    },   
結果を見る


458.3ms かかっていた処理が 91.584ms にも縮みました。
これはかなりよかったみたいですね。

重い箇所2:HatenaStar.js 1738 行目

    createButton: function(args) {
        var img = document.createElement('img');
        for (var attr in args) {
            img.setAttribute(attr, args[attr]);
        }    
        with (img.style) {
        cursor = 'pointer';
        margin = '0 3px';
            padding = '0'; 
            border = 'none';
            verticalAlign = 'middle';
        }    
        return img; 
    },
コードを読む

コードは読むまでもなく、

  • 画像を作って
  • 属性の設定
  • style の設定

をしていますね。
このコードが 254 回呼び出されています。

解説

こんなシンプルなソースコードが何故重いかというと、 Firefox での JavaScript による img.src の設定が激重なのです。

改善してみる

これは、 Firefox 固有の問題なのでブラウザを切り分けて対処します。 Firefox では img を辞めて span を使うようにしてみました。

    createButton: function(args) {

        // Firefox なら
        if (Ten.Browser.isMozilla) {

            // クラス
            var c = Hatena.Star.Button;
            
            // img の代わりに span 要素を使う
            var img = document.createElement('span');
            
            // title 要素の設定
            img.title = args.title;
            
            var style = img.style;
            
            // クラスに画像のキャッシュを持たせる
            c.imageCache = c.imageCache || [];
            
            // キャッシュに Image オブジェクトが入っているか
            var cache = c.imageCache[args.src]
            if (!cache) {
            
                // 無かったら作る
                cache = new Image;
                c.imageCache[args.src] = cache;
                
                // load したらフラグ立てる
                cache.addEventListener('load', function() { cache.loaded = true }, false);
                cache.src = args.src;
            }   

            // 高さと幅を設定する関数
            function setStyle() {
                style.width = cache.width + 'px';
                style.height = cache.height + 'px';
            }    

            // Image オブジェクトがロード済みだったらその場で呼び出す
            // 未ロードだったら、 load イベントリスナーに登録
            cache.loaded ? setStyle() : cache.addEventListener('load', setStyle, false);

            // img 要素(置換要素)と同じ display を付ける
            style.display = 'inline-block';

            // background-image の指定
            style.backgroundImage = 'url(' + args.src + ')'; 
        }    
        else {
            var img = document.createElement('img');
            for (var attr in args) {
                img.setAttribute(attr, args[attr]);
            }    
        }    
        with (img.style) {
        cursor = 'pointer';
        margin = '0 3px';
            padding = '0'; 
            border = 'none';
            verticalAlign = 'middle';
        }    
        return img; 
    },   
もう一度プロファイリング


初回 237ms 二回目 173ms かかっていた createButton が 90ms にまで減りました。これも、けっこうききました。
と思ったら、 addAddButton 関数(ここで作った要素を挿入する箇所)の時間が逆に 60ms 増えていますね。これではダメですね。
この箇所は、諦めてとりあえず前の状態に戻しておきます。
Firefox による、画像の動的挿入が思いのは不可避なのでしょうか。。。

追記:解決策があったようです。

2008-11-27 - つれずれなるままに…

重い箇所3:HatenaStar.js 943 行目

    getElementStyle: function(elem, prop) {
        var style = elem.style ? elem.style[prop] : null;
        if (!style) {
            var dv = document.defaultView;
            if (dv && dv.getComputedStyle) {
                try {
                    var styles = dv.getComputedStyle(elem, null);
                } catch(e) {
                    return null;
                }    
                prop = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
                style = styles ? styles.getPropertyValue(prop) : null;
            } else if (elem.currentStyle) {
                style = elem.currentStyle[prop];
            }    
        }    
        return style;
    },   
解説

これはよくある、現在のスタイルの値を求める関数ですね。
こういう関数は、扱う場合非常に注意すべきことがあります。

  • getComputedStyle で取得したオブジェクトのプロパティにアクセスすると、それまでの DOM の変更が一気に計算される
  • DOM に変更がない場合(スタイルを再計算する必要がない場合)は、プロパティのアクセスは軽い

ということです。
つまり、 ComputedStyle のプロパティアクセスと DOM の変更が交互に来るような場合が最悪で、一気に DOM を変更したあと、一気に getComputedStyle をするというのが理想です。
これは、 Firefox ではどの程度影響があるかは分かりません(実験したことがないので、今度実験してみます)が、 Google ChromeSafari など WebKit 系のブラウザではかなり顕著です。
HatenaStar.js での使い方を見てみると

    getImgSrc: function(c,container) {
        var sel = c.ImgSrcSelector;
        if (container) {
            var cname = sel.replace(/\./,'');
            var span = new Ten.Element('span',{
                className: cname
            });

            // DOM の変更
            container.appendChild(span);

           // スタイルが再計算される
            var bgimage = Ten.Style.getElementStyle(span,'backgroundImage');

            // DOM の変更
            container.removeChild(span);

            if (bgimage) {
                var url = Ten.Style.scrapeURL(bgimage);
                if (url) return url;
            }
        }
        if (sel) {
            var prop = Ten.Style.getGlobalStyle(sel,'backgroundImage');
            if (prop) {
                var url = Ten.Style.scrapeURL(prop);
                if (url) return url;
            }
        }
        return c.ImgSrc;
    }

このような場合が、一番重いです。(少なくとも WebKit 系では)

はてなスターでは何故これをやっているか

これは、ユーザーがスターや add ボタンの画像をカスタマイズ出来るようにするためで、ユーザーが設定した背景画像を取得しているんですね。
ただ、少なくとも「はてなブックマーク」に関しては、スターやボタンの画像をカスタマイズしている箇所はありません。

試しに

この getImgSrc が一切何もせずにデフォルトの画像を返すとどのくらい、速くなるかを実験してみます。

    getImgSrc: function(c,container) {
        // 決めうち!
        return c.ImgSrc;
        var sel = c.ImgSrcSelector;
        if (container) {
            var cname = sel.replace(/\./,'');
            var span = new Ten.Element('span',{
                className: cname
            });  
            container.appendChild(span);
            var bgimage = Ten.Style.getElementStyle(span,'backgroundImage');
            container.removeChild(span);
            if (bgimage) {
                var url = Ten.Style.scrapeURL(bgimage);
                if (url) return url; 
            }    
        }    
        if (sel) {
            var prop = Ten.Style.getGlobalStyle(sel,'backgroundImage');
            if (prop) {
                var url = Ten.Style.scrapeURL(prop);
                if (url) return url; 
            }    
        }    
        return c.ImgSrc;
    }    

結果が以下

200ms 速くなりました。

改善する

現状の「はてスタ」のカスタマイズ方法を変えるわけにはいかないだろうと思いますので、使う側で getImgSrc を上書きしてしまいましょう(「新はてブ」では「はてスタ」カスタマイズ出来ないので OK)
こんどは HatenaStar.js を使う側の Bookmark.js に以下の一行を追加します。

if (typeof Hatena.Star != 'undefined') {
    // ロードするURLを差し替える
    Hatena.Star.EntryLoader.loadEntries = function() {};
    Hatena.Star.EntryLoader.getStarEntries = Hatena.Bookmark.Star.getStarEntries;

    // この行を追加!!
    Hatena.Star.Button.getImgSrc = function(c) { return c.ImgSrc };

    // load swf
    // Ten.DOM.addEventListener('onload', function() {
    //     Hatena.Bookmark.Star.loadStarLoaderBySwf();
    // });
}

はてなブックマークに限らず、カスタマイズせずに HatenaStar.js を使う時はこれをしとくといいですね。

プロファイリング


最高新記録でました!

重い箇所4:HatenaStar.js 1945 行目

    getImage: function(container) {
        var img = document.createElement('img');
        var src = Hatena.Star.Button.getImgSrc(Hatena.Star.Star,container);
        img.src = src; 
        img.setAttribute('tabIndex', 0);
        img.className = 'hatena-star-star';
        with (img.style) {
            padding = '0'; 
            border = 'none';
        }    
        return img; 
    },   
うーん

これも 1738 行目 createButton と同じ Firefox の img.src 重い問題ですね。僕の知ってる範囲では対処がありません><

重い箇所5: HatenaStar.js 348 行目

    getElementsByTagAndClassName: function(tagName, className, parent) {
        if (typeof(parent) == 'undefined') parent = document;
        if (!tagName) return Ten.DOM.getElementsByClassName(className, parent);
        var children = parent.getElementsByTagName(tagName);
        if (className) { 
            var elements = [];
            for (var i = 0; i < children.length; i++) {
                var child = children[i];
                if (Ten.DOM.hasClassName(child, className)) {
                    elements.push(child);
                }    
            }    
            return elements;
        } else {
            return children;
        }    
    },   
解説

これは、タグの名前とクラス名から要素を取得する関数ですね。
とりあえず、パッと見て重そうなところは

  • XPath か Selectors API が使える場合は使ったほうがいい
  • 今時のブラウザは getElementsByClassName はネイティブで実装されているので、あればそっち使ったほうがいい
  • for 文の終了判定 children.length (live な NodeList の length)は、毎回 DOM アクセスが発生する(外せば、ループが倍速で回る)

って感じですかね。

実際にはてブではどういう引数で使っているか「統計」を取る

こういうときは、 Firebug の console.count を使います。

    getElementsByTagAndClassName: function(tagName, className, parent) {

        // こんな感じで仕込んでおく
        console.count(tagName + '.' + className);

        if (typeof(parent) == 'undefined') parent = document;
        if (!tagName) return Ten.DOM.getElementsByClassName(className, parent);
        var children = parent.getElementsByTagName(tagName);
        if (className) { 
            var elements = [];
            for (var i = 0; i < children.length; i++) {
                var child = children[i];
                if (Ten.DOM.hasClassName(child, className)) {
                    elements.push(child);
                }    
            }    
            return elements;
        } else {
            return children;
        }    
    },

で、リロードすると

こんな感じの統計が取れます。

統計を見ると

タグ名とクラス名両方指定されることしかないようですね。もし、タグ名しか指定されないとか、クラス名しか指定されないとかだったら、ショートカットできるかと思ったのですが、まあ、セオリー通り XPath を使いましょう。

改善してみる

まずは、ブラウザ判定のところに以下を追加します。

/* Ten.Browser */
Ten.Browser = {
    supportsXPath: !!document.evaluate,

    // 追加!
    supportsSelectorsAPI: !!document.querySelectorAll,
    supportsGetElementsByClassName: !!document.getElementsByClassName,

    isIE: navigator.userAgent.indexOf('MSIE') != -1,
    isMozilla: navigator.userAgent.indexOf('Mozilla') != -1 && !/compatible|WebKit/.test(navigator.userAgent),
    isOpera: !!window.opera,
    isSafari: navigator.userAgent.indexOf('WebKit') != -1,
    version: {
        string: (/(?:Firefox\/|MSIE |Opera\/|Version\/)([\d.]+)/.exec(navigator.userAgent) || []).pop(),
        valueOf: function() { return parseFloat(this.string) },
        toString: function() { return this.string }
    }    
};

以下のような感じ

    getElementsByTagAndClassName: function(tagName, className, parent) {
        if (typeof(parent) == 'undefined') parent = document;

        if (!tagName) {

            // ネイティブの getElementsByClassName があれば使う
            if (Ten.Browser.supportsGetElementsByClassName) {
                return parent.getElementsByClassName(className);
            }
            else {
                return Ten.DOM.getElementsByClassName(className, parent);
            }
        }

        // Selectors API が最速
        if (Ten.Browser.supportsSelectorsAPI) {
            return parent.querySelectorAll(tagName + '.' + className);
        }

        // XPath は次点
        else if (Ten.Browser.supportsXPath) {
            var result = document.evaluate("descendant::" + tagName + "[@class=" + className + " or contains(concat(' ', @class, ' '), ' " + className + " ')]", parent, null, 7, null);
            var elements = [];
            for (var i = 0, l = result.snapshotLength; i < l; i ++) {
                elements.push(result.snapshotItem(i));
            }
            return elements;
        }

        var children = parent.getElementsByTagName(tagName);
        if (className) {
            var elements = [];

            // children.length の参照回数を減らす
            for (var i = 0, l = children.length; i < l; i++) {
                var child = children[i];
                if (Ten.DOM.hasClassName(child, className)) {
                    elements.push(child);
                }
            }
            return elements;
        } else {
            return children;
        }
プロファイリング


前の結果は hasClassName を含んでいるので実際には 10ms ほど速くなりました。
とは言え、雀の涙ですね。やはり XPath 式が複雑になってしまうためではないでしょうか。
Firefox 3.1 では Selectors API を使えるようになるので、上のようにしておくと後々速くなることが期待できます。
また、前の章でカットした getImgSrc からは getElementsBySelector が呼ばれる可能性があるので、これも Selectors API を使うようにしたほうがいいでしょう。(ここではやりません)

重い箇所6: HatenaStar.js 1031 行目

    getElementPosition: function(e) {
        var pos = {x:0, y:0};
        if (document.documentElement.getBoundingClientRect) { // IE 
            var box = e.getBoundingClientRect();
            var owner = e.ownerDocument;
            pos.x = box.left + Math.max(owner.documentElement.scrollLeft, owner.body.scrollLeft) - 2; 
            pos.y = box.top  + Math.max(owner.documentElement.scrollTop,  owner.body.scrollTop) - 2
        } else if(document.getBoxObjectFor) { //Firefox
            pos.x = document.getBoxObjectFor(e).x;
            pos.y = document.getBoxObjectFor(e).y;
        } else {
            do { 
                pos.x += e.offsetLeft;
                pos.y += e.offsetTop;
            } while (e = e.offsetParent);
        }    
        return pos; 
    },   
解説

これは、一回しか呼ばれていないのに 40ms とか食っていますね。

どこから呼ばれたのか調べる

Error().stackを使います

    getElementPosition: function(e) {

        // これ!
        console.log(Error().stack);

        var pos = {x:0, y:0};
        if (document.documentElement.getBoundingClientRect) { // IE 
            var box = e.getBoundingClientRect();
            var owner = e.ownerDocument;
            pos.x = box.left + Math.max(owner.documentElement.scrollLeft, owner.body.scrollLeft) - 2; 
            pos.y = box.top  + Math.max(owner.documentElement.scrollTop,  owner.body.scrollTop) - 2
        } else if(document.getBoxObjectFor) { //Firefox
            pos.x = document.getBoxObjectFor(e).x;
            pos.y = document.getBoxObjectFor(e).y;
        } else {
            do { 
                pos.x += e.offsetLeft;
                pos.y += e.offsetTop;
            } while (e = e.offsetParent);
        }    
        return pos; 
    },   

こんな感じでスタックトレースが見れます。

Bookmark.js の 3580 行目から呼ばれているらしいです。

    fixedPosition: function() {
        var pos = Ten.Geometry.getElementPosition(this.form);
        var w = Ten.Geometry.getWindowSize();
     
        //this.layer.div.style.right = w.w - pos.x - this.form.offsetWidth + 'px';
        this.layer.div.style.right = '15px';
        this.layer.div.style.top = pos.y - this.form.offsetHeight - 30 +  'px';
        //this.layer.moveTo(0, pos.y + this.form.offsetHeight);
    },   

検索のポップアップの位置決めみたいですね。

改善してみる

document.getBoxObjectFor が二回も呼ばれている&使う側では y しか見ないので、以下のように改良してみます。

    getElementPosition: function(e) {
        var pos = {x:0, y:0};
        if (document.documentElement.getBoundingClientRect) { // IE 
            var box = e.getBoundingClientRect();
            var owner = e.ownerDocument;
            pos.x = box.left + Math.max(owner.documentElement.scrollLeft, owner.body.scrollLeft) - 2; 
            pos.y = box.top  + Math.max(owner.documentElement.scrollTop,  owner.body.scrollTop) - 2
        } else if(document.getBoxObjectFor) { //Firefox

            // そのまま返す
            return document.getBoxObjectFor(e);

        } else {
            do { 
                pos.x += e.offsetLeft;
                pos.y += e.offsetTop;
            } while (e = e.offsetParent);
        }    
        return pos; 
    },   
プロファイリング


全然変わってません><もとにもどしておきます。

他は

目立って、ネックになっている箇所はなさそうですね。

ここまでの diff

HatenaStar.js
--- HatenaStar.js.org	2008-11-04 18:20:37.000000000 +0900
+++ HatenaStar.js	2008-11-27 01:48:54.000000000 +0900
@@ -347,11 +347,39 @@
 Ten.DOM = new Ten.Class({
     getElementsByTagAndClassName: function(tagName, className, parent) {
         if (typeof(parent) == 'undefined') parent = document;
-        if (!tagName) return Ten.DOM.getElementsByClassName(className, parent);
+
+        if (!tagName) {
+
+            // ネイティブの getElementsByClassName があれば使う
+            if (Ten.Browser.supportsGetElementsByClassName) {
+                return parent.getElementsByClassName(className);
+            }
+            else {
+                return Ten.DOM.getElementsByClassName(className, parent);
+            }
+        }
+
+        // Selectors API が最速
+        if (Ten.Browser.supportsSelectorsAPI) {
+            return parent.querySelectorAll(tagName + '.' + className);
+        }
+
+        // XPath は次点
+        else if (Ten.Browser.supportsXPath) {
+            var result = document.evaluate("descendant::" + tagName + "[@class=" + className + " or contains(concat(' ', @class, ' '), ' " + className + " ')]", parent, null, 7, null);
+            var elements = [];
+            for (var i = 0, l = result.snapshotLength; i < l; i ++) {
+                elements.push(result.snapshotItem(i));
+            }
+            return elements;
+        }
+
         var children = parent.getElementsByTagName(tagName);
         if (className) { 
             var elements = [];
-            for (var i = 0; i < children.length; i++) {
+
+            // children.length の参照回数を減らす
+            for (var i = 0, l = children.length; i < l; i++) {
                 var child = children[i];
                 if (Ten.DOM.hasClassName(child, className)) {
                     elements.push(child);
@@ -1029,6 +1057,7 @@
         }
     },
     getElementPosition: function(e) {
+        console.log(Error().stack);
         var pos = {x:0, y:0};
         if (document.documentElement.getBoundingClientRect) { // IE 
             var box = e.getBoundingClientRect();
@@ -1157,6 +1186,9 @@
 
 /* Ten.Browser */
 Ten.Browser = {
+    supportsXPath: !!document.evaluate,
+    supportsSelectorsAPI: !!document.querySelectorAll,
+    supportsGetElementsByClassName: !!document.getElementsByClassName,
     isIE: navigator.userAgent.indexOf('MSIE') != -1,
     isMozilla: navigator.userAgent.indexOf('Mozilla') != -1 && !/compatible|WebKit/.test(navigator.userAgent),
     isOpera: !!window.opera,
@@ -1376,6 +1408,28 @@
         var isIE = navigator.userAgent.indexOf('MSIE') != -1;
         var texts = [];
         var pos = 0;
+
+        // XPath をサポートしていたら
+        if (Ten.Browser.supportsXPath) {
+
+            // XPath で全てのテキストノードを取得する
+            var result = document.evaluate('descendant::text()', document.body, null, 7, null);
+
+            // テキストノードの走査
+            for (var i = 0; i < result.snapshotLength; i ++) {
+                var node = result.snapshotItem(i);
+                c.textNodes.push(node);
+                c.textNodePositions.push(pos);
+                pos += node.length;
+            }
+
+            // textContent は一発で 
+            c.documentText = document.body.textContent || document.body.innerText;
+            c.loaded = true;
+
+            return;
+        }
+
         (function(node, parent) {
             if (isIE && parent && parent != node.parentNode) return;
             if (node.nodeType == 3) {
Bookmark.js
--- Bookmark.js.org	2008-11-26 19:35:06.000000000 +0900
+++ Bookmark.js	2008-11-27 00:33:45.000000000 +0900
@@ -4867,6 +4867,9 @@
     Hatena.Star.EntryLoader.loadEntries = function() {};
     Hatena.Star.EntryLoader.getStarEntries = Hatena.Bookmark.Star.getStarEntries;
 
+    // この行を追加
+    Hatena.Star.Button.getImgSrc = function(c) { return c.ImgSrc };
+
     // load swf
     // Ten.DOM.addEventListener('onload', function() {
     //     Hatena.Bookmark.Star.loadStarLoaderBySwf();

まとめ

結局、一番効果があったのは、以下の2つでした。

  • makeTextNodes 関数(全 TextNode の走査)に XPath を使う
  • カスタマイズしない場合は Hatena.Star.Button.getImgSrc を上書きする

これで倍近く速くなりました。
結局、 Firefox の場合は img.src 遅い問題が一番のネックになりますね。

感想

はてな」の JavaScript を触ってみた感想ですが、本当にしっかりと書けているなあと思いました。
僕の挙げたような高速化案はどれも、細かなブラウザ判定が必要な箇所ばかりで将来の「保守性」を犠牲にしてしまう可能性もあります。
きっと、「はてな」ではそのように判断して今のコードになったのではないでしょうか。