MAMP環境でgzip・Expiresを有効にしてみた


パフォーマンスチューニング系の書籍でほぼ必ず出てくるけど..
**「gzipって、つまり何者？」**
という、フロントエンド向けの記事です。

ファイル圧縮形式gzipとキャッシュ設定Expiresについて、MAMPのApacheサーバで簡易的にテストしてみました。

誤っている情報がありましたら、ご指摘いただけると助かります<(__)>

## 有効にしたくなった背景
[Gulp](http://gulpjs.com/)などのタスクランナーを利用して、html/css/js/画像といったリソースの最適化を行ったりする開発環境を作っていると、様々なパッケージを利用してコテコテのgulp環境を作りたくなってしまいます。

ふと、 **「クライアントで色々と頑張ってるけど、そもそもサーバサイドの[最強と名高いキャッシュ](https://developers.google.com/speed/docs/insights/LeverageBrowserCaching)や[Googleも比較的ゴリ押しgzip](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/optimize-encoding-and-transfer?hl=ja#gzip-)ってどうなんだろ？」**と思いました。

聞いた事位はあるけど、サーバーサイドの対策が必要なため調べるのが億劫です。
しかしながら、キャッシュやgzipの魅力を考えると取りあえず仕組みだけでも理解しておいた方が良さそうなので、調べてみる事にしました。

## gzipの仕組み
とりあえず、キャッシュの仕組みについては理解しているため、gzipの仕組みを調べました。
下記記事の説明が分かりやすかったです↓
[gzip圧縮のしくみ](https://github.com/t32k/speed/blob/master/articles/gzip.md)

gzipは、minifyやUglifyなどのフロントエンドにおなじみの圧縮形式とは異なり、画像の圧縮に用いられるような高圧縮な形式で行われるそうです。文章の原型を留めません。
（ざっくり理解ですみません）

かつ、JPEGのように非可逆的（画像が劣化したまま元に戻せない）な圧縮形式ではなく、元に戻せる形式をとるため、「リソースを通信時は圧縮、使うときに展開」といった利用方法が可能になります。

上記のような特性を持つため、利用する方法として一般的なのは下記いずれかのようです。
なお、いずれも **「利用（gzip展開）可能なブラウザのみで読み込まれるようにする設定」**が必要となります。（後述）

- 事前にgzipファイルを準備しておく（タスクランナーやシェルで作成）
- サーバサイドでリクエストを受け取った際、リアルタイムにgzipファイルを生成してクライアントに配布する

後者の場合、フロントエンドの作業が減りますね。
ちなみに、現行のブラウザのほとんどはgzipに対応しているようです。
（ガラケー時代レベルだと非対応のものもあるらしい）

## gzipファイルを作成
この記事では、前節で紹介したgzipの利用方法の内、「事前にgzipファイルを準備しておく」という方法を試してみたいと思います。

なお、この記事では下記のようなファイル・ディレクトリ構成を前提として進めていきます。

---
- app/
	- .htaccess # gzipの読み込み設定
 	- gulpfile.babel.js # gzip作成タスク
	- dev/
		- js/
			- app.js # gzip変換前のファイル
	- prod
		- index.html # gzipを読み込むhtml
		- js/
			- app.js.gz # gzip変換後のファイル
			- app.js # 比較用にuglifyしたファイル

---

> gzip変換前のapp.jsには、JQueryなど大きめのライブラリなどを入れておくと、比較する時のファイルサイズ差が顕著になると思います。
また、uglifyしたファイルとも比較したいと思います。(uglifyするタスクの書き方は紹介しません)

下記のようなgulpのタスクを[gulp-gzip](https://www.npmjs.com/package/gulp-gzip)というパッケージを利用して、記述します。
(gulpパッケージは、本当になんでもありますね)

```app/gulpfile.babel.js
import gulp from 'gulp';
import gzip from 'gulp-gzip';

gulp.task('jsGzip', () => {
  gulp.src('dev/js/app.js')
    .pipe(gzip())
    .pipe(gulp.dest('prod/js'))
});
```
`app/dev/js/app.js`を`app/prod/js/app.js.gz`に変換するタスクです。
これとは別途、gzip非対応ブラウザ用のファイルも`app/prod/js/app.js`として用意します。
なお、上記はjavascript用のgzipを作成する例ですが、html/css/画像でもgzip形式に圧縮できます。

`app/prod/index.html`からは、下記のようにファイルを読み込んで下さい。

```app/prod/index.html
<script src="./js/app.js"></script>
```

普通の読み込み方ですが、後ほどの`app/.htaccess`の設定によって、gzip対応ブラウザは自動的に`app/prod/js/app.js.gz`を読み込むようになります。

下記は、JQuery2系で作成した場合のファイル群のサイズです。

|ファイル名|dev/js/app.js|prod/js/app.js|prod/js/app.js.gz|
|---|---|---|---|
|圧縮形式|非圧縮|uglify|gzip|
|サイズ(KB)|258|88|38|

gzipされたファイルは、uglifyされたファイルから約57%サイズを削れています。
gzipでは同様のパターンが多い文字列である程、圧縮率が高くなるらしいです。


## みんな大好きMAMPの設定
この記事では`app/prod/index.html`を利用するため、まずhtmlを認識するように設定します。
`http.conf`の138行目に`.html`を追記します。

```/Applications/MAMP/conf/apache/http.conf
138 AddType application/x-httpd-php .php .phtml
↓
138 AddType application/x-httpd-php .php .phtml .html
```

次に、下記のように記述した`.htaccess`をアプリケーションルートに配置します。
内容について詳しくは、ページ下部の参考記事を参照して下さい。

```app/.htaccess
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteCond %{HTTP:Accept-Encoding} gzip
    RewriteCond %{REQUEST_FILENAME} \.(css|js|html)$
    RewriteCond %{REQUEST_FILENAME} !\.gz$
    RewriteCond %{REQUEST_FILENAME}\.gz -s
    RewriteRule .+ %{REQUEST_URI}.gz [L]

    <FilesMatch "\.html\.gz$">
        ForceType   text/html
        AddEncoding x-gzip .gz
    </FilesMatch>
    <FilesMatch "\.css\.gz$">
        ForceType   text/css
        AddEncoding x-gzip .gz
    </FilesMatch>
    <FilesMatch "\.js\.gz$">
        ForceType   application/javascript
        AddEncoding x-gzip .gz
    </FilesMatch>

    Header append Vary Accept-Encoding env=!dont-vary
</IfModule>
# 注意: ファイル末尾に一行空けて下さい (このコメントは削除)
```

あとは、MAMPの[設定→Webサーバ]から、WebサーバにApacheを選択して、ドキュメントルートを`app`に設定すれば完了です。

## gzipファイルを読み込んで（展開して）いるか確認
確認にはGoogleChromeのデベロッパーツールを利用します。
MAMPサーバを起動したら、`localhost:8888/prod`というURLを叩きます。(ポート8888はMAMP
初期設定)

この時点で`app/prod/js/app.js.gzip`が読み込まれている状態になります。
確認のため、`cmd + opt + i`でデベロッパーツールを開いて、[Network -> JS]の順にタブを選択します。
すると、`app.js`のSizeが表示されています。
(ファイルが何も表示されていない場合はタブが選択された状態でリロードして下さい)

![スクリーンショット 2016-06-15 0.27.17.png](https://qiita-image-store.s3.amazonaws.com/0/36492/baded3c2-87a2-07d7-4f01-24e488a96152.png "スクリーンショット 2016-06-15 0.27.17.png")


若干ファイルサイズに誤差がありますが、gzip後のファイルが読み込まれているのが確認出来ます。
（なお、サイトの表示・動きにも問題ありません）

では、次に`.htaccess`内の記述をコメントアウトした場合です。

![スクリーンショット 2016-06-15 0.27.40.png](https://qiita-image-store.s3.amazonaws.com/0/36492/b363605e-9ab3-9342-0675-e65f0c1baa83.png "スクリーンショット 2016-06-15 0.27.40.png")

ugilifyされたファイルが表示されています。
これで、gzipの設定と確認は終わりです。
今回は予めgzipファイルを用意しましたが、規模によってはサーバサイドで自動生成する仕組みを取り入れた方が、賢そうです。

ちなみに、実は`app.css`と`index.html`もgzip化しているので、スクリーンショットを比較してみて下さい。

## Expiresを設定
今度は、リソースのキャッシュの有効期限を設定します。
`.htaccess`に、Expireの設定を追記します。

```app/.htaccess
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteCond %{HTTP:Accept-Encoding} gzip
    RewriteCond %{REQUEST_FILENAME} \.(css|js|html)$
    RewriteCond %{REQUEST_FILENAME} !\.gz$
    RewriteCond %{REQUEST_FILENAME}\.gz -s
    RewriteRule .+ %{REQUEST_URI}.gz [L]

    <FilesMatch "\.html\.gz$">
        ForceType   text/html
        AddEncoding x-gzip .gz
    </FilesMatch>
    <FilesMatch "\.css\.gz$">
        ForceType   text/css
        AddEncoding x-gzip .gz
    </FilesMatch>
    <FilesMatch "\.js\.gz$">
        ForceType   application/javascript
        AddEncoding x-gzip .gz
    </FilesMatch>

    Header append Vary Accept-Encoding env=!dont-vary
    
    ExpiresActive On
    ExpiresByType image/png  "access plus 1 months"
    ExpiresByType image/jpeg "access plus 1 months"
    ExpiresByType image/gif  "access plus 1 months"
    ExpiresByType text/html  "access plus 1 months"
    ExpiresByType text/css   "access plus 1 months"
    ExpiresByType text/javascript "access plus 1 months"
    ExpiresByType application/javascript "access plus 1 months"

</IfModule>
# 注意: ファイル末尾に一行空けて下さい (このコメントは削除)
```
上記では、png,jpeg,gif,html,css,jsに設定しています。
（記述そのままですが..）

そして、なんとこれで設定完了です。

## Expiresキャッシュが効いているか確認
デベロッパーツールを開き、再び[Network -> JS]タブから、app.jsの状態を確認します。
今度は、app.jsをクリックして下さい。

すると、app.jsの[Header -> Response Header]に`Cache-control: max-age=2592000`という記載があるのが確認出来ます。

![スクリーンショット 2016-06-15 0.38.18.png](https://qiita-image-store.s3.amazonaws.com/0/36492/709e551b-5e20-e61c-de7a-a1f5af5f8c92.png "スクリーンショット 2016-06-15 0.38.18.png")


この状態が、キャッシュが効いている状態になります。

では、例によって`.htaccess`からExpiresの設定を削除して、ブラウザをリロードします。
> この際、キャッシュを一旦飛ばすため`Disable cache`にチェックを入れてからリロードして下さい。（下記画像の青枠で囲った部分）

すると[Header -> Response Header]に、先ほどの`Cache-control: max-age=2592000`の記載がなくなっているのが確認出来ます。

![スクリーンショット 2016-06-15 0.39.39.png](https://qiita-image-store.s3.amazonaws.com/0/36492/2d395ee3-c72e-ccc3-5249-ef0d1e1cf904.png "スクリーンショット 2016-06-15 0.39.39.png")

Expires設定がされていないため、キャッシュされていません。

Expires設定の注意点として、リソースのクエリとして[ダイジェストを付与するような仕組み](https://www.anothersky.pw/2014/07/site_performance_improvement.html)を用意していないと **「キャッシュが効いてて更新されない地獄」**に陥る可能性があります。

これで、gzip・Expiresについての検証は終わりです。


## 終わりに
今回の記事では`.htaccess`に設定を記述することで、gzipとExpiresを有効にしましたが、これが現場レベルで使える正しい方法なのかは分かりません<(__)>；
ちなみに、Railsでは[Production環境のアセットパイプライン](http://railsguides.jp/asset_pipeline.html#gzip%E5%9C%A7%E7%B8%AE)でもgzipファイル生成機能を提供しているようです。

しかし、いずれにしてもサーバサイドの領域になるため、活用する場は個人的な案件、もしくは現場にて「サーバーサイドにお願いする」といったパターンになりそうです。

コンマ1秒のスピードが収益に関係してくるようなサービス、もしくは、もはやタスクランナー改造する位しかやる事無い状態なケースでは、フロントエンドからも積極的に働きかける価値があると思いました。


## 参考記事
[gzip圧縮のしくみ](https://github.com/t32k/speed/blob/master/articles/gzip.md)
[gzip圧縮でCSSやJSなどの転送量を減らす方法](https://syncer.jp/how-to-make-gzip)
[gzip圧縮転送についてトコトン調べてみた](http://satoyan419.com/gzip-compression/)
[改行削除するくらいなら gzip したらいいじゃない](https://hyper-text.org/archives/2012/11/webcontents_gzip.shtml)
（´-`）.｡oO（minify、uglifyでドヤってましたすいません..）