2017年4月29日土曜日

brotliやgzipとHTML最小化の効果比較

html-minifierはコマンドラインでもHTML最小化ができる非常に便利なツールです。 しかしHTTPアクセス時はbrotliやgizpなどのデータ圧縮のほうが遥かに効果があるはずなので、どこまで効果があるのかを改めて確認してみようと思いました。

以下は今回の検証に利用したhtml-minifierのオプションで、HTMLにエラーが出ない状態で高圧縮をかけられます。どれくらい効果があるでしょうか。
html-minifier #{from} \
  --collapse-boolean-attributes \
  --collapse-whitespace \
  --remove-attribute-quotes \
  --remove-comments \
  --remove-empty-attributes \
  --remove-redundant-attributes \
  --remove-script-type-attributes \
  --remove-style-link-type-attributes \
  --html5 \
  --use-short-doctype \
  --minify-js \
  --minify-css > #{to}

やはりデータ圧縮のほうが遥かに効果があります。brotliとgzipではかなり差がありますが、データ圧縮をかければHTMLの記述効率は通信量に影響してくるデータサイズにさほど影響しません。
original:                 17172
html-minifier:            15652 ( -8.8%)
original + gzip 9:         4948 (-71.2%)
html-minifier + gzip 9:    4729 (-72.5%)
original + brotli 10:      3977 (-76.8%)
html-minifier + brotli 10: 3881 (-77.3%)

html-minifierは結構実行時間が掛かるので、大量ページに適用するのは辛いです。少しでも通信量を削減したいファイルや、静的ファイルにのみ適用するのが良いでしょう。

2017年4月28日金曜日

brotliの圧縮率と圧縮時間の検証 (gzip, zopfliと比較)

前投稿でzopfliの圧縮率と圧縮時間を再検討した事もあり、良い機会なので運用中サイト群の完全HTTPS化を頑張ってみようかと。 効果を確認するために事前検討で以下のスクリプトを走らせてみました。検証ではbrotli-0.60, zopfli-1.01を利用しています。
file = ARGV[0]

def calc(label, cmd, file)
  t = Time.now
  `#{cmd}`
  t = Time.now - t
  size = File.size(file)
  puts "#{label}: #{sprintf("%.6f", t)} sec, #{size} bytes"
end

puts "original: #{File.size(file)} bytes"
(1..10).each {|level|
  calc("brotli #{level}", "brotli --force --quality #{level} --input #{file} --output #{file}.br", "#{file}.br")
}
((1..9).to_a + [10,15,20,25,30]).each {|level|
  calc("zopfli #{level}", "zopfli --i#{level} #{file}", "#{file}.gz")
}
(1..9).each {|level|
  calc("gzip   #{level}", "gzip -#{level} -c -f #{file} > #{file}.gz", "#{file}.gz")
}

ギリギリまで冗長性を削除しているHTMLをテストに利用してみたところ、gzip-18%、zopfli-16%となり素晴らしい結果でした。
$ ruby test.rb test.html
original: 15652 bytes
brotli 1: 0.002033 sec, 5047 bytes
brotli 2: 0.001699 sec, 5009 bytes
brotli 3: 0.001784 sec, 4980 bytes
brotli 4: 0.002044 sec, 4747 bytes
brotli 5: 0.002624 sec, 4172 bytes
brotli 6: 0.003607 sec, 4162 bytes
brotli 7: 0.004323 sec, 4107 bytes
brotli 8: 0.004056 sec, 4100 bytes
brotli 9: 0.005396 sec, 4087 bytes
brotli 10: 0.043826 sec, 3881 bytes
zopfli 1: 0.252186 sec, 4612 bytes
zopfli 2: 0.273344 sec, 4606 bytes
zopfli 3: 0.306744 sec, 4603 bytes
zopfli 4: 0.265884 sec, 4603 bytes
zopfli 5: 0.327768 sec, 4603 bytes
zopfli 6: 0.302046 sec, 4603 bytes
zopfli 7: 0.322396 sec, 4603 bytes
zopfli 8: 0.310021 sec, 4603 bytes
zopfli 9: 0.340069 sec, 4605 bytes
zopfli 10: 0.338060 sec, 4605 bytes
zopfli 15: 0.383038 sec, 4603 bytes
zopfli 20: 0.392550 sec, 4603 bytes
zopfli 25: 0.360510 sec, 4603 bytes
zopfli 30: 0.277653 sec, 4603 bytes
gzip   1: 0.002162 sec, 5122 bytes
gzip   2: 0.001989 sec, 5033 bytes
gzip   3: 0.001755 sec, 4981 bytes
gzip   4: 0.001775 sec, 4791 bytes
gzip   5: 0.001814 sec, 4739 bytes
gzip   6: 0.002046 sec, 4734 bytes
gzip   7: 0.002149 sec, 4732 bytes
gzip   8: 0.003049 sec, 4729 bytes
gzip   9: 0.002149 sec, 4729 bytes

前回利用したJSONファイルも確認してみた。これもギリギリまで冗長性は排除していますが、gzip-18%、zopfli-13%と素晴らしい結果でした。
$ ruby test.rb test.json
original: 438146 bytes
brotli 1: 0.008709 sec, 143812 bytes
brotli 2: 0.010706 sec, 142193 bytes
brotli 3: 0.012212 sec, 139530 bytes
brotli 4: 0.014156 sec, 138066 bytes
brotli 5: 0.026652 sec, 125302 bytes
brotli 6: 0.034410 sec, 123004 bytes
brotli 7: 0.043270 sec, 120508 bytes
brotli 8: 0.052716 sec, 119024 bytes
brotli 9: 0.070479 sec, 118026 bytes
brotli 10: 1.337605 sec, 104952 bytes
zopfli 1: 1.116614 sec, 120899 bytes
zopfli 2: 1.178194 sec, 120623 bytes
zopfli 3: 1.271736 sec, 120548 bytes
zopfli 4: 1.345818 sec, 120485 bytes
zopfli 5: 1.492534 sec, 120455 bytes
zopfli 6: 1.529910 sec, 120438 bytes
zopfli 7: 1.594525 sec, 120432 bytes
zopfli 8: 1.665754 sec, 120408 bytes
zopfli 9: 1.750016 sec, 120398 bytes
zopfli 10: 1.884538 sec, 120392 bytes
zopfli 15: 2.519045 sec, 120390 bytes
zopfli 20: 3.101787 sec, 120390 bytes
zopfli 25: 2.980007 sec, 120390 bytes
zopfli 30: 3.701557 sec, 120387 bytes
gzip   1: 0.013838 sec, 170265 bytes
gzip   2: 0.039754 sec, 161152 bytes
gzip   3: 0.034746 sec, 151130 bytes
gzip   4: 0.015178 sec, 144631 bytes
gzip   5: 0.019380 sec, 134481 bytes
gzip   6: 0.030051 sec, 130714 bytes
gzip   7: 0.040470 sec, 129113 bytes
gzip   8: 0.066297 sec, 127468 bytes
gzip   9: 0.066368 sec, 127468 bytes

brotliのqualityは、応答性を求めるなら4、ほどほどの圧縮時間で良ければ9、圧縮時間を問わなければ10を利用するのが良さそうです。

2017年4月25日火曜日

gzipとzopfliの圧縮率と圧縮時間の比較

静的ファイルは主にzopfliを使って圧縮していたものの、バッチがあまりにも遅かったので、いまさらながら見直しをしてみました。 手元のJSONファイルの圧縮率と圧縮時間を調査してみます。
def check_zopfli(file, level)
  t = Time.now
  `zopfli -c --i#{level} #{file} > #{file}.gz`
  t = Time.now - t
  puts "zopfli #{level}: #{t} sec, #{File.size("#{file}.gz")} bytes"
end


file = ARGV[0]

puts "original: #{File.size(file)} bytes"
t = Time.now
`gzip -9 -c -f #{file} > #{file}.gz`
t = Time.now - t
puts "gzip 9: #{t} sec, #{File.size("#{file}.gz")} bytes"

[1,5,10,15,20,25,30].each {|level|
  check_zopfli(file, level)
}

実行結果はこちら。
$ ruby check.rb test.json
original: 438146 bytes
gzip 9: 0.069797465 sec, 127468 bytes
zopfli 1: 1.139244898 sec, 120899 bytes
zopfli 5: 1.497824568 sec, 120455 bytes
zopfli 10: 1.976062207 sec, 120392 bytes
zopfli 15: 2.352069506 sec, 120390 bytes
zopfli 20: 2.688475763 sec, 120390 bytes
zopfli 25: 3.136264936 sec, 120390 bytes
zopfli 30: 3.394463868 sec, 120387 bytes

400KB程度のファイルであればlevel=1-5でも十分のようです。大量ファイルの場合は実行時間が辛いのでgzip -9で妥協して、クリティカルな部分だけzopfli -30などを使う事にしようと思います。

brotliを使えば圧縮率も圧縮時間も良好ながら、HTTPSでしか利用できないので現時点では保留しています。HTTPSでしか利用できない理由はエンコード判定の問題や、BREACHやCRIM攻撃に課題があるためだそうです。圧縮も意外と難解ですね。

2017年4月21日金曜日

direnvがcronでなぜか動かない時の対処法

direnvはPythonなどの作業には非常に便利なツールです。しかしいざ本番環境に移行しcronで使おうとすると動かなくて困る事があります。 例えば以下のような症状が出た時は、まず実行方法を確認すると良いと思います。
  1. cronで実行すると作業ディレクトリで指定したpythonと違うバージョンで動いてしまう
  2. cronで実行するとpythonが見つからないと怒られる

これはcronで実行した時にdirenvが実行ディレクトリを判断できないために起こる問題です。 普段の作業ではcdした瞬間にdirenvがよしなに取り計らってくれるので、以下の実行方法でdirenvで設定したpythonを実行してくれます。
cd working_dir
python hoge.py

しかしcronから実行する場合にはcdして平凡に実行するだけではdirenvはきちんと判別できないようです。どこで実行しているのかを明示するために、以下のように実行する必要があります。
direnv exec working_dir python hoge.py

direnv --helpにはこの実行方法がきちんと書かれているのですが、普段が便利過ぎて最初は気付けませんでした。

2017年4月20日木曜日

rbenvとbundlerがcronで動かない時の対処法

rbenvとbundlerはRubyでの作業には必須のツールです。しかしいざ本番環境に移行しcronで使おうとすると動かなくて困る事があります。 rbenvをcronで使う方法は色々ありますが、bash -lcで以下のように実行するのが自然です。
* * * * * user /bin/bash -lc 'cd /path/to/program-dir && ruby program.rb'

設定が適切ならこれで問題なく動くのですが、以下のように怒られる事があります。
  1. bundlerでgemをインストールしたはずなのにライブラリが見つからない
  2. bundlerでgemをインストールしたはずなのにバージョンが違うと言われる

これはbundlerでgemの保存場所を指定していない場合に起きやすい問題です。 bundlerは保存場所を何も指定しないとgemをシステムに保存しますが、cronから呼び出す際には保存場所が指定されていないと読み出しに失敗したり、バージョン不整合が起きます。 この問題はbundlerの実行時に--pathオプションを付ける事で解決します。
bundle install --path vendor/bundle

bundlerの保存場所を明示するとその設定は.bundle/configに記憶されるため、cronで実行した際にも読み出し位置が明確になります。エラーも消えるはずです。bundle installする際は、忘れずに--pathオプションを付けましょう。

技術系ブログを書くためのブログ比較と初期設定

2017年現在、技術系ブログを書く場合にはBlogger, Tumblr, はてなブログのどれかを使っている人が多いと思います。 最近はQiitaに書いてしまう人も多いですが、著者を明確にするにはやはりブログのほうが良いです。

ブログサービスは上述以外にも様々なサービスはありますが、 (1)一定期間投稿をしていない時に表示を妨害されない、 (2)軽量でUIの使い勝手が良く拡張性も良い、 といったシンプルな要件を満たしているのがこの4つと思っています。 特にはてなブログは技術系ブログを書くための記法(はてな記法)まであるので、お手軽さでは一番良いでしょう。


私のおすすめはBloggerです。その理由は、 (1)広告を掲載できる(はてなブログ×)、 (2)検索しやすい(Tumblr×)、 の2つです。これらを気にしなければどれを使っても良いとは思うのですが…。しかしBloggerは機能面でもなかなか素晴らしい機能があると思います。

特に魅力的な機能は以下です。 (1)レスポンスが非常に良いです。巷のほとんどのブログは運用コストを下げるためにコンテンツはキャッシュに乗らないような運用をしています。しかしBloggerはほぼすべてのコンテンツにキャッシュが効いています。これはレスポンスが良くて当然で、さすがGoogleというところでしょうか。 (2)テーマは少ないものの編集しやすさが抜群。管理ページのレスポンスも素晴らしいですし、表示領域をプログレスバーで変更できる機能はシンプルながら感動しました。


ただしBloggerには技術系ブログで必須なコードハイライト機能が用意されていないため、自前で用意する必要があります。 プラグインとしてはSyntaxHighlighter, google-code-prettify, highlight.jsの3つが有名ですが、SyntaxHighlighterはやや重めなので今回は×。google-code-prettifyはデフォルトでは非対応の言語への対処が面倒なので×。今回は対応言語が多くお手軽なhighlight.jsを使う事にしました。

設定も簡単で、Bloggerのテーマのheadに以下を追加するだけです。バージョン(9.11.0)やテーマ(default.min.css)は適宜設定が必要です。
<link href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.11.0/styles/default.min.css' rel='stylesheet'/>
<script src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.11.0/highlight.min.js'/>
<script>hljs.initHighlightingOnLoad();</script>

記事を書く際には以下のように書けばハイライトされます。ただしここで注意点があり、BloggerではHTMLコードの挿入にはHTMLモードで記事を書く必要があります。 つまりこのコードはHTMLエスケープされた状態で入力する必要があります。そのためHTMLコードを多様する技術ブログでは、Bloggerは少し使いにくいかも知れません。
<pre><code class="python">...</code></pre>

昔は技術系ブログと言えばはてなダイアリー一択に近かったのですが、最近はレスポンスが悪くなっていて時代の変化を感じます。