SyntaxHighlighterを最適化する

SyntaxHighlighterの読込み数を最小にします。

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

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

コード

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

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

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

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

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

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

  // css動的挿入
  function addStyles(urls, sync) {
    let styles = [];
    for (let i=0; i<urls.length; i++) {
      let style = doc.createElement('link');
      style.rel = 'stylesheet';
      style.type = 'text/css';
      style.href = urls[i];
      styles.push(style);
    }
    loadFile(styles, sync);
  }

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

  //  main -----> chain
  // async  ------->
  //  sync   >   ----> chain
  //  sync    >       ----> chain
  // async     ------->
  //  sync      >          ----> chain
  // 非同期処理は、即座に実行される
  // 同期処理は、実行待ち配列作成完了後、実行する(同期非同期処理を含む)
  // 実行待ち配列をすべて実行すると終了する

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

    // ページ内のブラシの確認
    // ※要初期設定
    let scripts = [];
    let 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 のブラシを検索する
    let tags = doc.getElementsByTagName('pre');
    for (let i=0; i<tags.length; i++) {
      // 不具合:単一の<pre>で複数ブラシを指定していると、2個目以降を読み込まない
      let found = tags[i].className.match(/brush:\s*([^\s;]+)\s*;?/);
      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'], false);
      // shCore.jsは、最初に読み込むこと、最初でない場合、
      // 「SyntaxHighlighter Can't find brush for: css」エラーが発生する。
      addScripts([scriptURL + 'shCore.min.js'], true);
      addScripts(scripts, true);
      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を選択コピーすると、
        // 空白行(改行)が消える問題を回避するため、改行文字に置き換える。
        let space = '&nbsp;';
        let enter = '&#010;';
        let code = '</code>';
        let line = doc.querySelectorAll('.syntaxhighlighter .code .container .line');
        for (let i=0; i<line.length; i++) {
          let text = line[i].innerHTML;
          //if (text.endsWith('&nbsp;')) {
          if (text.substring(text.length-space.length, text.length) === space) {
            let 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);

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

備考

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

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

関連記事

コメント: 0

コメントを書く