はじめに
「新はてなブックマーク」になったということで、とっても便利になったのですが、ブックマーク一覧ページ*1が若干 JavaScript に時間が掛かっているみたいです。
というわけで
調査してみたいと思います。調査して、改善できそうなところは後で纏めて「はてなアイデア」にでも登録しようと思います。
この日記は調査しながら、過程を書いていくつもりです。
準備
まずは、人のサイトの JavaScript を書き換えて試してみるための環境を作ります。
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 のパフォーマンスチューニングをする際に一番最初にやるべきことは、プロファイリングです。
手順は以下の通りです。
- Firebug のコンソールを開いて、上のほうにあるプロファイルボタンを押して、ページをリロード
- ページが読み込まれ、しばらくしてプロファイルボタンをもう一度押す
- 結果が表示される
プロファイリングの結果
- 「時間」は、その関数の開始から終わりまでの時間の合計
- 「所有時間」は、その関数の開始から終わりまでの時間の合計から、自分の呼び出した関数の「時間」を引いたもの
- たいていの場合は「所有時間」から重い場所を特定できる
結果の考察
はてなブックマークの 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; },
重い箇所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 による、画像の動的挿入が思いのは不可避なのでしょうか。。。
追記:解決策があったようです。
重い箇所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 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 を使う時はこれをしとくといいですね。
重い箇所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; }
重い箇所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 を触ってみた感想ですが、本当にしっかりと書けているなあと思いました。
僕の挙げたような高速化案はどれも、細かなブラウザ判定が必要な箇所ばかりで将来の「保守性」を犠牲にしてしまう可能性もあります。
きっと、「はてな」ではそのように判断して今のコードになったのではないでしょうか。