可能な限り最新の情報を反映していますが、追いつけていないこともあります。本サイトに採用していても、記事に反映できていない設定もあります。ページのソースを読んでいただくと、参考になる箇所があるかもしれません。
ウェブページの高速化に関するテクニックは、ネットで検索すれば簡単に見つけることができます。優れた情報も数多くありますが、「CSSとJavaScriptはminify(ミニファイ)しておけばOK!」のような都市伝説も少なくありません。
そこで、ここでは本サイトのデザインリニューアル時に施した対策をもとに、一歩進んだウェブページの高速化の方法と、それを支える原理について、できる限り分かりやすく説明したいと思います。フロントエンジニアやデザイナーの方からすれば「んなもん知っとるわ!」な情報なのかもしれませんが、都市伝説を駆逐すべく、私なりの仕方で解説(≒加勢)したいと思います。
初めに結果を紹介しておくと、ページの表示開始まで3秒近くかかっていたところ、対策後は0.8秒~1.8秒程度まで短縮できました。
この記事は、以下の3部構成になっています。
- 原理:コネクションとブラウザの構造
- 方法:原理から導かれる高速化の方法
- 結果
まずは基本に立ち帰り、「ブラウザがページを表示するとはどういうことなのか?」という地点から見ていくことにします。
原理:コネクションとブラウザの構造
コネクションの構造
初めに、コネクションの構造から見ていくことにしましょう。
ブラウザが1つのリソースをダウンロードするまでの過程は、主に4つ(SSL接続の場合は5つ)の部分から構成されています。
Source: Optimizing the Critical Rendering Path - Velocity SC 2013, Ilya Grigorik.
簡略化すると次のようになります。
- DNSルックアップ:ドメイン(www.philosophyguides.org)を機械が理解できるIPアドレス(103.245.222.133)に変換すること
- ソケットコネクト:IPアドレスというビルの中から、ソケットという1つの部屋に接続すること
- HTTPリクエスト:「ファイルを送ってください」とサーバーに要求すること
技術的に興味のある方は、以下のページの説明を読んでみてください。ビックリするほど詳しいです。
DNSルックアップからHTTPリクエストを経て、データをダウンロードするまで一連の過程をラウンドトリップといいます。
ページの表示を高速化するためには、ラウンドトリップに掛かる時間(ラウンドトリップタイム、RTT)を可能な限り短縮する必要があります。ラウンドトリップタイムの全体を考慮に入れることが大事です。
Googleは2013年、QUICというプロトコルを発表しました。これはTCPコネクションをUDPに置きかえることで、TCPコネクションでは原理的に生じざるをえなかったRTTをゼロにするものです。HTTP/2のベースとして使うことでさらなる高速化が期待されています。
QUICの概要に関しては、以下の記事を読んでみてください。
- Experimenting with QUIC - Chromium Blog
- Networkキーワード - QUIC - ITpro
- SPDYやQUIC登場の背景。Webの進化がプロトコルを変えつつある。HTML5 Conference 2013 - Publickey
Source: 従来のHTTPS、SPDY、QUICの比較 - ITpro
GitHub上でQUICのstandaloneなライブラリが公開されています→ google/proto-quic
ブラウザの構造
次に、ブラウザがページを表示するプロセスについて見ていきます。
ブラウザがページを表示する仕組みについては、GoogleのエンジニアIlya Grigorikさんのプレゼンテーションが詳しく説明してくれています。
Source: Optimizing the Critical Rendering Path - Velocity SC 2013, Ilya Grigorik.
Paul Irishさんによる説明もあります。
Source: ブラウザの仕組み: 最新ウェブブラウザの内部構造, Tali Garsiel and Paul Irish.
以下のような順序になっているようです。
- HTMLをダウンロード
- HTMLパーサーを通じて機械言語に翻訳し、DOMツリーを作る
- 1・2と並行して、スタイルシートをダウンロード
- CSSパーサーを通じてスタイルルールを作る
- DOMツリーとスタイルルールを合わせてレンダーツリーを作る
- 描写する
- 表示される
ここでのポイントは、ページのレンダリングはDOMとCSSOMの二本柱で成立しているということです。CSSのダウンロードが完了しない限り、ブラウザはページのレンダリングを開始できません。
言いかえると、HTMLのパースが終わっても、CSSをダウンロードしている間は、Paintingに移ることができないので、ブラウザには何も表示されません。
Source: Optimizing the Critical Rendering Path - Velocity SC 2013, Ilya Grigorik.
こうして生じるタイムラグが、レンダリングブロックと呼ばれるものです。
GoogleのPageSpeed Insightsでページをチェックしたときに、「スクロールせずに見えるコンテンツのレンダリングブロックJavaScript/CSSを排除する」の項目が解決できず、気になっているひともいると思います。
レンダリングブロックを解消できれば、ページの表示は確実に早くなります。優先度で言えば、上のスライドにあるように、CSSのレンダリングブロックを解消することが先決です。
2016年現在策定中のStreams APIでは、HTMLのパースと平行してDOM構築とページレンダリングが行えるようになる模様です。同規格はWHATWGによって開発が進められています。
2016 - the year of web streams - JakeArchibald.com
This is probably the most practical application of service worker + streams. The benefit is huge in bx of performance.
どうhugeかというと、以下のような感じです。一番右がServiceWorkerとStreamを併用した場合です。
Using service worker + streams means you can get an almost-instant first render, then beat a regular server render by piping a smaller amount of content from the network. Content goes through the regular HTML parser, so you get streaming, and none of the behavioural differences you get with adding content to the DOM manually.
Streamの概念はページの表示に対するスタンスを一新させると言えます。適切に実装すれば、クリティカルパスの表示だけでなく、Onloadイベントの発火も1秒以内に行うことができるようになるかもしれません。
方法
以上を踏まえて、次に高速化の方法について見ていきます。
前提となる対策
GoogleのPageSpeed Insightsでは、以下の方法が高速化に役立つとしています。色々なサイトが解説していますが、本家Googleにある解説を読んでおけば十分です。
- リンク先ページのリダイレクトを使用しない
- 圧縮を有効にする
- サーバーの応答時間を短縮する(高速なサーバーを使う)
- ブラウザのキャッシュを活用する
- リソース(HTML, CSS, JavaScript)を圧縮する
- 画像を最適化する
- CSSの配信を最適化する
- 見える範囲のコンテンツを優先する
- レンダリングを妨げるJavaScriptを削除する
- 非同期スクリプトを使用する
どれも1秒台でページを表示するためには必須です。とりわけ、サーバーの選択は重要です。というのも、たとえウェブページの設計を最適化しても、サーバー側がどうしようもなければ意味が無いからです。
サーバーが物理的に遠いところにあれば、それだけラウンドトリップタイム(RTT)が掛かります。サーバーのスペックが低ければ、それだけソケット接続やHTTPリクエストの処理に時間が掛かります。無料ホスティングサービスや、海外の格安サーバーではその傾向が顕著です。安物買いの銭失いならぬ、タイムロスとはまさにこのことです。
方針
基本方針は、上記にあるレンダリングブロックの原因を解消し、Above the foldを最優先で表示させることです。
特にモバイル端末では、ページを1秒台で表示させるのは、かなりタフな作業です。というのも、先にリンクしておいたGrigorikさんのスライドを見ると分かりますが(9ページ目)、ブラウザがページのダウンロードを開始するまで、3G接続では0.8秒、4G接続では0.4秒が必要なので、実質的には1.2秒~1.6秒しか残されていないことになるからです。
Grigorikさんの計算では、一回のラウンドトリップでダウンロードするデータを14KB以内に抑えられれば、モバイル端末でも1秒以内にページを表示できるとのことです。そのためには、以下の3点に注意する必要があるようです。
- 1回のラウンドトリップでAbove the foldを表示させる(=Above the foldを14KB以内に抑える)
- リダイレクトは使わず、反応速度の早いサーバーを使う(0.2秒以内が理想)
- クリティカルレンダリングを最適化する
- クリティカルCSSをインライン化
- レンダリングをブロックするJavaScriptを取り除く
1秒台で表示させるために行った対策
以上の方法と方針を踏まえて、次の対策を行いました。
- Preconnectを実装
- CDN(CloudFlare)でHTTP/2に対応
- HTTP/1.1向けのハックを取り除く
- Riot.jsを採用
- CSSをインライン化
- Grunt.jsでCSSを最適化
- 画像を最適化する
- リソースの選択と調整
- Googleフォント → システムフォント
- FontAwesome → SVGアイコン+SVGO
- ダウンロードのタイミングを調整
Preconnectを実装
冒頭の「コネクションの構造」でも確認しましたが、リソースをダウンロードするにあたっては、DNSルックアップ、ソケット接続、リソースのリクエストを経る必要があります。
以上の手続きに掛かる時間は、1つのコネクションのうちでかなりの部分を占めています。調べてみたところ、DNSルックアップの時間が最も長く34.4%、ファイルのダウンロードの時間が最も短く14.8%という場合もありました。ダウンロードよりも、それ以前の準備作業のほうが時間を消費していることになります。
なので、リソースのダウンロードに先立ち、あらかじめそれに必要な準備を行っておくよう指示しておくことができれば、時間の短縮につながります。
そこで、Resource Hintsの仕様の1つとして開発が進められている Preconnect を実装しました。
Preconnect は、HTTPリクエストにおいて、ブラウザにDNSルックアップからソケットセットアップまで行っておくよう指示する(=ヒントを与える)ことで、リソースのダウンロードに掛かる時間を短縮する仕組みです(ブラウザごとの対応状況)。具体的には以下のようになります。
Source: Eliminating Roundtrips with Preconnect - igvita.com - Ilya Grigorik
Googleフォントでは、フォントを指定するCSSのサーバーと、フォントファイル本体が置かれているサーバーは異なっているので、通常であれば、CSSファイルを読み込んだ後にソケットセットアップが行われます(上のグラフ)。ここで、Preconnectにより、フォントファイル本体が置かれているサーバーとのソケットセットアップを終わらせておけば、表示に掛かるまでの時間を短縮することができます(下のグラフ)。
Preconnectはあくまで「ヒント」です。必ず従わなければならないものではありません。Preconnectをどこまで考慮するかは、ヒントを受けるサーバー側の設定によります。ソケット接続まで完了させてしまうサーバーもあれば、DNSルックアップにとどめるサーバーもあります。開発時に「なぜソケット接続まで行かない?」と頭を抱えないように…。
なお、Adsenseの最適化には、以下のコードが使えるはずです。各自調整してお使いください。
<link rel="preconnect" href="https://pagead2.googlesyndication.com" crossorigin>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<link rel="preconnect" href="https://googleads.g.doubleclick.net" crossorigin>
<link rel="preconnect" href="https://stats.g.doubleclick.net" crossorigin>
楽天アフィリエイト(楽天モーションウィジェット)でもPreconnectがよく効きます(DNSルックアップからSSLネゴシエーションまで完了してくれます)。
<link rel="preconnect" href="https://static.affiliate.rakuten.co.jp">
<link rel="preconnect" href="https://mtwidget01.affiliate.rakuten.co.jp">
<link rel="preconnect" href="https://log.affiliate.rakuten.co.jp">
<link rel="preconnect" href="https://imp.pt.afl.rakuten.co.jp">
<link rel="preconnect" href="https://thumbnail.image.rakuten.co.jp">
以下のようになります。
http://www.webpagetest.org/result/160405_2F_1697/8/details/
CDN(CloudFlare)でHTTP/2に対応
時代はHTTP/2です。高速化にあたっては、HTTP/2対応が最も効果的です。
HTTP/2の概要については、Grigorikさんの著書High-Performance Browser Networkingがとても参考になります。
英語版はリンク先から無料で読めます。日本語訳もありますが、少し高いです。しかし押さえておきたいトピックなので、頑張って英語版を読むか、日本語版を手に入れて、ぜひ読んでみてください。
以下の記事も参考になります。
- HTTP/2 入門 - Yahoo! JAPAN Tech Blog
- 初めてのHTTP/2サーバプッシュ - GREE Engineers’ Blog
HTTP/2を自前で設定するのが大変というひとは、大手CDNのCloudFlareがHTTP/2に対応しているので、これを使うことをおすすめします。餅は餅屋。CDNとHTTP/2に同時に対応できるので、お手軽かつ一石二鳥です。登録・設定はさほど難しくありませんが、英語がよくわからないひとは、以下のページが参考になると思います。
- すぐできる!CloudFlare の設定方法 - バズ部
- CloudFlareでWordPressをHTTP/2(SPDY)対応してみた - 意識低い系ブログ
HTTP/1.1向けのハックを取り除く
HTTP/2においては、HTTP/1.1時代に用いられていた対策のうち、不要となるばかりか、むしろ逆効果になるものがあります。代表的なものは以下の通りです。
- ドメイン・シャーディングによるコネクションの複数化
- 必要以上のリソースの結合
- CSS、JavaScriptの結合
- 画像のスプライト
- Base64形式へのエンコード
利用すべきドメインは最小限に抑え、リソースは分割すること。これがHTTP/2時代の最適化の方向です。その背景については、以下のリンクを参考にしてみてください。
- Optimizing for HTTP/2, High Performance Browser Networking - Ilya Grigorik
Base64形式へのエンコードは、DataURIスキームでインライン化し、コネクション数を節約するために使われる手法ですが、HTTP/2では1つのコネクションでリクエストを複数行うことができるので、メリットがなくなります。単にファイルのサイズを増加させるだけです。
Riot.jsを採用
RiotはUIライブラリの1つです。同種のライブラリにReact.jsやPolymer.jsなどがありますが、Riot.jsはシンプルで覚えやすいのが特徴です。
- Riot.js ドキュメント日本語版 - qiita
- Riot.js 2.0 を触ってみた — まだReactで消耗しているの? - qiita
- コンポーネントを使うのはJSerじゃない、HTML/CSSコーダーだ - qiita
- React.jsではなくRiot.jsを採用した話、運用中サービス『GAMY』でリニューアル - qiita
- カスタムタグ - Riot
Riot.jsでは、カスタムタグ(=コンポーネント)を定義し、それを使い回すことができます。サイトのメンテナンス性が向上するだけでなく、ページのパース時にダウンロードするバイト数を減らすことができるので、レンダリングの開始時間を早めることもできます。
カスタムタグは以下のような仕方で設定できます。
<customTag>
<div class="customTag">
<a href="/wrote-my-book/">本を書きました!</a>
</div>
<style scoped>
.customTag {
display: block;
text-align: center;
padding: 1rem 0;
background: rgba(71,132,124,0.9);
color: #fff;
font-size: 1em;
}
.customTag > a {
color: #fff;
border-bottom: 2px dotted;
}
</style>
</customTag>
これをcustomTag.js
として保存したうえで、
<body>
<customTag></customTag>
...
<script src="riot.min.js"></script>
<script src="customTag.js" type="riot/tag"></script>
<script>riot.mount('customTag')</script>
</body>
とすれば、Scoped CSSを活用しつつ、カスタムタグ<customTag>
を表示させることができます。
なお、<style>
にはLESSが対応しているので、以下のようにすることもできます。
<customTag>
<div class="customTag">
<a href="/wrote-my-book/">本を書きました!</a>
</div>
<style scoped type=less>
@white: #fff;
.customTag {
display: block;
text-align: center;
padding: 1rem 0;
background: rgba(71,132,124,0.9);
color: @white;
font-size: 1em;
> a {
color: @white;
border-bottom: 2px dotted;
}
}
</style>
</customTag>
このサイトでは他にも、ヘッダー、ソーシャルメディアの共有リンク、フッターなどをコンポーネント化してあります。詳しくはソースを参照してみてください。
CSSをインライン化
Riot.jsのカスタムタグに移行していないCSSについては、すべてインライン化することにしました。外部ファイルは置きません。CloudflareではGzipが設定されているので、外部ファイルを読み込むよりも効率がいいです(1回のラウンドトリップでダウンロードするデータ量を14KB以内に抑えたうえで)。
ただしこれは、CloudFlareのHTTP/2サーバーがServer pushに対応するまでの過渡的な処置です(Server pushが実装されれば、クリティカルパスに必要なリソースの遅延ダウンロードに関する施策は、ほぼ不要となります)。
- https://plus.google.com/+IlyaGrigorik/posts/7Qe8CFGHptz
- http://blog.kazuhooku.com/2015/12/optimizing-performance-of-multi-tiered.html
- http://chimera.labs.oreilly.com/books/1230000000545/ch13.html#_eliminate_roundtrips_with_server_push
Grunt.jsでCSSを最適化
CSSインライン化にあたっては、タスクランナーのGrunt.jsを使い、CSSの整理や軽量化を行うよう設定しました。Gruntのインストール方法については、以下のページが参考になります。
- ブラックなWeb開発現場の救世主、Gruntのインストールと使い方 - atmarkit
- CoffeeScriptやSassなどの使用時にオススメのGruntプラグイン一覧 - atmarkit
色々試した結果、以下の作業フローに落ち着きました。
- grunt-contrib-less … モジュールごとに設定したLESSファイルを、それぞれCSSに変換
- grunt-csscomb … CSSのプロパティのソート、ショートハンド化など
- grunt-postcss … PostCSSを使い、ベンダープレフィックスなどを自動で設定
- 使用しているプラグイン(プラグイン一覧)
- autoprefixer
- postcss-calc
- postcss-discard-empty
- postcss-merge-longhand
- postcss-discard-duplicates
- postcss-font-family
- postcss-colormin
- postcss-focus
- 使用しているプラグイン(プラグイン一覧)
- grunt-contrib-concat … CSSを結合
- sanitize.cssと結合
- grunt-combine-mq … 結合したCSSのメディアクエリをまとめる
- grunt-contrib-cssmin … 空白・改行の削除、再構成(restructuring)
設定ファイルが長くなるので、load-grunt-configを使って分割しています。
画像を最適化する
grunt-contrib-imageminなど一括処理するソフトもありますが、ここではIrfanViewという画像加工ソフトウェアで一つずつ最適化しています(Windowsのみ可)。その際、RIOTというプラグインを使い、画像をより一層圧縮するようにしました。
インストール後、ソフトを起動し、メインメニューの一番左にある「File」から「Save for Web (PlugIn)」を選択すると、以下のような画面が出てきます。
画質の変化を確認しながら作業できるので、狙い通りの画質に圧縮できます。サイズ指定による圧縮もできます。
リソースの選択と調整
ページのデザインにGoogleフォントとFontAwesomeを使っていましたが、必要かどうかを考えなおして、結果これを取ることにしました。
リソースのダウンロードに掛かるRTTについては、すぐ上で見たように、Preconnectを実装すれば短縮することができます。とはいえ、そのリソースが必要かどうかを考えず、無闇に取り入れることは得策とは言えません。リソースはコストであることを念頭に置いて、ムダの無いページ作りを意識しました。
Googleフォント → システムフォント
以下の記事を参考に、Googleフォントの代替として、システムフォントを使うように設定しました。
font-family:
-apple-system, BlinkMacSystemFont,
"Segoe UI",
"游ゴシック体", "YuGothic",
"Hiragino Sans", "ヒラギノ角ゴシック", "Hiragino Kaku Gothic ProN", "ヒラギノ角ゴ ProN W3",
"Meiryo",
"Ubuntu", "Takao Pゴシック",
"Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", "Lucida Grande", sans-serif;
Segoe UI、游ゴシック体、ヒラギノ、メイリオの位置はお好みです。
FontAwesome → SVGアイコン+SVGO
FontAwesomeの代替としては、SVGアイコンを採用しました。
SVGはベクター形式の画像フォーマットです。Retinaディスプレイのような高解像度ディスプレイでも画像がぼやけることなくクリアに表示できることや、データ量の少なさから、次第に普及が進んでいます。GitHubでもSVGアイコンが採用されました。
ただし、Internet Explorerについては、IE8以前では対応していなかったり、2015年現在で最新版のIE11でもFirefoxやChromeといった他のブラウザと異なる挙動を見せたりするので、慎重な検討が必要です。
SVGアイコンを探すなら、アイコン検索・販売サービスの「Iconfinder」が便利です。本サイトのホームアイコンやソーシャルアイコンには、そこからダウンロードしたアイコンを加工して使っています。
なお、SVGファイルは、SVGOを使って最適化しています。ファイルと設定によっては7割程度圧縮することも可能です。
npm install -g svgo
svgo --config=config.yml
設定ファイル(config.yml)はオプションに従って準備しておきます。普段使っている設定ファイルをここに置いておきますので、参考にしてみてください。
ダウンロードのタイミングを調整
Chrome 50以降では、<link rel="preload">
を使うと、レンダリングブロックを引き起こすことなく、リソースの先読みを行うことができます。
- https://w3c.github.io/preload/
- https://plus.google.com/+IlyaGrigorik/posts/hzHFti9H84y
- Preload: What Is It Good For? - Smashing Magazine
たとえばCSSファイルstyle.cssにbackground-image: url("image.jpg")
とある場合、<head>
に
<link rel="preload" as="image" href="image.jpg">
<link rel="stylesheet" href="style.css">
とすれば、style.css
のパースに先立ってimage.jpg
のダウンロードを開始できます。後で使うことが分かっているリソースを、あらかじめダウンロードしておくことで、時間短縮を図ります。
なお、loadCSSを使うと、<link rel="preload">
についてのpolyfillを効かせつつ、CSSを遅延ダウンロードさせることができるようです。loadCSS.jsとpolyfillを読み込み、
<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="path/to/mystylesheet.css"></noscript>
とすれば、onloadイベントの発火と同時に、Preloadされたスタイルシートが読み込まれるはずです。
結果
今回の対策を加えた後で行ったテストの結果は、以下のようになりました。レイアウト上必要なRiot.jsに関するリソースなどは<link rel="preload">
で先読みしています。
http://www.webpagetest.org/result/170315_EP_137V/
Load Time | First Byte | Start Render | Speed Index |
---|---|---|---|
2.896s | 0.122s | 0.586s | 752 |
Googleのエンジニア Paul Irish さんのコメントによると、Speed Indexが1000以下であれば十分に速いということなので、満足できる結果と言えると思います。
まとめ:原理的に高速化する
ページの読み込みを高速化する方法については、色々なサイトが様々な仕方で紹介しています。ですが大事なのは、なぜこの方法で早くなるのか、その理由と構造を知っておくことです。そうすれば「なんとなく」に任せることなく、より一貫した高速化が可能になるはずです。ページの高速化を妨げている要因をひとつずつ取り除いていけば、1秒台でページを表示させるのはそれほど難しくありません。
高速化の試みに究極のゴールはありません。今日有効だった対策が、3年後には古すぎて足を引っ張っている、というような状況にならないよう、こまめに情報収集するよう努めたいですね。
もっと深めたいひとは
Grigorikさんを含むGoogle Developersチームによる、ウェブマスター向けのチュートリアル「Web Fundamentals」でも、クリティカルレンダリングパスの最適化について論じています。そちらも併せて確認してみてください。
- Tobias (CC BY-SA 2.0; modified)
- Muut Inc (MIT License)
- jQuery Foundation (License)
- Tobias (thumbnail, CC BY-SA 2.0; modified)