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

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

2014年4月30日水曜日

ページ表示速度改善:SyntaxHighlighter使用箇所があれば読み込む(完成スクリプトの配布)

はじめに

SyntaxHighlighterを使用するとページに記載したソースコードをきれいに表示することができます。

しかし、SyntaxHighlighterを利用する場合、(当然ですが)外部のJavaScriptとスタイルシートを読み込む必要があり、その分表示が遅くなります。
従って、SyntaxHighlighterを使用しているページならその表示遅延は許容せざるを得ませんが、SyntaxHighlighterを使用していないページまでその表示遅延を許容する必要はありません。

そこで、SyntaxHighlighterの使用状況に応じて、読み込むSyntaxHighlighterのJavaScriptとスタイルシートを動的に変更する仕組みを考えてみました。


前回まででコンセプトコードの検証作業は終了したため、今回はそこで完成したスクリプトを他の人が使用できるように全てのブラシに対応して一般化したスクリプトを作成し、配布します。


2014/5/1 追記:------------------------------------------------

【重要】ブラシの指定時に次のように ブラシ名の次に ; が付くケースでうまく対応する JavaScript ファイルを読み込めないケースがあったため、バグフィックスを行いました。

<pre class="brush: plain;">

バグフィックス詳細

追記ここまで ------------------------------------------------------


作業過程は「ページ表示速度改善:SyntaxHighlighter使用箇所があれば読み込む(完成スクリプトの配布)の作業」にて公開しています。



一般化したSyntaxHighlighter の動的必要最低限機能の読み込み JavaScript 

全てのブラシに対応した SyntaxHighlighter の動的必要最低限機能の読み込み JavaScript コードを以下に示します。
なお SyntaxHighlighter を公式ホスティングサービスではなく、自分自身の Web サーバなどでホスティングしている人はコード中の「http://alexgorbatchev.com/pub/sh/current/」の部分を自分自身の Web サーバなどのアドレスに変更してください。

<script type="text/javascript">
<!--
(function () {

    // --- Util -------------------------------------------
    // css動的挿入
    function addStyleSheet(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) {

        var script = document.createElement('script');
        script.setAttribute("type", "text/javascript");
        script.setAttribute("src", src);

        // 同期的に読み込むように指定されていた場合、
        // スクリプトの終了を検出するコードを埋め込み
        if (sync) {
            script.onload = script.onreadystatechange = function () {

                // onload イベント もしくは onreadystatechange イベントで 読み込みが完了状態 のいずれかだったら
                // IE と その他ブラウザに対応するために onload と onreadystatechange の両方のイベントに対応している
                if (!script.readyState || /loaded|complete/.test(script.readyState)) {
                    script.onload = script.onreadystatechange = null;

                    // 非同期メソッドの終了を通知する
                    runSync_NotifyAsyncMethodEnd()
                }
            };

            // 非同期処理である 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 () {
            syncRunningFlag = true;
            func();
            syncRunningFlag = false;
        });
    }

    // 同期実行:非同期メソッド
    function runSync_AsyncMethod(func) {
        runSync(function () {
            syncRunningFlag = true;
            func();
            /* syncRunningFlag = false; は非同期メソッドの終了イベントに委譲 */
        });
    }

    // 同期実行:非同期メソッドの終了を通知する
    function runSync_NotifyAsyncMethodEnd() {

        syncRunningFlag = false;

        // 同期的に実行するためのチェーン処理
        runSyncChain();
    }

    // 要素の同期的実行
    var syncFuncArray = new Array();        // 実行待ち配列(先頭から実行されていく)
    var syncRunningFlag = false;            // 同期実行中フラグ
    function runSync(func) {
        // 同期実行中かつ実行待ちメソッドがなければ即実行するが、
        // それ以外であれば、実行待ち状態にする
        if ((!syncRunningFlag) && (syncFuncArray.length == 0)) {
            runSyncChainAfterRunFunc(func);

        } else {
            syncFuncArray.push(function () {
                runSyncChainAfterRunFunc(func);
            });
        }
    }

    // 同期処理の必要なメソッドを実行した後に、同期的に実行するためのチェーン処理を実施
    function runSyncChainAfterRunFunc(func) {

        func();
        runSyncChain();

    }

    // 同期的に実行するためのチェーン処理
    function runSyncChain() {
        // 処理未実行状態 かつ 実行待ちメソッドがある場合に実行待ちメソッドを実行
        if (!syncRunningFlag) {
            if (syncFuncArray.length > 0) {
                // 先頭の実行待ちメソッドを取り出し、実行待ちから削除した後実行
                var func = syncFuncArray[0];
                syncFuncArray.splice(0, 1);
                func();
            }
        }
    }

    // --- main -------------------------------------------

    LoadMinimumSyntaxHighlighter();
    // 最低限の SyntaxHighlighter を読み込む
    function LoadMinimumSyntaxHighlighter() {

        var commonURL = "http://alexgorbatchev.com/pub/sh/current/";        // 共通 URL
        var scriptURL = commonURL + "scripts/";
        var cssURL = commonURL + "styles/";

        // ブラシの名前とファイルを対応付けるクラス
        function CBrushRelation(name, brushNames, file) {
            this.name = name;                   // 関連付け名
            this.brushNames = brushNames;       // ブラシ名の配列
            this.file = file;                   // ブラシのファイル
        };

        // ブラシの関連付け配列
        var brushRelations = [
                new CBrushRelation("ActionScript3", ["as3", "actionscript3"], "shBrushAS3.js"),
                new CBrushRelation("Bash/shell", ["bash", "shell"], "shBrushBash.js"),
                new CBrushRelation("ColdFusion", ["cf", "coldfusion"], "shBrushColdFusion.js"),
                new CBrushRelation("C#", ["c-sharp", "csharp"], "shBrushCSharp.js"),
                new CBrushRelation("C++", ["cpp", "c"], "shBrushCpp.js"),
                new CBrushRelation("CSS", ["css"], "shBrushCss.js"),
                new CBrushRelation("Delphi", ["delphi", "pas", "pascal"], "shBrushDelphi.js"),
                new CBrushRelation("Diff", ["diff", "patch"], "shBrushDiff.js"),
                new CBrushRelation("Erlang", ["erl", "erlang"], "shBrushErlang.js"),
                new CBrushRelation("Groovy", ["groovy"], "shBrushGroovy.js"),
                new CBrushRelation("JavaScript", ["js", "jscript", "javascript"], "shBrushJScript.js"),
                new CBrushRelation("Java", ["java"], "shBrushJava.js"),
                new CBrushRelation("JavaFX", ["jfx", "javafx"], "shBrushJavaFX.js"),
                new CBrushRelation("Perl", ["perl", "pl"], "shBrushPerl.js"),
                new CBrushRelation("PHP", ["php"], "shBrushPhp.js"),
                new CBrushRelation("Plain Text", ["plain", "text"], "shBrushPlain.js"),
                new CBrushRelation("PowerShell", ["ps", "powershell"], "shBrushPowerShell.js"),
                new CBrushRelation("Python", ["py", "python"], "shBrushPython.js"),
                new CBrushRelation("Ruby", ["rails", "ror", "ruby"], "shBrushRuby.js"),
                new CBrushRelation("Scala", ["scala"], "shBrushScala.js"),
                new CBrushRelation("SQL", ["sql"], "shBrushSql.js"),
                new CBrushRelation("Visual Basic", ["vb", "vbnet"], "shBrushVb.js"),
                new CBrushRelation("XML", ["xml", "xhtml", "xslt", "html", "xhtml"], "shBrushXml.js")
            ];

        // 各ブラシ用の URL を関連付けから作成
        var brushURLs = new Array();                                               // 各ブラシ用 URL
        for (var i = 0; i < brushRelations.length; i++) {
            brushURLs[brushRelations[i].name] = scriptURL + brushRelations[i].file;
        }

        /*
        <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;]+)(\s*;*)/;     // ブラシを発見するための正規表現
            var found = preTags[i].className.match(target);

            if (found != null) {

                // 初回発見時は共通データの読み込みを実施
                if (brushCount == 0) {
                    addStyleSheet(cssURL + 'shCore.css');
                    addStyleSheet(cssURL + 'shThemeDefault.css');

                    addScript(scriptURL + 'shCore.js', true);
                }
                // ブラシ名からスクリプトを読み込む
                addScriptFromBrush(found[2]);
                brushCount++;
            }
        }

        // ページ内にブラシが存在したら、SyntaxHighlighter の使用準備を実行
        if (brushCount > 0) {

            runSync_SyncMethod(function () {
                SyntaxHighlighter.config.bloggerMode = true;
                SyntaxHighlighter.all();
            });

        }
        // 各ブラシに対応するスクリプトの読み込みを行う
        function addScriptFromBrush(brush) {

            // ブラシの関連付けからブラシに対応するブラシの URL を特定する
            for (var i = 0; i < brushRelations.length; i++) {
                for (var brushNameCount = 0; brushNameCount < brushRelations[i].brushNames.length; brushNameCount++) {

                    if (brush == brushRelations[i].brushNames[brushNameCount]) {

                        // 初回のみスクリプトの読み込みを実施
                        addScriptFirst(brushRelations[i].name, brushURLs);
                        return;
                    }
                }
            }
        }

        // 初回のみスクリプトの読み込みを実施
        function addScriptFirst(type, URLs) {

            if (URLs[type] != "") {

                addScript(URLs[type], true);
                URLs[type] = "";
            }
        }
    }

})();

//-->
</script>


最適化した「一般化した SyntaxHighlighter の動的必要最低限機能の読み込み JavaScript」  

JavaScript コードは、Closure Compiler(Googleが提供しているコード圧縮・最適化ツール)にて最適化しました。
上に示したソースコードに一切手を加えずにそのままコードを使用したい人は、以下のコードを SyntaxHighlighter で装飾を加えたい箇所よりも後の位置に挿入してください。(<body>~</body>内)

<script type="text/javascript">
<!--
(function(){function k(a){var d=document.createElement("link");d.setAttribute("rel","stylesheet");d.setAttribute("type","text/css");d.setAttribute("href",a);g(d)}function l(a,d){var b=document.createElement("script");b.setAttribute("type","text/javascript");b.setAttribute("src",a);d?(b.onload=b.onreadystatechange=function(){if(!b.readyState||/loaded|complete/.test(b.readyState))b.onload=b.onreadystatechange=null,e=!1,h()},n(function(){g(b)})):g(b)}function g(a){document.getElementsByTagName("head")[0].appendChild(a)}
function p(a){m(function(){e=!0;a();e=!1})}function n(a){m(function(){e=!0;a()})}function m(a){e||0!=c.length?c.push(function(){a();h()}):(a(),h())}function h(){if(!e&&0<c.length){var a=c[0];c.splice(0,1);a()}}var c=[],e=!1;(function(){function a(a,b,e){this.name=a;this.brushNames=b;this.file=e}function d(a){for(var c=0;c<b.length;c++)for(var d=0;d<b[c].brushNames.length;d++)if(a==b[c].brushNames[d]){a=b[c].name;c=e;""!=c[a]&&(l(c[a],!0),c[a]="");return}}for(var b=[new a("ActionScript3",["as3","actionscript3"],
"shBrushAS3.js"),new a("Bash/shell",["bash","shell"],"shBrushBash.js"),new a("ColdFusion",["cf","coldfusion"],"shBrushColdFusion.js"),new a("C#",["c-sharp","csharp"],"shBrushCSharp.js"),new a("C++",["cpp","c"],"shBrushCpp.js"),new a("CSS",["css"],"shBrushCss.js"),new a("Delphi",["delphi","pas","pascal"],"shBrushDelphi.js"),new a("Diff",["diff","patch"],"shBrushDiff.js"),new a("Erlang",["erl","erlang"],"shBrushErlang.js"),new a("Groovy",["groovy"],"shBrushGroovy.js"),new a("JavaScript",["js","jscript",
"javascript"],"shBrushJScript.js"),new a("Java",["java"],"shBrushJava.js"),new a("JavaFX",["jfx","javafx"],"shBrushJavaFX.js"),new a("Perl",["perl","pl"],"shBrushPerl.js"),new a("PHP",["php"],"shBrushPhp.js"),new a("Plain Text",["plain","text"],"shBrushPlain.js"),new a("PowerShell",["ps","powershell"],"shBrushPowerShell.js"),new a("Python",["py","python"],"shBrushPython.js"),new a("Ruby",["rails","ror","ruby"],"shBrushRuby.js"),new a("Scala",["scala"],"shBrushScala.js"),new a("SQL",["sql"],"shBrushSql.js"),
new a("Visual Basic",["vb","vbnet"],"shBrushVb.js"),new a("XML",["xml","xhtml","xslt","html","xhtml"],"shBrushXml.js")],e=[],f=0;f<b.length;f++)e[b[f].name]="http://alexgorbatchev.com/pub/sh/current/scripts/"+b[f].file;for(var c=0,g=document.getElementsByTagName("pre"),f=0;f<g.length;f++){var h=g[f].className.match(/(brush:\s*)([^\s;]+)(\s*;*)/);null!=h&&(0==c&&(k("http://alexgorbatchev.com/pub/sh/current/styles/shCore.css"),k("http://alexgorbatchev.com/pub/sh/current/styles/shThemeDefault.css"),
l("http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js",!0)),d(h[2]),c++)}0<c&&p(function(){SyntaxHighlighter.config.bloggerMode=!0;SyntaxHighlighter.all()})})()})();
//-->
</script>

なお、Bloggerへの設置方法は前回までの記事を参照してください。

効率化率

効率化前の JavaScript コードのサイズ:

バグフィックス後:8.92 KB (9,143 バイト) ←無駄な空行を削除したりしたので、バグフィックスの修正以上にファイルサイズが減っています。
 バグフィックス前:9.70 KB (9,933 バイト)

効率化後の JavaScript コードのサイズ:

バグフィックス後:2.67 KB (2,736 バイト)
 バグフィックス前:2.66 KB (2,728 バイト)

効率化によってコードサイズは 1/4 弱 になりました。

まとめ

全てのブラシに対応して一般化したスクリプトを作成しました。

SyntaxHighlighter の外部ファイルへの参照を最低限にしたい人は使ってみるとよいかもしれません。






関連記事

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

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