「新はてなブックマーク」になったということで、とっても便利になったのですが、ブックマーク一覧ページ*1が若干JavaScript に時間が掛かっているみたいです。
調査してみたいと思います。調査して、改善できそうなところは後で纏めて「はてなアイデア」にでも登録しようと思います。
この日記は調査しながら、過程を書いていくつもりです。
まずは、人のサイトのJavaScript を書き換えて試してみるための環境を作ります。
以下から CocProxy というツールをダウンロードしてきます。
http://coderepos.org/share/wiki/CocProxy
$ wget http://svn.coderepos.org/share/lang/ruby/cocproxy/proxy.rb
CocProxy はid:cho45 が作った超絶便利ツールです。
ローカルに Proxy サーバーを立ち上げて、 Web サーバからの応答をローカルのファイルの内容に差し替えることができます。
CocProxy は、 proxy.rb と同じディレクトリ内にある files サブディレクトリ内にファイルを置いておけば、自動で応答を差し替えてくれるようになります。
$ mkdir files
はてなブックマーク一覧ページで使っている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.rb1 directory, 5 files
ruby で proxy.rb を起動します。以下のように、表示されます。
$ ruby proxy.rb Use default configuration.Port : 5432Dir : files/Cache: trueRules: 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ディレクトリ内のファイルを、書き換えればその結果をブラウザ上で確認できるようになります。
というわけで、実際に書き換えていきましょう。
とりあえず、Firefox 3.0 を最初のターゲットにします。
JavaScript のパフォーマンスチューニングをする際に一番最初にやるべきことは、プロファイリングです。
手順は以下の通りです。
はてなブックマークのJavaScript の実行時間のほとんどが HatenaStar.jsということが分かりました。
まずは、 HatenaStar.js をカスタマイズしていきましょう。
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;},
このコードは、
って感じですね。
まず、 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(){returnthis.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;},
createButton:function(args){var img =document.createElement('img');for (var attrin args){ img.setAttribute(attr, args[attr]);}with (img.style){ cursor ='pointer'; margin ='0 3px'; padding ='0'; border ='none'; verticalAlign ='middle';}return img;},
コードは読むまでもなく、
をしていますね。
このコードが 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 attrin 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 による、画像の動的挿入が思いのは不可避なのでしょうか。。。
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){returnnull;} prop = prop.replace(/([A-Z])/g,'-$1').toLowerCase(); style = styles ? styles.getPropertyValue(prop) :null;}elseif (elem.currentStyle){ style = elem.currentStyle[prop];}}return style;},
これはよくある、現在のスタイルの値を求める関数ですね。
こういう関数は、扱う場合非常に注意すべきことがあります。
ということです。
つまり、 ComputedStyle のプロパティアクセスと DOM の変更が交互に来るような場合が最悪で、一気に DOM を変更したあと、一気に getComputedStyle をするというのが理想です。
これは、Firefox ではどの程度影響があるかは分かりません(実験したことがないので、今度実験してみます)が、Google Chrome やSafari など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;}
現状の「はてスタ」のカスタマイズ方法を変えるわけにはいかないだろうと思いますので、使う側で 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 を使う時はこれをしとくといいですね。
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 重い問題ですね。僕の知ってる範囲では対処がありません><
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;}},
これは、タグの名前とクラス名から要素を取得する関数ですね。
とりあえず、パッと見て重そうなところは
って感じですかね。
こういうときは、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(){returnthis.string}}};
以下のような感じ
getElementsByTagAndClassName:function(tagName, className,parent){if (typeof(parent) =='undefined')parent =document;if (!tagName){// ネイティブの getElementsByClassName があれば使うif (Ten.Browser.supportsGetElementsByClassName){returnparent.getElementsByClassName(className);}else{return Ten.DOM.getElementsByClassName(className,parent);}}// Selectors API が最速if (Ten.Browser.supportsSelectorsAPI){returnparent.querySelectorAll(tagName +'.' + className);}// XPath は次点elseif (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;}
getElementPosition:function(e){var pos ={x:0, y:0};if (document.documentElement.getBoundingClientRect){// IEvar 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}elseif(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){// IEvar 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}elseif(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){// IEvar 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}elseif(document.getBoxObjectFor){//Firefox// そのまま返すreturndocument.getBoxObjectFor(e);}else{do{ pos.x += e.offsetLeft; pos.y += e.offsetTop;}while (e = e.offsetParent);}return pos;},
目立って、ネックになっている箇所はなさそうですね。
--- HatenaStar.js.org2008-11-04 18:20:37.000000000 +0900+++ HatenaStar.js2008-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.org2008-11-26 19:35:06.000000000 +0900+++ Bookmark.js2008-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つでした。
これで倍近く速くなりました。
結局、Firefox の場合は img.src 遅い問題が一番のネックになりますね。
「はてな」のJavaScript を触ってみた感想ですが、本当にしっかりと書けているなあと思いました。
僕の挙げたような高速化案はどれも、細かなブラウザ判定が必要な箇所ばかりで将来の「保守性」を犠牲にしてしまう可能性もあります。
きっと、「はてな」ではそのように判断して今のコードになったのではないでしょうか。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。