日々のコンピュータ情報の集積と整理

Dr.ウーパのコンピュータ備忘録

2014年4月25日金曜日

ページ表示速度改善:SyntaxHighlighter使用箇所があれば読み込む

追記:

タイトルが長かったため

「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">
&lt;html&gt;
&lt;!-- xml --&gt;
&lt;/html&gt;
</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)が期待通り読み込まれたことがわかります。



SyntaxHighlighter_loader_fs xml

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)が期待通り読み込まれたことがわかります。

SyntaxHighlighter_loader_js

JavaScript用ブラシ(shBrushJScript.js)と共通のSyntaxHighlighter のコアとなるスクリプト(shCore.js)が期待通り読み込まれた。



SyntaxHighlighter が使われていないケース

画面


期待通り SyntaxHighlighter のファイルが一切読み込まれなかったことがわかります。

SyntaxHighlighter_loader_none

期待通り SyntaxHighlighter のファイルが一切読み込まれなかった。



IE での動作も確認

画面


IE でも期待通りに動作することを確認しました。

SyntaxHighlighter_loader_fs xml_ie

IE でも期待通りに動作することを確認。



まとめ

JavaScript を使用して SyntaxHighlighter の JavaScript とスタイルシートを動的に必要最低限のものを選択して読み込めることを確認しました。

次回はこのソースコードを実際に Blogger に設置してみたいと思います。

参考文献

*1

Blogger:SyntaxHighlighter(6)オートローダーを使う場合と使わない場合の表示速度の比較

http://p--q.blogspot.jp/2013/09/bloggersyntaxhighlighter6.html

Blogger:SyntaxHighlighter(4)オートローダーで必要なブラシファイルのみロードする

http://p--q.blogspot.jp/2013/08/bloggersyntaxhighlighter4.html


*2

CSSとJavaScriptの動的注入

http://ichitcltk.hustle.ne.jp/lib/php/sample_page.php?source=/gudon/rd/ajax/html/inject_CSS_JS.html&return_url=/gudon/modules/pico_rd/index.php?content_id=31

*3

JavaScript開発のデバッグを加速するlog出力&#65281; | WebTerminal

http://web-terminal.blogspot.jp/2013/04/javascriptlog.html#Lets-output-the-log-to-the-JavaScript-console

*4

JavaScriptの配列の要素を削除する(delete演算子とspliceメソッド) | 山本隆の開発日誌

http://www.gesource.jp/weblog/?p=4112

Array.push - 配列の最後に値を追加 - JavaScriptリファレンス

http://javascriptist.net/ref/Array.push.html

Array - JavaScript | MDN

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array#Methods

JavaScriptのclassName属性は,要素の複数のクラス名をスペース区切りで取得する - プログラミングとIT技術をコツコツ勉強するブログ

http://d.hatena.ne.jp/TipsMemo+computer-technology/20140104/p2

element.className - エレメントのclassを取得/設定 - JavaScriptリファレンス

http://javascriptist.net/ref/element.className.html

配列(Array)

http://www.tohoho-web.com/js/array.htm

同期した js ファイルの読み込み方法 - Yahoo!知恵袋

http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1462557389


正規表現 - JavaScript | MDN

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions

オライリーの「JavaScriptパターン」を読んだ - takanamitoのブログ

http://takanamito.hateblo.jp/entry/2013/04/12/000357




関連記事

関連記事を読み込み中...

同じラベルの記事を読み込み中...