皆さんjQueryは使ったことありますよね。Webでの開発ではとても便利で、ほぼ必須と言っても過言ではありません。しかしながらこのjQueryはメモリーリークを誘発しやすい構造であることはあまり知られていません。
GCのあるJavaScriptでメモリーリークが発生するとは何を言っとるんだ、と思われる向きもあるやもしれません。しかしGCがあっても、もう使わなくなったオブジェクトを配列やテーブル(Object)にしまいこんでいて、それを回収するタイミングが存在しなければ積もり積もってメモリを圧迫する、メモリーリークとなりうるというのは想像に難くないでしょう。jQueryで起こりうるメモリーリークはそのような性質のものです。
では具体的にどうするとメモリーリークが起こるのでしょう。答えはシンプルで jQuery API 以外で DOM 操作を行うとメモリーリークが起こりえます。正確に言いますと、jQuery events や jQuery data を使った=追加・設定したノードを DOM API 経由で削除すると…リークします。
実例を挙げるとこうなります。
// クラス foo を持つノードにイベントを設定
$('.foo').on('click', funciton() {...});
// (snip)
// クラス foo を持つノードを DOM 操作で削除
var node = $('.foo')[0]
node.parentNode.removeChild(node);
// メモリーリーク発生!
その他にも innerHtml
を設定してノードが消えたとか、パッと見ではわかりにくいケースでもメモリーリークが発生しえます。
「じゃあ jQuery を使うのはやめよう!」
それができたら苦労はしません。jQuery は優れたライブラリであり、またそれを用いたたくさんの優秀なライブラリの基盤となっています。いまさら外すという選択肢は現実的ではありません。
「そんなこと言うなら全部jQueryで書けば良いんじゃ?」
ええ。それができるなら、そのとおりですね。しかしJavaScriptによるWebのクライアントサイドプログラムでは、jQueryではないライブラリも組み合わせて使うケースが多々有ります。特に NodeJS の登場以降、サーバサイドには非jQueryな良いライブラリが大量に生まれ、クライアントサイドへの転用も進んでます。そんな中、非jQueryなライブラリは使わないという決定をするのは、jQueryを使わないという決定をするのと同じくらい馬鹿げています。
「んじゃどうしたらええのよ…」
次善の策として jQuery API外でのDOM操作=削除を検出してjQuery APIに伝える という仕組み (=jquery.gc-helper.js)を考案してみました。
(function(global, jQuery) {
var observer;
function callback(mutations) {
for (var i = 0, I = mutations.length; i < I; ++i) {
var m = mutations[i];
if (m.removedNodes) {
for (var j = 0, J = m.removedNodes.length; j < J; ++j) {
var n = m.removedNodes[j];
jQuery(n).remove();
}
}
}
}
observer = new global.MutationObserver(callback);
jQuery(function() {
observer.observe(global.document, {childList: true, subtree: true});
});
})(window, jQuery);
DOM Level 4 で追加された MutationObserver を使うと、指定したノード以下のノード操作をスマートに検出できます。この MutationObserver によりノードの削除を検出しそれを jQuery API (remove) に伝えているだけのとてもシンプルなライブラリです。できたてホヤホヤでまだまだ見落としがあるとは思いますが、簡単なテストではリークするハズのオブジェクトを正しく回収できることが確かめられています。
以上、jQuery を使うときにはメモリーリークが起こりうること、また MutationObserver でそれを回避する仕組みを作れることの紹介でした。
2014/03/29 19:00 追記
上記の jquery.gc-helper.js は一部の jQuery plugin と相性がよくありません。gist にある最新版は多少マシになってますが、それでも jQuery.detach() を使われると何かしらの不具合が発生することがわかっています。本記事に書いてあることは鵜呑みにすることなく、あくまでも参考にとどめてケース・バイ・ケースで対策されることをオススメしておきます。