IE のメモリリーク パターン(基本編)

0 件のコメント

IE6 の時代から言われ続けている、メモリーリークパターンを復習してみます。 あくまで基本編という扱いです。 ここにあるパターンは IE8 以降で一部解消されているようです。

  • 循環参照
  • クロージャ
  • クロスページリーク

※IE8以降でメモリリークするパターンについては こちら をご参照ください。

循環参照

一言で"循環参照"、されど"循環参照"。 2つほどパターンがあるようなので、それぞれ取り上げてみます。

パターン1:DOM要素の独自プロパティがDOM要素を参照する

<html>
<head>
    <script type="text/javascript">
        // グローバル変数
        var globalObject;
        
        // メモリリークするコードを呼び出し
        function setupLeak() {
            var element;
            
            // グローバル変数に DOM要素 への参照を保持
            globalObject = document.getElementById('foo');
            
            // "bar"要素の expandProperty に DOM要素 への参照を保持
            element = document.getElementById('bar');
            element.expandProperty = globalObject;
        };
        
        // メモリリークしたオブジェクトを破棄して解放
        function teardownLeak() {
            document.getElementById('bar').expandProperty = null;
        };
        
        window.onload = function () {
            document.getElementById('setup').onclick = setupLeak;
            document.getElementById('teardown').onclick = teardownLeak;
        };
    </script>
</head>
<body>
    <input type="button" id="setup" value="setup leak" />
    <input type="button" id="teardown" value="teardown leak" />
    <div id="foo"></div>
    <div id="bar"></div>
</body>
</html>

DOM要素の独自プロパティ(document.getElementById('bar').expandProperty)に、 別のDOM要素への参照(document.getElementById('foo'))を取り付けることで、 HTML と JavaScript の間に循環参照を引き起こしているパターンです。

パターン2:DOM要素の独自プロパティがDOM要素を保持したインスタンスを参照する

<html>
<head>
    <script type="text/javascript">
        // クラスを定義
        function HogeClass (element) {
            // "this" に DOM要素 への参照を保持
            this.elementRef = element;
            
            // DOM要素 の expandProperty へ "this" への参照を保持
            // "this" には DOM要素 への参照 "this.elementRef" が含まれている
            element.expandProperty = this;
        }
        
        // メモリリークするコードを呼び出し
        function setupLeak() {
            // HogeClass のインスタンスを生成
            new HogeClass(document.getElementById('foo'));
        };
        
        // メモリリークしたオブジェクトを破棄して解放
        function teardownLeak() {
            document.getElementById('foo').expandProperty = null;
        };
        
        window.onload = function () {
            document.getElementById('setup').onclick = setupLeak;
            document.getElementById('teardown').onclick = teardownLeak;
        };
    </script>
</head>
<body>
    <input type="button" id="setup" value="setup leak" />
    <input type="button" id="teardown" value="teardown leak" />
    <div id="foo"></div>
    <div id="bar"></div>
</body>
</html>

作成したクラスの this.elementRef に DOM要素への参照を保持し、 DOM要素 への参照があるクラスのインスタンスを DOM要素 の expandProperty へ保持させることで、 循環参照を引き起こすパターンです。

クロージャ

クロージャを書いてしまいやすいのはやはりイベントを設定するときだと思います。 jQuery なんかも平気でクロージャを使うので…IE6世代だとマズそうな気がします。 (最近、そういった環境の開発をしてないので詳細がわかりませんが。。)

DOM要素のイベント(匿名関数)にDOM要素を参照する変数が含まれる

<html>
<head>
    <script type="text/javascript">
        // メモリリークするコードを呼び出し
        function setupLeak() {
            var element;
            
            // DOM要素への参照を取得
            element = document.getElementById('foo');
            
            // DOM要素へイベントを匿名関数で取り付ける
            document.getElementById('bar').onclick = function (event) {
                // クリック時に動作するコード
                // このコード内では "element" = DOM要素 へアクセスできる
            };
        };
        
        // メモリリークしたオブジェクトを破棄して解放
        function teardownLeak() {
            document.getElementById('bar').onclick = null;
        };
        
        window.onload = function () {
            document.getElementById('setup').onclick = setupLeak;
            document.getElementById('teardown').onclick = teardownLeak;
        };
    </script>
</head>
<body>
    <input type="button" id="setup" value="setup leak" />
    <input type="button" id="teardown" value="teardown leak" />
    <div id="foo"></div>
    <div id="bar"></div>
</body>
</html>

DOM要素のイベントに接続した匿名関数が参照可能な関数スコープ内に、 他のDOM要素への参照が含まれることで、 循環参照を引き起こしています。

クロスページリーク

DOM要素を動的に生成、構築していく順序により、メモリリークしてしまう現象です。

○メモリリークしないコード
var rootElement = document.getElementById('root');
var parentElement = rootElement;
var childElement;
for (var i = 0; i < 5000; i++) {
    // 子要素を生成
    childElement = document.createElement('div');
    
    // 親要素に生成した子要素を追加
    parentElement.appendChild(childElement);
    
    // 親子関係を更新
    parentElement = childElement;
}
×メモリリークするコード
var rootElement = document.getElementById('root');
var parentElement;
var childElement = document.createElement('div');
for (var i = 0; i < 5000; i++) {
    // 親要素を生成
    parentElement = document.createElement('div');
    
    // 生成した親要素に子要素を追加
    parentElement.appendChild(childElement);
    
    // 親子関係を更新
    childElement = parentElement;
}
rootElement.appendChild(childElement);

DOMツリーへ接続されないDOM要素はリークの元になるようです。 ただ…一般的に「DOMツリーへの接続は必要最小限にすること」が言われています。 DOMツリーへ接続するたび再描画が走るので、JavaScript の処理が重くなるからです。

メモリリークとスピードの格闘…悩ましい限りです。。

今回、参考にしたサイトは以下の通りです。