タイトルが長かったため
「SyntaxHighlighter:ページの表示速度を改善するため、ページ内にSyntaxHighlighterを使用する箇所がある場合に、jsとcssを読み込むようにした」
を
「ページ表示速度改善:SyntaxHighlighter使用箇所があれば読み込む」
に修正しました。
はじめに
SyntaxHighlighterを使用するとページに記載したソースコードをきれいに表示することができます。しかし、SyntaxHighlighterを利用する場合、(当然ですが)外部のJavaScriptとスタイルシートを読み込む必要があり、その分表示が遅くなります。
従って、SyntaxHighlighterを使用しているページならその表示遅延は許容せざるを得ませんが、SyntaxHighlighterを使用していないページまでその表示遅延を許容する必要はありません。
そこで、SyntaxHighlighterの使用状況に応じて、読み込むSyntaxHighlighterのJavaScriptとスタイルシートを動的に変更する仕組みを考えてみました。
前提
そもそもSyntaxHighlighterには、オートローダーという仕組みがあります。これはページ内で使用しているSyntaxHighlighterの機能に応じて、必要最低限のJavaScriptを読み込むというものです。
(SyntaxHighlighterは装飾するソースコードの種類ごとに、個別のブラシファイル(JavaScript)を読み込みます。
例えば、C++用のブラシであったり、JavaScript用のブラシなどその数は何十種類にも及びます。
そのため、共通的にSyntaxHighlighterの読み込み処理をページに記載しておくと、そのページでは使用しないブラシファイルまで読み込まれることになり、無駄が生じることになります。)
しかしながら、オートローダーを使用するとかえって遅くなるという情報があったり(*1)、そもそもSyntaxHighlighterのファイル自体を読み込みたくないと考えていたため、別の手段を考えることにしました。
考案した処理方式
そのページで利用している SyntaxHighlighter の機能に応じて、必要最低限のJavaScriptを読み込むには、動的に JavaScript でページを解析して、JavaScript を読み込むのが手っ取り早く、楽な方法だと思いました。そこでページ内に記載されている<pre class="brush: js"></pre>を検出し、classに記載されているbrushに対応した javascript ファイルを読み込みます。
更に不要なファイルの読み込みを抑えるために、ページ内に SyntaxHighlighter を利用している箇所が無い場合には、SyntaxHighlighter のスタイルシートや共通の JavaScript すら読み込まない(SyntaxHighlighter をまったく読み込まない)という動作を実現したいと思います。
コンセプトコード
<html>
<head>
<title>test page</title>
</head>
<body>
<h1>TEST.</h1>
<div id="post-body-1234">
記事本文です。
</div>
<pre class="brush: js">
function () {
    /* javascript */
}
</pre>
<pre class="brush:xml">
<html>
<!-- xml -->
</html>
</pre>
<pre>
何もなし
</pre>
<script type="text/javascript">
<!--
    (function (){
        // --- デバッグ用 Util -------------------------------------------
        (function () {
            // consoleが使えない場合は空のオブジェクトを設定しておく
            if (typeof console === "undefined") {
                console = {};
            }
            // console.@@がメソッドでない場合は空のメソッドを用意する
            if (typeof console.log !== "function") {
                console.log = function () { };
            }
        })();
        // --- Util -------------------------------------------
        // css動的挿入
        function addStyleSheet(href) {
            console.log("LoadMinimumSyntaxHighlighter, addStyleSheet, href=" + href);
            var link = document.createElement("link");
            link.setAttribute("rel", "stylesheet");
            link.setAttribute("type", "text/css");
            link.setAttribute("href", href);
            header_setChild(link);
        }
        // JavaScript動的挿入
        function addScript(src, sync) {
            console.log("LoadMinimumSyntaxHighlighter, addScript, src=" + src + ", sync=" + sync);
            var script = document.createElement('script');
            script.setAttribute("type", "text/javascript");
            script.setAttribute("src", src);
            // 同期的に読み込むように指定されていた場合、
            // スクリプトの終了を検出するコードを埋め込み
            if (sync) {
                script.onload = script.onreadystatechange = function () {
                    console.log("LoadMinimumSyntaxHighlighter, onload|onreadystatechange, script.readyState=" + script.readyState);
                    // onload イベント もしくは onreadystatechange イベントで 読み込みが完了状態 のいずれかだったら
                    // IE と その他ブラウザに対応するために onload と onreadystatechange の両方のイベントに対応している
                    if (!script.readyState || /loaded|complete/.test(script.readyState)) {
                        script.onload = script.onreadystatechange = null;
                    
                        // 非同期メソッドの終了を通知する
                        runSync_NotifyAsyncMethodEnd()
                        console.log("LoadMinimumSyntaxHighlighter, ScriptLoaded, script.readyState=" + script.readyState);
                    }
                };
                // 非同期処理である JavaScript の動的挿入を同期的に実行する
                runSync_AsyncMethod(function () {
                    header_setChild(script);
                });
            } else {
                header_setChild(script);
            }
        }
        // <head>取得
        function getHeader() {
            return document.getElementsByTagName("head")[0];
        }
        // <head>に子要素を追加
        function header_setChild(child) {
            var head = getHeader();
            head.appendChild(child);
        }
        // 同期実行:即終了メソッド
        function runSync_SyncMethod(func) {
            runSync(function () {
                console.log("LoadMinimumSyntaxHighlighter, SyncMethod, Start");
                syncRunningFlag = true;
                func();
                syncRunningFlag = false;
                console.log("LoadMinimumSyntaxHighlighter, SyncMethod, End");
            });
        }
        // 同期実行:非同期メソッド
        function runSync_AsyncMethod(func) {
            runSync(function () {
                console.log("LoadMinimumSyntaxHighlighter, AsyncMethod, Start");
                syncRunningFlag = true;
                func();
                /* syncRunningFlag = false; は非同期メソッドの終了イベントに委譲 */
                console.log("LoadMinimumSyntaxHighlighter, AsyncMethod, TrigEnd");
            });
        }
        // 同期実行:非同期メソッドの終了を通知する
        function runSync_NotifyAsyncMethodEnd() {
            syncRunningFlag = false;
            // 同期的に実行するためのチェーン処理
            runSyncChain();
        }
        // 要素の同期的実行
        var syncFuncArray = new Array();        // 実行待ち配列(先頭から実行されていく)
        var syncRunningFlag = false;            // 同期実行中フラグ
        function runSync(func) {
            // 同期実行中かつ実行待ちメソッドがなければ即実行するが、
            // それ以外であれば、実行待ち状態にする
            if ((!syncRunningFlag) && (syncFuncArray.length == 0)) {
                console.log("LoadMinimumSyntaxHighlighter, runSync, NowRun");
                runSyncChainAfterRunFunc(func);
            } else {
                console.log("LoadMinimumSyntaxHighlighter, runSync, RunLater");
                syncFuncArray.push(function () {
                    runSyncChainAfterRunFunc(func);
                });
            }
        }
        // 同期処理の必要なメソッドを実行した後に、同期的に実行するためのチェーン処理を実施
        function runSyncChainAfterRunFunc(func) {
            func();
            runSyncChain();
        }
        // 同期的に実行するためのチェーン処理
        function runSyncChain() {
            console.log("LoadMinimumSyntaxHighlighter, runSyncChain, Start, syncRunningFlag=" + syncRunningFlag + ", syncFuncArray.length=" + syncFuncArray.length);
            // 処理未実行状態 かつ 実行待ちメソッドがある場合に実行待ちメソッドを実行
            if (!syncRunningFlag) {
                if (syncFuncArray.length > 0) {
                    console.log("LoadMinimumSyntaxHighlighter, runSyncChain, RunSyncChain, Start");
                    // 先頭の実行待ちメソッドを取り出し、実行待ちから削除した後実行
                    var func = syncFuncArray[0];
                    syncFuncArray.splice(0, 1);
                    func();
                    console.log("LoadMinimumSyntaxHighlighter, runSyncChain, RunSyncChain, End");
                }
            }
            console.log("LoadMinimumSyntaxHighlighter, runSyncChain, End, syncRunningFlag=" + syncRunningFlag + ", syncFuncArray.length=" + syncFuncArray.length);
        }
        // --- main -------------------------------------------
    
        LoadMinimumSyntaxHighlighter();
    
        // 最低限の SyntaxHighlighter を読み込む
        function LoadMinimumSyntaxHighlighter() {
            console.log("LoadMinimumSyntaxHighlighter, Start");
            var commonURL = "http://alexgorbatchev.com/pub/sh/current/";        // 共通 URL
            var scriptURL = commonURL + "scripts/";
            var cssURL = commonURL + "styles/";
            var brushURLs = new Array();                                                // 各ブラシ用 URL
            brushURLs["js"] = scriptURL + 'shBrushJScript.js';
            brushURLs["xml"] = scriptURL + 'shBrushXml.js';
            /*
             <pre> を検索し、SyntaxHighlighter のブラシを検索する 
             ブラシが見つかったら、ブラシに応じた js を読み込む
             初期発見時には共通の css と js を読み込む
             */
            var brushCount = 0;                         // 見つかったブラシの数
            var preTags = document.getElementsByTagName("pre");
            for (var i = 0; i < preTags.length; i++) {
                var target = /(brush:\s*)([^\s]+)/;     // ブラシを発見するための正規表現
                var found = preTags[i].className.match(target);
                if (found != null) {
                    // 初回発見時は共通データの読み込みを実施
                    if (brushCount == 0) {
                        console.log("LoadMinimumSyntaxHighlighter, LoadCommon, Start");
                        addStyleSheet(cssURL + 'shCore.css');
                        addStyleSheet(cssURL + 'shThemeDefault.css');
                        addScript(scriptURL + 'shCore.js', true);
                        console.log("LoadMinimumSyntaxHighlighter, LoadCommon, End");
                    }
                    switch (found[2]) {
                        case "js":
                            addScriptFirst("js", brushURLs);
                            break;
                        case "xml":
                            addScriptFirst("xml", brushURLs);
                            break;
                    }
                
                    brushCount++;
                }
            }
            // ページ内にブラシが存在したら、SyntaxHighlighter の使用準備を実行
            if (brushCount > 0) {
                runSync_SyncMethod(function () {
                    console.log("LoadMinimumSyntaxHighlighter, SyntaxHighlighter, Init, Start");
                    SyntaxHighlighter.config.bloggerMode = true;
                    SyntaxHighlighter.all();
                    console.log("LoadMinimumSyntaxHighlighter, SyntaxHighlighter, Init, End");
                });
            }
            console.log("LoadMinimumSyntaxHighlighter, End");
            // 初回のみスクリプトの読み込みを実施
            function addScriptFirst(type, URLs) {
                if (URLs[type] != "") {
                    console.log("LoadMinimumSyntaxHighlighter, addScriptFirst, type=" + type + ", URLs[type]=" + URLs[type]);
                    addScript(URLs[type], true);
                    URLs[type] = "";
                }
            }
        }
    })();
//-->
</script>
</body>
</html>
技術情報
スタイルシートと JavaScript を動的に読み込む方法とソースコードは(*2)のサイトを参考にさせていただきました。
デバッグ用のコンソールログ出力プログラムは(*3)のサイトを参考にさせていただきました。
その他プログラム上の参考資料は(*4)のサイトを参考にさせていただきました。
JavaScriptとスタイルシートの動的な読み込みは<head>タグに後から <link>タグと<script>タグを挿入することで実現しています。
その際に、JavaScriptを動的に読み込むと非同期で JavaScript の読み込みと実行が行われてしまいます。そうすると、スクリプトの途中で SyntaxHighlighter オブジェクトを参照した際に、まだ SyntaxHighlighter オブジェクトが読み込まれておらず、エラーが発生してしまいます。
それを防止するために、動的な JavaScript の読み込み処理は、先に読み込むように指示した JavaScript が完全に読み込み終わった後に、次の JavaScript の読み込み処理を実施するように制御しています。
実行結果
ブラシとして js と xml があるケース
画面・読み込まれたソース
JavaScript用ブラシ(shBrushJScript.js)とxml用ブラシ(shBrushXml.js)と共通のSyntaxHighlighter のコアとなるスクリプト(shCore.js)が期待通り読み込まれたことがわかります。
JavaScript用ブラシ(shBrushJScript.js)とxml用ブラシ(shBrushXml.js)と共通のSyntaxHighlighter のコアとなるスクリプト(shCore.js)が期待通り読み込まれた。
デバッグログ
console.log()で出力したデバッグログを以下に示します。JavaScriptを動的に読み込む際に、最初に登録したものから排他的に順次実行されていく様子がわかります。
LoadMinimumSyntaxHighlighter, Start try1.html:237 LoadMinimumSyntaxHighlighter, LoadCommon, Start try1.html:262 LoadMinimumSyntaxHighlighter, addStyleSheet, href=http://alexgorbatchev.com/pub/sh/current/styles/shCore.css try1.html:84 LoadMinimumSyntaxHighlighter, addStyleSheet, href=http://alexgorbatchev.com/pub/sh/current/styles/shThemeDefault.css try1.html:84 LoadMinimumSyntaxHighlighter, addScript, src=http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js, sync=true try1.html:96 LoadMinimumSyntaxHighlighter, runSync, NowRun try1.html:185 LoadMinimumSyntaxHighlighter, AsyncMethod, Start try1.html:158 LoadMinimumSyntaxHighlighter, AsyncMethod, TrigEnd try1.html:164 LoadMinimumSyntaxHighlighter, runSyncChain, Start, syncRunningFlag=true, syncFuncArray.length=0 try1.html:210 LoadMinimumSyntaxHighlighter, runSyncChain, End, syncRunningFlag=true, syncFuncArray.length=0 try1.html:227 LoadMinimumSyntaxHighlighter, LoadCommon, End try1.html:269 LoadMinimumSyntaxHighlighter, addScriptFirst, type=js, URLs[type]=http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJScript.js try1.html:307 LoadMinimumSyntaxHighlighter, addScript, src=http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJScript.js, sync=true try1.html:96 LoadMinimumSyntaxHighlighter, runSync, RunLater try1.html:191 LoadMinimumSyntaxHighlighter, addScriptFirst, type=xml, URLs[type]=http://alexgorbatchev.com/pub/sh/current/scripts/shBrushXml.js try1.html:307 LoadMinimumSyntaxHighlighter, addScript, src=http://alexgorbatchev.com/pub/sh/current/scripts/shBrushXml.js, sync=true try1.html:96 LoadMinimumSyntaxHighlighter, runSync, RunLater try1.html:191 LoadMinimumSyntaxHighlighter, runSync, RunLater try1.html:191 LoadMinimumSyntaxHighlighter, End try1.html:300 LoadMinimumSyntaxHighlighter, onload|onreadystatechange, script.readyState=undefined try1.html:106 LoadMinimumSyntaxHighlighter, runSyncChain, Start, syncRunningFlag=false, syncFuncArray.length=3 try1.html:210 LoadMinimumSyntaxHighlighter, runSyncChain, RunSyncChain, Start try1.html:216 LoadMinimumSyntaxHighlighter, AsyncMethod, Start try1.html:158 LoadMinimumSyntaxHighlighter, AsyncMethod, TrigEnd try1.html:164 LoadMinimumSyntaxHighlighter, runSyncChain, Start, syncRunningFlag=true, syncFuncArray.length=2 try1.html:210 LoadMinimumSyntaxHighlighter, runSyncChain, End, syncRunningFlag=true, syncFuncArray.length=2 try1.html:227 LoadMinimumSyntaxHighlighter, runSyncChain, RunSyncChain, End try1.html:223 LoadMinimumSyntaxHighlighter, runSyncChain, End, syncRunningFlag=true, syncFuncArray.length=2 try1.html:227 LoadMinimumSyntaxHighlighter, ScriptLoaded, script.readyState=undefined try1.html:116 LoadMinimumSyntaxHighlighter, onload|onreadystatechange, script.readyState=undefined try1.html:106 LoadMinimumSyntaxHighlighter, runSyncChain, Start, syncRunningFlag=false, syncFuncArray.length=2 try1.html:210 LoadMinimumSyntaxHighlighter, runSyncChain, RunSyncChain, Start try1.html:216 LoadMinimumSyntaxHighlighter, AsyncMethod, Start try1.html:158 LoadMinimumSyntaxHighlighter, AsyncMethod, TrigEnd try1.html:164 LoadMinimumSyntaxHighlighter, runSyncChain, Start, syncRunningFlag=true, syncFuncArray.length=1 try1.html:210 LoadMinimumSyntaxHighlighter, runSyncChain, End, syncRunningFlag=true, syncFuncArray.length=1 try1.html:227 LoadMinimumSyntaxHighlighter, runSyncChain, RunSyncChain, End try1.html:223 LoadMinimumSyntaxHighlighter, runSyncChain, End, syncRunningFlag=true, syncFuncArray.length=1 try1.html:227 LoadMinimumSyntaxHighlighter, ScriptLoaded, script.readyState=undefined try1.html:116 LoadMinimumSyntaxHighlighter, onload|onreadystatechange, script.readyState=undefined try1.html:106 LoadMinimumSyntaxHighlighter, runSyncChain, Start, syncRunningFlag=false, syncFuncArray.length=1 try1.html:210 LoadMinimumSyntaxHighlighter, runSyncChain, RunSyncChain, Start try1.html:216 LoadMinimumSyntaxHighlighter, SyncMethod, Start try1.html:145 LoadMinimumSyntaxHighlighter, SyntaxHighlighter, Init, Start try1.html:290 LoadMinimumSyntaxHighlighter, SyntaxHighlighter, Init, End try1.html:295 LoadMinimumSyntaxHighlighter, SyncMethod, End try1.html:151 LoadMinimumSyntaxHighlighter, runSyncChain, Start, syncRunningFlag=false, syncFuncArray.length=0 try1.html:210 LoadMinimumSyntaxHighlighter, runSyncChain, End, syncRunningFlag=false, syncFuncArray.length=0 try1.html:227 LoadMinimumSyntaxHighlighter, runSyncChain, RunSyncChain, End try1.html:223 LoadMinimumSyntaxHighlighter, runSyncChain, End, syncRunningFlag=false, syncFuncArray.length=0 try1.html:227 LoadMinimumSyntaxHighlighter, ScriptLoaded, script.readyState=undefined try1.html:116
ブラシとして js のみがあるケース
画面
JavaScript用ブラシ(shBrushJScript.js)と共通のSyntaxHighlighter のコアとなるスクリプト(shCore.js)が期待通り読み込まれたことがわかります。
JavaScript用ブラシ(shBrushJScript.js)と共通のSyntaxHighlighter のコアとなるスクリプト(shCore.js)が期待通り読み込まれた。
SyntaxHighlighter が使われていないケース
画面
期待通り SyntaxHighlighter のファイルが一切読み込まれなかったことがわかります。
期待通り SyntaxHighlighter のファイルが一切読み込まれなかった。
IE での動作も確認
画面
IE でも期待通りに動作することを確認しました。
IE でも期待通りに動作することを確認。
まとめ
JavaScript を使用して SyntaxHighlighter の JavaScript とスタイルシートを動的に必要最低限のものを選択して読み込めることを確認しました。次回はこのソースコードを実際に Blogger に設置してみたいと思います。
コメントを投稿
コメント投稿機能について