SyntaxHighlighterの読込みを最適化する

SyntaxHighlighterの読込みを最小限にします。概要は次の通りです。

  • <pre>なしならば読み込まない
  • 必要なブラシは、最低限読み込む
  • 遅延読込みも合わせて更に高速化する

元ネタは、下記の記事です。不必要な強い同期処理を簡易なものに置き換えてます。その他、自分用のチューニングをいろいろ盛り込んでます。

コード

SyntaxHighlighterSimpleLoader.js/**
 * SyntaxHighlighterSimpleLoader.js
 * 最小ファイル読込み
 * JSとCSSの一本化(ファイル数削減)
 * SyntaxHighlighterの問題動作修正
 * 
 * [参考]
 * Dr.ウーパのコンピュータ備忘録
 * ページ表示速度改善:SyntaxHighlighter使用箇所があれば読み込む(完成スクリプトの配布)
 * see http://upa-pc.blogspot.com/2014/04/syntaxhighlighter28.html
 */
(function(document) {
  // --- 同期非同期関連 ----------------------------------------
  // すべての登録が完了後、実行開始を想定(以降、新規追加なし)
  const _syncfuncs = [];  // 実行待ち配列

  // 次の登録関数を実行する
  const nextSyncChain = function() {
    if (_syncfuncs.length != 0) {
      _syncfuncs.shift()();
    }
  };

  // 同期実行の関数を登録する
  const addSyncFunc = function(func) {
    _syncfuncs.push(function() {
      func();
      nextSyncChain();
    });
  };

  // 同期非同期実行の関数を登録する
  // 呼び出し関数内で nextSyncChain を実行すること
  const addSyncAsyncFunc = function(func) {
    _syncfuncs.push(func);
  };

  // chain >
  // sync   ----> chain
  // syncasync   ->   -> chain
  // sync               ----> chain
  // syncasync               ->   -> chain
  // sync                           ----> chain(end)
  // chain の呼び出しによって次関数を実行する。最後まで行ったら終わり。

  // --- js/css関連 --------------------------------------------

  // ローカルスタイル挿入(末尾追加:優先)
  const addLocalStyle = function(text) {
    const style = document.createElement('style');
    style.type = 'text/css';
    const rule = document.createTextNode(text);
    style.appendChild(rule);
    document.head.appendChild(style);
  };

  // css動的挿入(先頭追加:非優先)
  const addStyles = function(urls) {
    for (let i=0; i<urls.length; i++) {
      const style = document.createElement('link');
      style.rel = 'stylesheet';
      style.type = 'text/css';
      style.href = urls[i];
      document.head.appendChild(style);
    }
  };

  // js動的同期挿入
  const addScripts = function(urls) {
    const scripts = [];
    for (let i=0; i<urls.length; i++) {
      const script = document.createElement('script');
      script.type = 'text/javascript';
      script.async = true;
      script.src = urls[i];
      scripts.push(script);
    }

    // 非同期処理を同期実行(ファイルを読み込む後、次処理を実行する)
    addSyncAsyncFunc(function() {
      const sc = document.getElementsByTagName('script')[0];
      let count = 0;
      for (let i=0; i<scripts.length; i++) {
        const file = scripts[i];
        file.onload = file.onreadystatechange = function() {
          if (!file.readyState || /loaded|complete/.test(file.readyState)) {
            file.onload = file.onreadystatechange = null;
            count++;
            if (count == scripts.length) {
              nextSyncChain();
            }
          }
        };
        sc.parentNode.insertBefore(file, sc);
        //console.log('lazy: '+(file.href || file.src));
      }
    });
  };

  // --- main --------------------------------------------------
  // 最低限の SyntaxHighlighter を読み込む
  function main() {
    //console.log('lazy: main');
    // ※要初期設定
    const commonURL = 'https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/';
    const scriptURL = commonURL + 'scripts/';
    const styleURL = commonURL + 'styles/';

    // ページ内のブラシの確認
    // ※要初期設定
    const scripts = [];
    const brushs = [
//      {names:['applescript '], file:'shBrushAppleScript.min.js'},
//      {names:['as3', 'actionscript3'], file:'shBrushAS3.min.js'},
//      {names:['bash', 'shell'], file:'shBrushBash.min.js'},
//      {names:['cf', 'coldfusion'], file:'shBrushColdFusion.min.js'},
//      {names:['cpp', 'c'], file:'shBrushCpp.min.js'},
//      {names:['c-sharp', 'csharp'], file:'shBrushCSharp.min.js'},
      {names:['css'], file:'shBrushCss.min.js'},
//      {names:['delphi', 'pas', 'pascal'], file:'shBrushDelphi.min.js'},
//      {names:['diff', 'patch'], file:'shBrushDiff.min.js'},
//      {names:['erl', 'erlang'], file:'shBrushErlang.min.js'},
//      {names:['groovy'], file:'shBrushGroovy.min.js'},
//      {names:['java'], file:'shBrushJava.min.js'},
//      {names:['jfx', 'javafx'], file:'shBrushJavaFX.min.js'},
      {names:['js', 'jscript', 'javascript'], file:'shBrushJScript.min.js'},
//      {names:['perl', 'pl'], file:'shBrushPerl.min.js'},
//      {names:['php'], file:'shBrushPhp.min.js'},
      {names:['plain', 'text'], file:'shBrushPlain.min.js'},
//      {names:['ps', 'powershell'], file:'shBrushPowerShell.min.js'},
//      {names:['py', 'python'], file:'shBrushPython.min.js'},
//      {names:['rails', 'ror', 'ruby'], file:'shBrushRuby.min.js'},
//      {names:['sass '], file:'shBrushSass.min.js'},
//      {names:['scala'], file:'shBrushScala.min.js'},
//      {names:['sql'], file:'shBrushSql.min.js'},
//      {names:['vb', 'vbnet'], file:'shBrushVb.min.js'},
      {names:['xml', 'xhtml', 'xslt', 'html', 'xhtml'], file:'shBrushXml.min.js'}
    ];
    // <pre> を検索し、SyntaxHighlighter のブラシを検索する
    const tags = document.getElementsByTagName('pre');
    const re = /brush:\s*([^\s;]+)\s*;?/;
    for (let i=0; i<tags.length; i++) {
      // 不具合:単一の<pre>で複数ブラシを指定していると、2個目以降を読み込まない
      const found = tags[i].className.match(re);
      if (found != null) {
        // ブラシの追加
        for (let n=0; n<brushs.length; n++) {
          if (brushs[n].names.indexOf(found[1]) != -1) {
            //console.log('lazy: '+found[1]);
            scripts.push(scriptURL + brushs[n].file);
            brushs.splice(n, 1);        // 再実行防止
            break;
          }
        }
      }
    }

    // ページ内にブラシが存在したら、SyntaxHighlighterの使用準備を実行
    if (scripts.length > 0) {
      //console.log('lazy: load');
      // ※要初期設定
      addStyles([styleURL + 'shCoreFadeToGrey.min.css']);
      addLocalStyle("_{font-size:1.4rem!;border:1px solid #aaa!}_ *{cursor:auto}_ a,_ code,_ div,_ table,_ table caption,_ table tbody,_ table td,_ table thead,_ table tr,_ textarea{line-height:1!;min-height:1.6rem!}_ table caption{background-color:#666!;color:#ffe!;line-height:1.4!;padding:.25em 0 .125em 1em!}_ table td.code{padding:.25em 0!}_ .line.alt1,_ .line.alt2{background-color:#2c2c2c!}_ .plain,_ .plain a{color:#e6e6e6!}_ .comments,_ .comments a,_ .html.color2,_ .preprocessor{color:#87d46f!}_ .string,_ .string a{color:#ff91a7!}_ .keyword{color:#e9a7ff!}_ .value{color:#6cd0dc!}_.nogutter td.code .container textarea,_.nogutter td.code .line{padding-left:1em!}_ .line.highlighted.alt1,_ .line.highlighted.alt2{background-color:#146!}_ .gutter .line.highlighted{background-color:#3185b9!;color:#121212!}_ code{font-family:'Ricty Diminished','Noto Sans Mono CJK JP Regular','Source Han Code JP N',SFMono-Regular,Consolas,'Courier New',monospace!}".replace(/!/g, '!important').replace(/_/g, '.syntaxhighlighter'));

      // shCore.jsは、読込みが最初でない場合、
      // 「SyntaxHighlighter Cant find brush for: css」エラーが発生する。
      addScripts([scriptURL + 'shCore.min.js']);
      addScripts(scripts);
      addSyncFunc(function() {
        // ※要初期設定
        SyntaxHighlighter.config.bloggerMode = true;        // Bloggerで使用する
        SyntaxHighlighter.defaults['toolbar'] = false;      // ツールバー非表示
        SyntaxHighlighter.defaults['auto-links'] = false;   // URLに自動でリンクを設定しない
        SyntaxHighlighter.defaults['tab-size'] = 2;         // タブ数
        SyntaxHighlighter.defaults['quick-code'] = false;   // ダブルクリックでの全選択しない

        // ハイライト
        // 本スクリプトをページ読み込み後(ページ末尾)ロードすることで、
        // loadイベントで実行する`highlight()`を直接呼び出す。
        // 直接呼び出すことで高速化と同期処理を実現する。
        //SyntaxHighlighter.all();
        SyntaxHighlighter.highlight();
      });
      addSyncFunc(function() {
        // 空白行(文字のない行、スペースのみを含む)
        // 空白行の行末にスペースを追加する(SyntaxHighlighterの仕様?)を除去する。
        // 空白行にスペースすらない場合、HTMLを選択コピーすると、
        // 空白行(改行)が消える問題を回避するため、改行文字に置き換える。
        const space = '&nbsp;';
        const enter = '&#010;';
        const code = '</code>';
        const line = document.querySelectorAll('.syntaxhighlighter .code .container .line');
        for (let i=0; i<line.length; i++) {
          const text = line[i].innerHTML;
          //if (text.endsWith('&nbsp;')) {
          if (text.substring(text.length-space.length, text.length) === space) {
            const idx = text.lastIndexOf(code);
            if (idx != -1) {
              // 行末スペースを除去
              line[i].innerHTML = text.substring(0, idx+code.length);
            } else if (text.length == space.length) {
              line[i].innerHTML = enter;
            }
          }
        }
      });
      // スターター
      nextSyncChain();
    }
  };

  //console.log('lazy: init');
  main();
  //window.addEventListener('lazy', main);
})(document);

※「※要初期設定」を設定する
※遅延読込みは、関連参照

変更履歴

日付備考
2019/05/21初版
2019/12/13CSSをJS内部に保持するように修正
2020/02/08CSSの読込み順を修正(ローカルを後ろで読み込む)

備考

SyntaxHighlighterは、shCore.jsより先に他のスクリプトを読み込んではならない。しかし、他のスクリプトの読込み順は特に意識しない。

SyntaxHighlighterのスクリプトローダーは、shAutoloader.jsが標準で準備されているが、引数で指定したブラシを読み込んでいるだけです。動的に引数を作成すれば最小の読込みにできるが、自作すればshAutoloader.js分読込みを減らせる。自作ならば不必要な場合、shCore.jsと標準CSS分の読込みも減らせる。ただし、コード量で比べると自作しても大差はない。

関連