さぁ、高速化だ!

2017-10-12



Webサービスの高速化は、重要だ。曰く、0.1秒の低下でECサイトの売り上げは1%低下し、0.5秒の低下でWebサイトのアクセスが20%も落ちる。わずか0.1秒だ。

しかしSIerに開発を委託しているようなサービスでは何故か蔑ろにされがちである。自分の周りで動いているSI案件も少しの間任せてもらえればリピートで1秒縮めることは別に難しくない。

先述の理屈で言えば売り上げが10%上がるワケで、下手なSEO対策なんて目じゃない。年に100億からの売り上げがあるECサイトなら、10%も上がった日にゃ利益が数千万どころか数億ほど浮く。

まずは計測と診断

「測定できるものは測定し、できないものはできるようにせよ」
- ガリレオ・ガリレイ

高速化は計測に始まり計測に終わる。効果測定をしない限り何も改善されず、ただ効果測定をするだけでも早くなる。人間は可視化されるスコアを改善せずにはいられない。

定石を知らないのなら、診断サイトを利用して効果的な施策を教えてもらおう。ここでは2つの診断サイトを利用する。

PageSpeed Insights

https://developers.google.com/speed/pagespeed/insights/

PageSpeed InsightsはGoogle謹製の診断ツールだ。十分に最適化されていない部分を見つけ出してくれる。診断項目は以下の通り。

  1. スクロールせずに見えるコンテンツのレンダリングをブロックしている JavaScript/CSS を排除する
  2. ブラウザのキャッシュを活用する
  3. JavaScript を縮小する
  4. CSS を縮小する
  5. HTML を縮小する
  6. サーバーの応答時間を短縮する
  7. リンク先ページのリダイレクトを使用しない
  8. 圧縮を有効にする
  9. 画像を最適化する
  10. 表示可能コンテンツの優先順位を決定する

さて、自分のWebサービス(つまりココ、owl-works.orgだ)が引っかかった項目は

  1. スクロールせずに見えるコンテンツのレンダリングをブロックしている JavaScript/CSS を排除する
  2. ブラウザのキャッシュを活用する
  3. JavaScript を縮小する
  4. CSS を縮小する
  5. 圧縮を有効にする

の5つで、スコアは僅か「37点(Poor)」だ

WebPagetest

http://www.webpagetest.org/

WebPagetestはWebページにアクセスし表示までにかかった時間を測定してくれる。便利なのが初めてページにアクセスしたとき(FirstView)ともう一度アクセスしたとき(RepeatView)をそれぞれ別に計測してくれる点だ。

FirstViewはCSS/JSなど使い回されるファイルをダウンロードするために遅くなるが、適切にキャッシュするよう設定されていれば、そのサイトで他のページに移動する時には純粋なコンテンツ(HTMLや画像)のダウンロードだけで済む。

このWebサービスの計測結果はFirstViewで3.386s、RepeatViewで0.558sとなった。ContentBreakdownのタブを見るとFirstViewにかかっている時間のほとんどはWebフォントの読み込みに費やされていることがわかる。……つまり、Webフォントを削る以外にはほぼ改善する余地がないということだが…。

owl-works.orgを支える技術

改良する手段に関係してくるので、このサイトの構築について問題のない範囲で紹介しとこう。

少し浮いているのはSQLiteだろうか。データ数が小規模で複数クライアントに対応する必要がなく、高水準の可用性も期待されない。こういった条件ではSQLiteは他のDBに比べて高速で、バックアップを取るのが容易といった長所を生かすことができる。mysqldのリソース消費もマイクロサーバでは馬鹿にならない。モバイルアプリで使うだけのオモチャではない。

この程度のアプリならいっそのこと、全部のデータをRedisなどオンメモリで持っていても良いんじゃなかろうか。

css/jsの縮小と統合

まずはcss/jsの縮小からだ。縮小といってもzip圧縮のような話ではない。css/jsに含まれるスペースやコメント文、ローカル変数名などを削り込むことでファイルのバイト数を挙動は変えずに縮小することが出来る。よくjs/cssのライブラリでmin.cssやmin.jsのようなメチャ長い一行のファイルが配布されているだろう。あれが縮小コードだ。ちなみにこういった縮小をミニファイ(minify)と呼ぶ。

また、cssやjsは複数のファイルにしておく必要がなければ、可能な限り統合しておくことで効率化できる。つまり二つのa.cssとb.cssを読み込むよりも、それらを結合したab.cssを読む方が高速だ。

理由は簡単で、HTTPのリクエスト/レスポンスが一度で済むからだ。時間がかかるのはcssファイルのダウンロードだけではなく、ダウンロードするHTTPリクエスト自体も同じである。

無論、というか言うまでもなく我らがWebアプリフレームワークのRailsはこれを良い感じにやってくれる。そう、Assets::precompileだ。Asset::precompileが何をやっているかといえば、まさにこの縮小と統合なのだ。

Rails使いのみんなuglifierにはインストールバトルで泣かされたこともあるだろう。そもそもコイツは何のためのgemなんだ? 答えは、JSのミニファイだ。

標準でサポートされるとはいえ、上手く動いていないこともあるのでキチンと確認しよう。実際、自分の環境では最初jsもcssもミニファイされていなかった。

jsのほうはSprocketsか何かの影響だったらしく一度assetsを消してから再生成することで正常に動いた。

bundle exec rake tmp:cache:clear
rm -rf public/assets/*

cssの方は標準の設定ではミニファイされない。configを書き換えて有効にしよう。

config/enviroments/production.rb

  # Compress JavaScripts and CSS.
  config.assets.css_compressor = :yui
  config.assets.js_compressor = :uglifier

ここではcssのミニファイをYUI Compressorにやらせる。
Gemfileにgem 'yui-compressor'を追加し、config.assets.css_compressorで:yuiを指定すれば良い。本来、yuiはjsのミニファイにも利用できるがjQueryとの相性が悪いのか上手く機能しなかったためjsは標準のuglifierをそのまま使う。

nginxのチューニング

さて、チューニングといっても当たり前のことを当たり前にやる。特別なことは何もしない。

UNIXドメインソケットの利用

まず、unicornなどWebアプリ側との通信にTCPを使っている場合はUNIXドメインソケットを使うように変更する。

具体的に言えば、proxy_pass http://localhost:5000;のように指定するのをやめて、upstreamでunicornのソケットファイルを指定してやる。理由は単純にUNIXドメインソケットの方が早いからだ。そもそも同じOS上のプロセス間なのにTCPはわざわざ一度ネットワークに乗せるのだから遅くて当たり前だ。

gzip圧縮

次に、PageSpeed Insightsで指摘されていたように通信時にgzip圧縮が有効になっていないのでここも変更する。gzip圧縮ってなんだ? まぁ要するに圧縮方式の一種だが、HTTPはリクエスト側(ブラウザ)がgzipを解凍可能な場合はその旨をリクエストのヘッダに記述する。そして、レスポンス側(サーバ)がgzip可能ならコンテンツを圧縮して通信できるというワケ。

こんな便利な機能だがnginxでは初期設定で無効となっている。何故か? CPUリソースを使ってしまうからだ。通信する前にファイルを圧縮して送信するので圧縮処理にCPUを使う。回線間の通信は削減できるが、やり過ぎるとサーバの処理負荷が高まって結局遅くなってしまうこともある。

が、基本的にアクセスが少ないうちはCPUが暇なのでガンガン使って行く。それに、賢い読者の諸君は気づいたかもしれないが、静的なファイルならば予め圧縮して使い回せばCPUを使わずに済む。それがgzip_static設定だ。

server {
  -------
  gzip on; # 圧縮を行う
  # 圧縮の対象ファイルを設定する(設定しないとtext/html以外は圧縮しない)
  gzip_types text/html text/css application/javascript application/json application/font-woff application/font-tff image/gif image/png image/jpeg application/octet-stream;
  gzip_min_length 1000; # 小さすぎるファイルは圧縮する方が無駄なので最低値を設定する
  gzip_proxied any; # Viaヘッダが含まれていても圧縮する
  gzip_static always; # gzファイルがあれば常にそれを渡す
  gunzip on; # gzipに対応しないクライアントには解凍して渡す
  -------
}

ええっ、でも静的なファイルにそれぞれgzファイルを作るなんて面倒だなぁ、と思ったエンジニアの鑑である読者の方も心配いらない。RailsでAssetsとして扱う限り、precompile時にgzファイルも生成されている。(public/assets以下を見てみよう!)

ブラウザ・キャッシュの活用

お次はGoogle御大からのご指摘、「ブラウザのキャッシュを活用する」について。これもnginxで指定することによって、ブラウザが一度ダウンロードしたファイルをどの程度の期間なら都度ダウンロードせずに使い回してもいいのかを設定出来る。

  location ~ .*\.(jpe?g|gif|png|css|js|ico|swf|inc|woff2) {
    expires max;
  }

変わることがないような静的ファイルについては全て有効期間を最大に設定してしまおう。3日間とか1週間のようにチマチマ設定してもInsightは納得せず「適切に設定されていない」と警告を出す。

「えっ、ちょっと待って! cssやjsをキャッシュしちゃったらデプロイでそれらのファイルを変更しても、変更前のファイルが参照されちゃうんじゃないの!?」と思った賢明な読者の方もいるだろう。安心してほしい、そういう心配はないんだ。そう、Railsならね。

RailsのAssets::precompileで生成されるファイル名がapplication-f178abec9cf45592edc13a40cdb4a6c3fc87e22b6455d62c0e5ecdae4a7842ee.cssのようにハッシュ値のようなものがついていて、コンパイルの度にファイルが新しく作られているのを覚えているかな。

そう、もしもCSS/JSなどのアセットファイルに変更がある場合には、ファイル名ごと新しく生成され、HTML内で指定するファイル名も変わる。故に更新されたCSS/JSはキャッシュが使われずに新しくダウンロードされる。上手く出来た仕組みだろう。

逆に言えば、こういった仕組みがない場合には読者諸君の心配は全て実現する。その場合はなんとかせよ。

HTTP/2化

ついでにWebサイトをフルSSL化してHTTP/2対応とした。
HTTP/2とはHTTPの新しいバージョンであり、簡単に言えばこれを使うと高速化できる。理由は先述したようにHTTPリクエストを複数投げるとその分だけ時間がかかる。また、HTTPリクエストは同時に多数投げることが出来ないので前のファイルがダウンロードし終わるまで待機状態になる。HTTP/2ではこの問題が解決する。詳しくは他の解説サイトを読んでくれ。

なお、Chromeは古いverのopensslが使われていると、HTTP/2の接続を拒否するせいで、opensslのバージョンを上げる必要があった。しかもnginxがopensslをコンパイルレベルで内包しているせいで、nginxとopensslをyumではなく、ソースコードからのコンパイルで入れ直す羽目になった。

もっとも苦労が多かった改善施策だが、残念なことにほとんど速度改善には繋がらず。高速化する原理は先に書いた通りなので、読み込むべきファイルが大量になければ目に見える効果は出ないようだ。

ちなみにSSLの証明書には無料で発行でき、安全な通信を実現出来るLet's Encryptを利用している。

一旦おしまい

これでスコア改善はいったん終わり。計測されたInsightsスコアは89点にまで改善した。実際には、他にもWebフォントを読み込むためのCSSを統合したり、不必要なフォントセットは読み込まないようにしたりもしている。また、Webフォントを自分のサーバに置く、といったことも試したが効果はほとんどなかったため、素直にCDNを利用すべきと判断して戻した。

WebPagetestの測定値はFirstView1.478s、Repeat 0.574sになった。FirstViewで2秒ほど高速化に成功した。実際のところ…Webフォントなどさえ諦めればFirstViewで2-3秒、Insightsで95点くらいは狙えるがデザイン上の拘りで実施しない。デザインを蔑ろにする奴は獣だ。

結局、FirstViewとはサイトへの最初のアクセスだけで、それ以降の画面遷移では速度に影響を与えないので気にしない。スコアはUX向上のガイドラインや目安であり目的ではない。


 このエントリーをはてなブックマークに追加


   新コンテンツを知らせよう - RSSの追加 >>

[46]