requirejsで遅延読み込み(オンデマンド)を実現する方法

 requirejsは、htmlにscriptタグを量産することなく、依存性管理を行える非常に便利なライブラリです。

 今回は、これを使って、「必要になるまでモジュールを読み込まない」、遅延ローディングの方法を考えてみます。

スポンサーリンク
rectangle

通常の利用方法

 通常は、requirejsを利用するコードは以下のように書きます。

// app.js
require(["mod"],function(Module){
    console.log("app.js loaded");
    document.querySelector("#hello").onclick =  Module.hello;
});

// mod.js
define([],function(){
    console.log("mod.js loaded");
    return {
        hello : function(){
            alert("hello");
        }
    };
});

index.html

<!DOCTYPE htm>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.2/require.js" data-main="app.js"></script>

<button id="hello">hello!</button>

 このコードを実行すると、以下のログが出力されます。

mod.js:2 mod.js loaded
app.js:3 app.js loaded

 つまり、(当然ではありますが)helloボタンを押す前にモジュール側が先に読み込まれるわけです。

 この形では、ロード時にすべてのjsファイルを読み込むような構造になるため、表示したいページ以外の本来不要なモジュールも全て初回に読み込んでしまい、パフォーマンス上の問題が生じます。

必要になるまで読み込まないよう(オンデマンド形式)にする

 それでは、先ほどの例を参考にして、app.jsを変更してみましょう。

// app.js
require([],function(){
    console.log("app.js loaded");
    document.querySelector("#hello").onclick = function(){
        require(["mod"],function(Module){
            Module.hello();
        });
    };
});

 ラップしているrequire関数では何も読み込まないようにして、clickイベントでrequireをコールするようにしています。これは、以下のようにも書けます。

console.log("app.js loaded");
document.querySelector("#hello").onclick = function(){
    require(["mod"],function(Module){
        Module.hello();
    });
};

 即時関数などで囲っても構いません。

(function(){
    console.log("app.js loaded");
    document.querySelector("#hello").onclick = function(){
        require(["mod"],function(Module){
            Module.hello();
        });
    };
})();

 実行してみると、ボタンを押すまでは、

app.js:2 app.js loaded

 としかコンソールには表示されませんが、ボタンを押した瞬間に、アラートと同時に、

mod.js:2 module.js loaded

 が表示され、モジュールがロードされたことがわかります。

仕組み

 至極単純なことですが、requirejsは、require([],function(){});が実行された瞬間にscriptタグを生成し、scriptのonloadでコールバック関数を実行しています。

 つまり、そのrequire関数を必要になるまで実行しないようにすれば、任意のタイミングでモジュールのロードを行うことができます。

注意点

 読み込みは非同期で行われますので、同期的な処理を行う場合は、Deferredパターンなどを組み合わせるとよいでしょう。

 JavaScriptは、シングルプロセスながらイベントによる割り込みを行うことで、非同期的な処理を可能とした言語です。  今回は、この非...

var xxx = require("xxx");の場合

 define(function(require){});などの構文でモジュールを定義する場合は、同期読み込み的な構文を書くことができます。app.jsを変更して実行結果を見てみましょう。

// app.js
define(function(require){
    console.log("app.js loaded");
    document.querySelector("#hello").onclick = function(){
        var Module = require("mod");
        Module.hello();
    };
});

 このコードを読んだ人は、var Module = require("mod");のタイミングで、初めてモジュールが読み込まれることを期待しますが、実際は、以下の順番で読み込まれます。

mod.js:2 mod.js loaded
app.js:3 app.js loaded

 これはどういうことでしょうか?

 requirejsのソースコードを調べてみると、2000行目付近に以下のようなコードが存在しています。

                callback
                    .toString()
                    .replace(commentRegExp, commentReplace)
                    .replace(cjsRequireRegExp, function (match, dep) {
                        deps.push(dep);
                    });

 defineのコールバックをtoStringしてreplaceしています。replaceの引数に渡されている正規表現は以下です。

commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/mg,
function commentReplace(match, singlePrefix) {
    return singlePrefix || '';
}
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,

 これは、functionの内容をStringとして構文解析して、事前に(defineで指定したときと同じタイミングで)読み込んでいます。

 考えてみれば当然の話で、jsファイルが非同期で読み込まれる関係上、戻り値を同期的に変数に格納するようなvar xxx = require("xxx")という関数は、どうやっても成立しないわけです。

 しかし、同期的に読み込む(ように見せる)ようなコードの方が可読性が高くなることも多いので、あえてこのような構文も用意しているのでしょう。

まとめ

 依存性管理として、require.jsは有力な候補のひとつです。本記事のような使い方をすれば、あらゆるjsファイルを必要になるまで読み込まなくすることができ、パフォーマンスに大きく寄与することが期待できます。

 うまく活用して、パフォーマンスの高いシステムを作りましょう!