JavaScriptやCSSを動的にdeflate圧縮するのではなく、あらかじめ圧縮しておいたものを配信することでサーバーのCPUリソースを節約する

prototype.jsを10KBにする方法Safari と gzip 圧縮 JavaScriptなどですでに述べられてることですが、mod_deflateでリクエストがある度にアセット(CSSやJavaScript)にdeflate圧縮をかけるのは、deflate処理が軽いからと言っても、塵も積もれば馬鹿にならない(WWWサーバーやAPサーバーに本来使って欲しいCPUリソースを蝕む)訳で、deflateしたいアセットには予めgzip圧縮してそれを配信し、サーバーのCPUリソースに優しいようにしましょう、というお話。

今回は、予めgzip圧縮するという作業を自動化するために、Railsでよく使われるデプロイツールであるCapistranoを利用します。

基本的な方針としては、(1)ローカルの開発環境上ではCSSやJavaScriptは非圧縮(編集しやすいように)、(2)サーバー上にデプロイされたCSSやJavaScriptにはgzip圧縮をかける(帯域削減のため)、(3)非圧縮CSSや非圧縮JavaScriptをサーバー上にも置いておく(gzip対応してないブラウザ向け)、という3点です。

例えば(1)の例は/asets/js/prototype.js(非圧縮)、(2)の例は/asets/js/prototype.js(gzip圧縮済み)、(3)の例は/asets/js/prototype.js.ungz(非圧縮)と言った感じ。ここで(1)と(2)のファイル名を揃えているのは楽をするため。つまりローカル開発環境とサーバー上でファイル名が異なってると、いちいちscriptタグを書き換えるのがめんどくさいのです。

さて、上で述べたようにサーバー上にデプロイされたアセットをgzip圧縮するCapistranoのタスクは以下の通り。これは#{current_path}に最新リリースがデプロイされた後に実行されるコードです。:updateタスクの中で実行されるようにしておくと良いでしょう。

desc <<-DESC
Updates the code and fixes the symlink under a transaction
DESC
task :update do
transaction do
update_code
compress_assets
end
end

desc "Compress assets"
task :compress_assets do
run "find #{current_path}/public/assets/ -name '*.js' -exec gzip {} \;"
run "find #{current_path}/public/assets/ -name '*.css' -exec gzip {} \;"
run "cd #{current_path}/public/assets/js;for i in *;do mv $i ${i%.js.gz}.js;done"
run "cd #{current_path}/public/assets/css;for i in *;do mv $i ${i%.css.gz}.css;done"
run "cd #{current_path}/public/assets/js;for i in *;do gzip -dc $i >$i.ungz;done"
run "cd #{current_path}/public/assets/css;for i in *;do gzip -dc $i >$i.ungz;done"
end

いちおう説明しておくと、findの2行で/public/assets/配下のJavaScriptファイルやCSSファイルにgzip圧縮をしています。次の2行は、gzip圧縮をかけると例えば名前がprototype.js.gzと変わってしまうので、prototype.jsに戻しています。最後の2行は圧縮後のprototype.jsを解凍し、prototype.js.ungzというファイルを作る部分です。

そうすると、/assets/jsの中身は以下のようになります。

-/assets/js/
|
|-prototype.js(gzip圧縮済み)
|-prototype.js.ungz(非圧縮)

さて、これをApacheのディレクティブを活用して配信するわけです。方針はgzip受け取れるブラウザはprototype.jsをそのまま受け取る、gzipを受け取れないブラウザやgzip扱いにバグありブラウザはprototype.js.ungzを受け取る、というもの。

<Directory /home/httpd/vhosts/example.jp/current/public/assets>
<Files ~ ".(css|js)$">
SetEnvIf Accept-Encoding "gzip" can_compress_assets
# Netscape 4.x has some problems...
BrowserMatch ^Mozilla/4 !can_compress_assets
# Netscape 4.06-4.08 have some more problems
BrowserMatch ^Mozilla/4.0[678] !can_compress_assets
# MSIE masquerades as Netscape, but it is fine
BrowserMatch bMSIE can_compress_assets

RewriteCond {%ENV:can_compress_assets} !1
RewriteRule ^(.+)(.css|.js)$ $1$2.ungz [L,QSA]
Header set Content-Encoding: gzip env=can_compress_assets
</Files>
</Directory>

説明。まずSetEnvIf Accept-Encoding "gzip"でHTTPリクエストヘッダの中にAccept-Encoding: gzipがあれば、圧縮できる(can_compress_assets)という環境変数を立てます。その後、BrowserMatchでごにょごにょやってるのは、一部ブラウザでgzipの扱いに問題があるため、問題があるブラウザはgzipしてないコンテンツをサーブする(!can_compress_assets)ようにします。

次のRewriteCond、RewriteRuleは環境変数can_compress_assetsがセットされていなかった場合(非圧縮コンテンツを返すべき場合)、例えばprototype.jsがリクエストされたら、非圧縮のprototype.js.ungzを代わりに返すというものです。

最後のHeader set Content-Encoding: gzip env=can_compress_assetsは、圧縮されたコンテンツを返している場合、HTTPレスポンスヘッダにContent-Encoding: gzipを追加するというものです。こうしないとブラウザは返されたコンテンツがgzipされていることを知らないため、解凍してくれません。

Content-Encoding: gzipを付けるならAddType gzip .jsでいいじゃないか、という意見が聞こえてきますが、その場合、上の非圧縮コンテンツを返している際もHTTPレスポンスヘッダにContent-Encoding: gzipが付いてしまい、ブラウザが圧縮されてないものを解凍しようとして混乱してしまいます。

CSSファイルやJavaScriptファイルはサイト内で数十になることも多く、それが数千万回リクエストされるとなると、それらのdeflate処理にかかっていたコストは馬鹿に出来なくなります。こうして予めgzip圧縮しておくことで、それらのコストを削減し、少しでも地球に優しくしようという試みでした。

Leave a Reply

Your email address will not be published. Required fields are marked *