こんにちは、id:hakobe932です。はてなブログではユーザ体験の改善のために、ページ表示速度を向上させるための様々な取り組みを行っています。このエントリーでは、はてなブログで行っている、ブラウザキャッシュの活用、JavaScriptのページ最下部での読み込み、JavaScriptの圧縮、という3つの取り組みについて解説します。
ブラウザキャッシュの活用
同じ内容のJavaScriptやCSSを、ページを表示するたびにダウンロードすると、余分なHTTPリクエストが発生しますし、読み込み時間がかかります。
ブラウザのキャッシュを利用できれば、余分なリクエストを減らすことができます。はてなブログでは、なるべく長い間ブラウザにキャッシュを保存するために、JavaScriptなどの一部の種類のファイルのレスポンスに、以下のようなヘッダを指定しています。
$ curl -I http://hatenablog.com/js?abc078624b2a746c618156847827166b ... Cache-Control: public, max-age=315360000, s-maxage=315360000 Expires: Wed, 13 Mar 2024 15:05:27 GMT Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT ...
Cache-Controlヘッダ、Expiresヘッダではキャッシュの有効期限を10年先に指定しています。実質的にキャッシュの有効期限が切れることがなくなります。(nginxで簡単にこういった設定ができるオプションが用意されています)
Last-Modifiedヘッダでは、最後のリクエストからコンテンツが変更されていないことを、ブラウザに知らせます。キャッシュの有効期限が切れていなくても、ユーザがブラウザでリロードをした場合などに、If-Modified-Sinceヘッダのついた条件付GETリクエストが発生することがあるため、その対策です。ヘッダの内容は現在より過去であればいつでも良いのですが、キリの良い時間を設定しています。
このようにヘッダを設定することで、ユーザは一度JavaScriptをブラウザに読み込むと、それ以降は、HTTPリクエストをする必要がなくなります。はてなブログで開設できるブログのドメインは様々ですが(例: http://blog.hatenablog.com/ , http://bookmark.hatenastaff.com/ )、JavaScriptファイルは必ず http://hatenablog.com から配信されるので、ブラウザで一度でもはてなブログで開設されたブログを表示すれば、別のブログでも同じキャッシュを利用することができます。
キャッシュの有効期限のないJavaScriptを配信すると、一度ブラウザのキャッシュに入ったJavaScriptが更新されず、ユーザが古いJavaScriptを利用しつづけてしまうことが懸念されます。
はてなブログではJavaScriptを配信する際には、上記のURLのように、?よりあとの部分にabc078624b2a746c618156847827166bのようなバージョンIDを付与しています。JavaScriptが変更されるバージョンとともにURLも変化するため、古いキャッシュが利用されることはありません。
JavaScriptをページ最下部で読み込む
ブラウザはページの読み込みが途中でも、可能な部分を上から徐々にレンダリングしていきます。しかし、レンダリングしていくページの途中にasync属性やdefer属性を持たないscriptタグがあると、そのスクリプトのダウンロードと実行が完了するまで、scriptタグ以降の部分のレンダリングがブロックされてしまいます。
ブラウザがこういった振舞いをせざるを得ない原因の一つがdocument.writeの存在です。JavaScript内でdocument.writeが実行されると、ページコンテンツが書き換えられてしまい、レンダリング結果が変化したり、実行されるスクリプトが新しく追加される可能性があります。そのため、ブラウザは、スクリプトの実行が完了するまで、レンダリングやダウンロードの処理をストップしてしまいます。
はてなブログでは、この状況を回避するために、JavaScriptをページの最下部で読み込むように変更しました。これにより、ページがレンダリングされたあとでJavaScriptが実行されるようになり、結果として表示が早く行われるようになりました。
以下は、はてなブログのトップページについて、JavaScriptを最下部で読み込む前と後でパフォーマンステストをした結果です(2013/10/8時点)。(後述のWebPagetestを利用しています)
- JavaScriptをページ上部で読み込み: http://www.webpagetest.org/result/131008_FH_8HY/
- JavaScriptをページ下部で読み込み: http://www.webpagetest.org/result/131024_EZ_8WF/
特にStart Render(レンダリングが開始されるまでにかかる時間) が2.5~3.5秒程度短縮しているのが大きな変化です。ブログのエントリー個別のページなど他の種類のページでも、軒並、Start Renderの改善が見られ、最大で4.9秒、平均でも3.5秒の表示待ち時間が短縮できました。
体感にも表示の早さが実感できるなど、効果が高かったため、開発ブログでも紹介しています。
JavaScriptを圧縮する
JavaScriptや画像などのコンテンツを圧縮することで、特に、キャッシュが効いていない、初回アクセス時のページの読み込み時間を短縮することができます。
はてなブログではサイズが大きいJavaScriptファイルをgzipやminifyで圧縮することを試みました。
gzipによる圧縮
まず、JavaScriptをgzipで圧縮して返却するようにしました。nginxを利用している場合、ngx_http_gzip_moduleを利用することで特定のMIME typeのコンテンツに対してgzip圧縮を行うことができます。はてなブログでは以下のように設定しています。
gzip on; gzip_static on; gzip_buffers 16 128k; gzip_types text/plain text/css application/x-javascript text/javascript application/json; gzip_comp_level 1; gzip_min_length 1024; gzip_disable "MSIE [1-6]\.(?!.*SV1)";
以下は、JavaScriptをgzip圧縮する前後のパフォーマンス測定結果です(2013/12/11時点)。
- JavaScriptをgzip圧縮していない: http://www.webpagetest.org/result/131211_H1_JSD/1/details/
- JavaScriptをgzip圧縮している: http://www.webpagetest.org/result/131211_N1_K35/1/details/
gzip圧縮により、もともと1MB程度あったファイルサイズが300KB程度に減少しました。もともとのファイルが大きかったこともあり、Fully Loaded(ページの読み込みが完了するのにかかる時間)が改善しました。
minifyによる圧縮
次に、minifyを行うことでJavaScriptのサイズをさらに低減し、読み込みにかかる時間の短縮を試みました。minifyにはUglifyJSを利用しました。圧縮前後のJavaScriptのサイズについて調べると以下のようになりました。
original js | original js + gzip | original js + uglify | original js + uglify + gzip | |
---|---|---|---|---|
サイズ | 1015KB | 305KB | 656KB | 217KB |
original jsに対する割合 | 100.0% | 30.0% | 64.6% | 21.3% |
単にgzipしたJavaScriptとuglify後にgzipしたファイルのサイズ差は88KBになりました。このサイズ差だけでは実際の性能差がわからないため、minifyしたバージョンをリリースし、パフォーマンス測定を行いました。以下がパフォーマンス測定の結果です(2014/1/27時点)。
- JavaScript minifyなし: http://www.webpagetest.org/result/140127_VF_827/1/details/
- JaavScript minifyあり: http://www.webpagetest.org/result/140127_H1_8RN/1/details/
この結果では、Fully Loadedが500ms程度改善しました。ただ、ブログのエントリー個別ページなどでは特に変化がない場合もあり、いくつかのページでの結果を平均すると200ms程度の改善であることがわかりました。WebPagetestの機能を活用した、低速回線におけるパフォーマンステストでも特別大きな改善はみられませんでした。
この結果に基づいてチームで議論をした結果、
- 読み込み時間の改善幅があまり大きくない
- 改善が見こまれるのは、ブラウザにはてなブログのJavaScriptのキャッシュがない状態でアクセスした場合だけである
- 多くのアクセスではJavaScriptがキャッシュされており改善のインパクトが小さい
- JavaScriptをminifyして運用するにはコストがかかる
- 開発環境やビルド環境の保守にかかるコスト
- minifyが原因のバグ発生によるコスト
- ユーザ環境でエラーが発生した場合の原因箇所のトラックが難しくなる
- はてなブログではブラウザ上で発生したJavaScriptのエラーをサーバに集めて分析している
といった意見が集まりました。最終的にJavaScriptをminifyして圧縮することを見送ることになりました。(その場でminifyのための変更はとり消されました)
まとめ
はてなブログで取り組んだ、ブラウザキャッシュの活用、JavaScriptのページ最下部での読み込み、JavaScriptを圧縮するといった3つの方法によるパフォーマンス改善手法や事例を紹介しました。紹介したテクニックは良く知られたものですが、広く使われているWebサービスでの導入効果がどのようなものであったかが、少しでも参考になれば幸いです。
はてなブログでは今後も快適なブログを目指して、継続的に改善を進めてまいります。より快適に利用できるインターネットのために、今後もこういった知見を随時共有していきたいと思います。
はてなブログはまだまだ速くできる!というエンジニアの方は、ぜひ以下のボタンよりご応募ください。
参考文献
このエントリで紹介した技術の多くは、以下の書籍を参考にしています。
ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール
- 作者: Steve Souders,スティーブサウダーズ,武舎広幸,福地太郎,武舎るみ
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/04/11
- メディア: 大型本
- 購入: 32人 クリック: 676回
- この商品を含むブログ (126件) を見る
ツールの紹介
"推測するな計測せよ"というスローガンがあるように、パフォーマンスの改善を行うには数値を計測し客観的な事実に基づくことが重要です。
はてなブログでは、ブラウザに付属のWebインスペクタを用いた調査以外にも以下のツールを活用しています。
PageSpeed Insights
PageSpeed Insighs はWebサイトのパフォーマンス測定を行ってくれる、Webサイトです。Googleが公開しています。
URL指定して計測すると、Webサイトを遅くしている原因とその解決方法を具体的に示してくれるので便利です。例えば、ブラウザのキャッシュを有効に活用していないリソースがあった場合、そのリソースのURLの一覧と適切なヘッダを送出するようにするといった解決方法を提示してくれます。
ページ種ごとに、この結果を確認することで、主要なパフォーマンス上の問題を発見することができます。
WebPagetest
パフォーマンスの改善を行った前後で、Webサイトにどのような変化があったかをくわしく記録しておくことは重要です。前後の記録を比較することで期待していたようなパフォーマンス改善があったどうかがわかります。
ブラウザに付属のWebインスペクタツールも便利ですが、結果を保存しておいて、後から比較したりするのには不向きです。
WebPagetestは指定したURLに対してパフォーマンステストを行いその結果を保存していつでも見れるようにしてくれるサービスです。トップページからURLとテストを行いたい場所(!)とブラウザの種類を指定して"Start Test"ボタンを押すと以下のような結果が得られます。
http://www.webpagetest.org/result/140313_P5_JM0/
このテストではキャッシュの状態を変化させるために、二回のリクエストを行います。それぞれについて、全体の読み込み時間、DOMContentLoadedイベントが発火するまでの時間、レンダリングが開始するまでにかかった時間などを記録してくれます。
また、ページの表示のために、どういったタイミングでどのようなリクエストが発生したかもすべて記録されており、以下のように確認することができます。
http://www.webpagetest.org/result/140313_P5_JM0/1/details/
多くの場合、パフォーマンス改善が有効だったかを確認するにはこの程度の情報があれば十分でしょう。