HTML圧縮機を作ってみた

完成品は、次のリンク先です。

既存のHTML圧縮の問題点

既存のHTML圧縮は、みな大小様々な問題を抱えています。次に例を示します。

  1. HTML minifier
    • 特になし(これでよかった)
    • 正しくないHTMLを入力してもそのまま処理される
      • 利点であり、欠点です
    • 設定が多くてわかりにくいのが難点です
      • 利点でもあります
      • 問題が起きた時どの設定が原因か調べるので一苦労する(そして、忘れる)
      • 次の項目のみチェックすると問題を起こさずある程度圧縮できます
        • Collapse whitespace - ホワイトスペース削除
        • HTML5 - noscript対策
        • Keep closing slash - meta対策
        • Preserve line-breaks - レイアウト対策
        • Remove comments - コメント削除
        • Trim white space around custom fragments - 空白削除
  2. HTML圧縮 | TM - WebTools
    • HTML用のため、XMLを処理できない(エラー扱いとなる)
    • 次のコードを処理できない
      • <html><body>&lt;!--</body>--&gt;&lt;/body&gt;</html>
      • 一般的なコードではないが、Bloggerでは必須で処理して欲しいコードです
  3. Compress HTML Source Code
    • サーバーが重い
    • HTTPS未対応
      • 攻撃者に送信データを盗まれる可能性がある
      • 攻撃者が受信データに意図しないコードを挿入する可能性がある
  4. HTML Minifier - Minify HTML and any CSS or JS included in your markup
    • スクリプト内のホワイトスペースを削除する
      • 1行コメントがある場合、動作しない
  5. Compress HTML
    • スクリプト内のホワイトスペースを削除する
      • 1行コメントがある場合、動作しない

本ウェブアプリについて

XML(HTML)からホワイトスペース(スペース・タブ・改行)とコメント(<!-- comment -->)を削除します。HTMLを圧縮することで「通信バイト数の削減」と「ドキュメントツリーのノード数削減」を目指します。また、構文エラー検出機能により出力HTML(XML)の正しさがある程度保証します。

本ウェブアプリは、スクリプト部分が200行程度の簡易なアプリです。機能の中核をwindow.DOMParserに依存しています。DOMParserは、ブラウザ側の標準機能です。ブラウザ側の標準機能によるパース処理であるため、バイト数削減を目的とした大多数のウェブアプリに比べて圧縮結果に対して信頼性があります。

動作概要(DOMParser)

  • HTML圧縮処理は、サーバーと通信を行いません
    • ローカルのJavaScriptのみで動作します
    • ページ取得時には、オンライン環境が必要です
      • Searvice Worker対応(オフライン動作)は、時期未定で現状未対応です
      • ページ全体のローカル保存でも動作します
  • パーサーとして、DOMParserを使用しています
    • そのため、ブラウザ毎に挙動が異なります
    • 主に結果やエラー出力が異なります
    • うまく動作しない場合、別ブラウザを使用することでうまくいく可能性があります
  • タグを最適化します
    • タグの属性間のホワイトスペースを削除します
  • 文中の<>&lt;&gt;に置換します
    • <![CDATA[]]>を使用することで回避できます
    • ただし、<style>文中は、逆に&lt;&gt;<>に置換します
      • 動作概要(独自機能)参照
  • XML(HTML)として正しくない場合、動作しません
    • 動作しない場合、エラーを出力します
      • これは、本アプリの独自エラーではなく、DOMParserのブラウザ標準のエラーです
      • HTML(XML)のチェックツールとしても利用できます
    • 例:タグが閉じられていない:<img src="..."><img src="..."/>
    • 例:名前空間を宣言してない:<svg><use xlink:href='#icon-search'/></svg><body xmlns:xlink="http://www.w3.org/1999/xlink"/>

動作概要(独自機能)

  • ルート外のホワイトスペースは、削除しません
    • <!DOCTYPE html>などの前後ホワイトスペースが対象です
    • ただし、ファイルの文頭・文末ホワイトスペースは削除します
  • ホワイトスペースを削除します
    • スペース・タブ・改行が対象です
    • 正確には、末端ノードの文頭・文末ホワイトスペースが対象です
    • ホワイトスペースを削除するため、レイアウトが崩れる可能性があります
      • スペースをタグ間に挿入することで回避可能ですが、対応しません
        • タグ間にホワイトスペースを挟まないことで、ノード数を削減できます
        • ノード数を削減することでHTMLパース処理の高速化が狙えます
        • そのため、スペースを挿入しません
      • &nbsp;などを利用して明示的にスペースを挿入することで回避できます
  • コメントを削除します
    • <!-- コメント -->が対象です
    • 正確には、コメントノードが対象です
  • xmlns:b='http://www.google.com/2005/gml/b'のコメントを削除します
    • <b:comment></b:comment>が対象です
    • render属性がある場合でも削除します
  • <style>文中を&lt;&gt;<>に置換します
    • これは、子セレクタ用の措置です
    • イレギュラーな処理なため、問題のある可能性があります
      • この処理により、問題が発生しましたら連絡いただければ幸いです
  • <script>の文中ホワイトスペースを削除しません
    • ただし、文頭・文末のホワイトスペースは削除します
      • これは、1行コメントや意味あるホワイトスペースを保護するための措置です
    • 圧縮なども行いません。圧縮が必要な場合は、事前に圧縮して下さい
  • <img>などの終了タグを記述してはいけないタグの開始タグを閉じる
    • <img><img/>として扱う
    • XML的には、閉じていないとエラーとなるため、エラーになるのを抑制する
  • 属性を強制的に""で括る
    • 論理属性のXHTML対応

既知の問題

  • 次のIE用ハックコードが削除される
    • <!--[if IE]><![endif]-->
    • IE9-までのハックコードのため、もはや不要?
  • xmlnsが大量に増殖する
    • <html>だけでなく<head>,<body>,<svg>xmlns指定することで回避可能です
  • &lt;&gt;で表現されたタグ文頭・文末のホワイトスペースを削除しない
    • 仕様です
    • 手動で&lt;&gt;前後のホワイトスペースを削除して下さい

テストコード

テストコード.html<!DOCTYPE html>
<html lang="ja"
      xmlns='http://www.w3.org/1999/xhtml'>
<!-- html comment -->
<head>
  <title> タイトル </title>
  <style>
body {
  background: red; /* style comment */
}
  </style>
  <style>
body > element1 {
  color: white;
}
  </style>
  <!-- head comment -->
</head>
<body xmlns:b='http://www.google.com/2005/gml/b'>
  <element0 attribute="value0">テキストA</element0>
  <element1 attribute1="value1"     attribute2="a"    >  テキストB  </element1>
  <element2 attribute1='value1'
            attribute2="2">&nbsp;テキストC&nbsp;</element2>
  <element2 attribute1='value1'
            attribute2="2">&amp;nbsp;テキストD&amp;nbsp;</element2>
  <b:comment>bcomment</b:comment>
  <b:comment render='true'>bcomment render true</b:comment>
  <b:if cond='true'>
    <img src=""/>
  </b:if>
  <img  src="a"  ><img  src="b" /><img  src="c" ></img   ><img  src="
  d
  "  >
  <ins></ins>
  <script>
//<![CDATA[
//<!-- script comment -->
var x = '<'+'/script>';  // script comment
console.log('1: '+x);
//]]>
  </script>
  <script>/*<![CDATA[*/var x = '<'+'/script>';console.log('2: '+x);/*]]>*/     </script>
  <script>   var x = '\x3C/script\x3E';console.log('3: '+x);</script>
  <script>   
  var x = String.fromCharCode(60)+'/script'+String.fromCharCode(62);console.log('4: '+x);</script>
  <!-- body comment -->
  <hr>
  <hr/>
  <hr />
  <hr></hr>
  &amp;lt;test-&amp;gt;
    test
  &amp;lt;/test-&amp;gt;
  <iframe test aaa=11 bbb=abc data-ccc-ddd='xxx' eee></iframe>
  <iframe test aaa=11 bbb=abc data-ccc-ddd='xxx' eee/>
&lt;!--</body>--&gt;&lt;/body&gt;
</html>

結果.html<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja"><head><title>タイトル</title><style>body {
  background: red; /* style comment */
}</style><style>body > element1 {
  color: white;
}</style></head><body xmlns:b="http://www.google.com/2005/gml/b"><element0 attribute="value0">テキストA</element0><element1 attribute1="value1" attribute2="a">テキストB</element1><element2 attribute1="value1" attribute2="2">&nbsp;テキストC&nbsp;</element2><element2 attribute1="value1" attribute2="2">&amp;nbsp;テキストD&amp;nbsp;</element2><b:if cond="true"><img src="" /></b:if><img src="a" /><img src="b" /><img src="c" /><img src="   d   " /><ins></ins><script>//<![CDATA[
//<!-- script comment -->
var x = '<'+'/script>';  // script comment
console.log('1: '+x);
//]]></script><script>/*<![CDATA[*/var x = '<'+'/script>';console.log('2: '+x);/*]]>*/</script><script>var x = '\x3C/script\x3E';console.log('3: '+x);</script><script>var x = String.fromCharCode(60)+'/script'+String.fromCharCode(62);console.log('4: '+x);</script><hr /><hr /><hr /><hr />&amp;lt;test-&amp;gt;
    test
  &amp;lt;/test-&amp;gt;<iframe test="" aaa="11" bbb="abc" data-ccc-ddd="xxx" eee=""></iframe><iframe test="" aaa="11" bbb="abc" data-ccc-ddd="xxx" eee=""></iframe>&lt;!--</body>--&gt;&lt;/body&gt;</html>

変更履歴

日付説明
2020/02/29初版
2020/03/01xmlns:b='http://www.google.com/2005/gml/b'b:commentを削除する
2020/03/04<html>の属性を最適化
2020/03/05<img><img/>として扱う
2020/03/06xmlns属性削除オプション追加
2020/03/06終了タグが存在するタグを閉じないように仕様変更(W3Cにエラー指摘される)
2020/03/07属性を強制的に""で括るように仕様変更
2020/03/09コード量削減