INSANELY FAST
Qiitaを読んでる人なら https://dev.to をほとんどの人が見たはず。見てない人は見てきてください、速すぎて驚くはず。またmizchiさんがdev.toに書いた なぜ dev.to がこんなにも速く、こんなにも自分にとって感動的なのか - dev.to を見た人も多いと思う。個人的にHeroku, Railsを採用してここまで爆速なサイトを構築出来ていることは今までの常識を覆す衝撃な出来事だった。こんな新しい発見をもたらしてくれたdev.toには本当に感謝してる。自分もこんなサイト作ってみたいなと思ってdev.toのことを色々調べてて少し知見がたまったので共有してみます。
この記事はOkinawa.rb Advent Calendar 2017 7日目の記事です。
Twitterやってるのでよかったらフォローしてください🙋♀️ @saboyutaka
はじめに
作者の方もdev.toにどうやってdev.toを高速化したのかを幾つかポストしている。
- Making dev.to insanely fast - dev.to
- How I Made this Website Hella Fast Without Overcomplicating Things - dev.to
- The dev.to tech stack - dev.to
Here's our stack:
Open Source
- Ruby on Rails
- VanillaJS more or less
- Postgres (hosted on Heroku)
Infra/Services
- Heroku
- Fastly
- Airbrake
- Skylight
- Sendgrid
- Amazon S3
- Google Analytics
- Stream
- Algolia
- Algorithmia
- Timber
- Codeship
- Codeclimate
- GitHub
設計方針
- ページのほとんどを静的HTMLとしてキャッシュ出来る状態で返す
- コンテンツをCDNにより配信することでレイテンシを少なくする
- HTTP/2で通信する。(Fastly, CloudinaryがHTTP/2で返してくるのでサーバーサイドでは考える必要ない。)
- レンダリングブロッキングなCSS, Javascriptを排除してレンダリングまでの時間を短縮する
- ファーストビューの構築に不要なJavascript, CSS, 画像を全て非同期で取得し実行、描画する
- 広告やウィジェットなど外部のCSSやJSをリクエストするコンテンツを入れない
キーワード
Google Web Fundamentals に飛びます
HTTP/2
HTTP/2では1つのコネクションで同一ホストに複数のリクエストを多重化出来る。JS, CSS, 画像の件数が大量にあったとしてもほぼ同時に全てのリソースに対してリクエストを送ることが出来る。記事のトップに貼った画像はdev.toをChromeのDeveloper Toolsで見た図が、画像が一斉にリクエスト開始しているのがわかる。HTTP/1.1以前は同一ホストにリクエスト送る場合に同時接続数に制限がある(6件)ので、通信が終了次第後続のリクエストが発生する。そのためリクエストが同時接続数を超える場合、処理が直列になってしまう。Developer Toolsで見ると階段のようになるアレ。FastlyとCloudinaryがHTTP/2をしゃべるのでサービスを利用するだけでHTTP/2の恩恵を受ける事が出来る。
HerokuをHTTP/2化するのは現状むずかしいので、アプリケーションはHTTP/1.1を利用している。
Google Web Fundamentals HTTP/2 の概要
HTML
originサーバーはGETのページのほとんど全てを静的にキャッシュ出来る状態で配信する
通常WebアプリケーションはRequestに対してユーザーのログイン状態やコンテンツの状態によって動的にページを返すので、document自体をキャッシュさせることはあまりやらないのがいままでの定石だった。しかしdev.toはFastly(CDN)での配信に最適化するためにページのコンテンツを動的な部分と静的(あまり更新されない)な部分に分けて、静的な部分はoriginサーバーでHTMLを構築した後にFastlyのCDNにキャッシュする戦略を取っている。動的コンテンツはブラウザで描画後にAjaxで取得する。
描画後にAjaxで取得している部分
- ログインユーザーに紐づく表示
- アイコン
- タグのfollowの有無
- などなど
- 通知件数
- ファーストビューに乗らない後続の記事一覧
- 記事ごとのいいね数
Fastly
dev.toではHTML, JS, CSSの配信に利用する。たぶんHerokuのaddon。Fastlyの最寄りのCDNからレスポンスを受け取るので、コンテンツのレイテンシーがかなり抑えられている。Herokuを日本で使う場合に一番懸念になるのがアメリカのサーバーにoriginを置くのでレイテンシーが大きい事が問題で、それをCDNを使うことで克服している。記事の更新時やコメントの追加時にinstant purgeさせる仕様っぽい。HTMLの配信にはautomatic gzippingを利用してgzip形式で配信する。
POSTやsearchなどはやはりoriginに問い合わせする必要がありそれらのリクエストはそれなりに時間がかかる。基本的にはほとんどのGETリクエストがFastlyとのやり取りになるのでoriginサーバーにリクエストが来る頻度は普通のweb appに比べてかなり低いはず。
InstantClick
ページ遷移が爆速なのはdev.toに訪れた人なら最初に気づくはず。これはInstantClickで実現している。Railsを使ったことある人ならTurbolinksを触ったことあるかもしれないがアイディアとしてはほとんど同じ。リンクの遷移を簡単にSPA風にするjsのライブラリ。
ユーザーがリンクにマウスカーソルを合わせるとAjaxでリンク遷移先のHTMLを先読みする。リンクをクリックした時には既に手元にHTMLがあるのでHTMLのDOMの差し替えのみで遷移が完了する。具体的には既存の描画されているDOMの<title>
タグの部分と<body>
の任意の部分を遷移先のコンテンツに差し替える。先読みしている分速く描画出来るのに加えて、HTMLのDOM構築、CSSOMの構築、Javascriptのローディングをスキップすることが出来るため通常のページ遷移に比べて高速化することが出来る。
ユーザーがリンクにマウスカーソルを合わせてクリックするまでに平均300~400msかかるので、サーバーはその間にレスポンスを返せばクリックした瞬時にページが変わる体験を味わうことが出来る。
How I Made this Website Hella Fast Without Overcomplicating Things - dev.to である通り、少し改良してるぽい。挙動を見ていると、landing時にトップの5記事ぐらいは先読みしているように見える。あとInstantClickでHTLMLを取得する際には<head>
タグを<title>
だけにし、<body>
ではメインのコンテンツのみでnavやsidebarはHTMLに含めないようにしてる。もともとはpodcastを再生出来てそのタグをDOMから消したくないという目的だったみたいだけど、HTMLのコンテンツを減らすことでHTMLのサイズが1/3くらいになってるため軽量化にも役立っている。
InstantClick時は?i=i
をqueryに含めてrequestしてるのでhttps://dev.to?i=i
などにアクセスして試してみると面白い。この辺りの部分的な入れ替えはTurbolinks3でpartial replacementとして実装される予定だったけどその扱いの難しさから?頓挫した辺りなので興味深い。強力だけリスクが伴う、ここはアーキテクチャを一貫して責任持てるからこそ出来る技な気がする。
先読みで無駄にリクエスト飛ばしてるの気になるところなんだけどそれぞれ30KBくらいで多くのユーザーは10リクエストするかどうかなので300KBくらいなら気にならないかも。それよりもUX優先。
CSS
共通のスタイルはHTMLの<head>
の中に<style>
タグで直接埋め込みしている
外部のCSSファイルへのリクエストはレンダリングブロッキングなので、CSSの読み込みが終わるまでブラウザはレンダリングを開始することが出来ない。dev.toでは共通部分のCSSをHTMLの中に直接埋め込むことで外部リクエストが必要なCSSファイルを0にしている。そのためブラウザはHTMLのレスポンスが返るとレンダリングブロッキングな外部ファイルアクセスが無いため即座にレンダリングできる。そうすることでDOMContentLoadedまでの時間を短縮し画面が真っ白な状態を限りなく短くすることでユーザーは瞬時にページが表示される体験を得ることが出来る。
追加のCSS読み込みを非同期で行う
ファーストビューの表示に必要のないその他のCSSはonloadイベント以降に<link>
タグを<head>
タグにappendすることでレンダリング後に取得するように記述されている。この辺りは今後<link ref="preload">
が来るとさらに実装はシンプルになりそうだけど、今のところFirefoxは未実装だったりするので標準的に使えるのはまだ先になりそう。
Javascript
- 全てのjsをasyncでダウンロードする
- JSのライブラリを極力使わずpure jsで記述してる(読むとすごい勉強になる)
- ファーストビュー以外のコンテンツの非同期取得・描画
- service workerを利用する
- ここらへんあまり理解できていない。以下のような認識
- ブラウザのJSのランタイムとは別のランタイムで動くのでレンダリングや処理の中断を発生させない
- Cache APIを使うことで柔軟なCache機構を使う事ができる
- その他いろいろ
Image
Cloudinary
画像の配信はCloudinaryを使用している。これもたぶんHerokuのaddon。Cloudinaryではアップロードした画像のリサイズ、http2での配信、ブラウザごとに最適な拡張子(webp/jpeg)の選択などをオンデマンドで行ってくれる。
日本国内で似た同様なサービスだとImageFluxなどがあります。
Service Workerによる非同期通信
Service Workerで非同期通信を行うことでメインのランタイムで取得するよりも効率的に受信を行うことが出来る(らしい)。SafariではService Workerの実装はまだ行われていない(in developmentらしい)のでChromeやFirefoxだと恩恵を受けることが出来る。Safariでも十分速いのでここはボトルネックとしてはあとの方か。
やらないこと
- jsのライブラリを基本的に使わない
- CSSのライブラリを使わない
- ソーシャルウィジェットを入れない(cssやjsの通信が増えるもの) ここはmizchiさんが言及していたところ
- 広告を入れない
向いてるサイト
作者の人も書いてるけど、リードヘビーなサイト。動的に変化するコンテンツが少なめのサイトほどHTMLのキャッシングが有効になる。ブログ、メディア、ECサイトとか。SNSは向いてない。動的に変化するコンテンツが大量にある場合はSPA、静的な部分が多い場合はこういった高速化と棲み分け出来ていくと良さそう。dev.toでも作者の方が投稿でpreactかVue.js使うかもみたいな投稿があるし、ある程度進むとpure jsだけでリッチなサイト作るのが難しいはずなので操作性も取るなら軽めなフレームワークを取り入れると良さそう。
先読みと遅延読み込み(preloading, lazy loading)
dev.to では先読みと遅延読み込みを使うことでユーザー体験を向上させる工夫を仕掛けてる。
先読み
first printまでの時間短縮
ブラウザは1発目のアクセスのHTMLのレスポンスが到着後にブラウザの表示を初期化し(真っ白になる)、その後外部CSS, JSのリクエストを投げてレスポンスが到着後にCSS, JS解析した後に描画を開始する(first print)。CSSをHTMLに直書きし、Javascriptを同期的に取得していない理由はHTMLのレスポンスが到着後にCSS, Javascriptのリクエストを投げずにすぐにfirst printするためで画面が真っ白の時間を最小限にする工夫。
1発目のHTMLのリクエストを投げる時ブラウザは遷移前の画面に滞在し続ける。google検索から来るとするとgoogleのページが表示されたままだし、リンクを直打ちした場合はその画面に居続ける。レスポンスが返ってきて画面が白くなる時間が長いとユーザーは処理に時間がかかっていることを認識する。しかし以前のページに居る場合にはそれほど気にならない。
Instant Click
InstantClickを利用することで遷移先のHTMLをmousehover
で取得している。ユーザーがリンクをクリックするのに平均的に300ms以上かかるので、その間にCDNからHTMLを取得することが出来ればユーザーは瞬時にページが遷移したように感じる。
遅延読み込み
外部ファイル
Javascript, ファーストビューに必要のないCSSは全て遅延読み込みしてる(onloadでのappend処理)。画像はブラウザが基本的に遅延読み込みする。
Google Developer Tool - Performance
わかります?HTTP/2.0で流れるようなリクエストとHTMLのレスポンス到着後のfirst printが呼ばれるまでの速さとその後のDOMContentLoadedまでの時間が極端に短い感じ。
まとめ
最初にも書いたけどHerokuでRailsでここまで爆速なサイトを作れることに感銘を受けた。そして調べて見ると真似できそうなアイディアも多いのがまた良い。一部やり過ぎなところもあるけどInsanely Fastはそれぐらいやらないとという感じか。CDNサービスの進化とNative Javascriptの進化によってWebも順当に進化しているんだなと改めて感じた。
今回の件でフロントエンドのパフォーマンスチューニングについてかなり勉強になった。Service WorkerのユースケースやPWA, PRPLデザインパターンなんかは今回の件で知ったので調べて行きたい。
またInstantClickにみんなが驚いている箇所はTurbolinksも同じようなことを目指していて、その差は大雑把に言うとonclick
かonhover
かの違いしかなくて、その差でTurbolinksは効果が薄くデメリットが目立ってしまい嫌われた背景を見ると興味深い。。と思ってたところTurbolinksにもInstantClick化したらどうかというissueが上がっていたのでめっちゃ期待してる。
見逃してる点、間違ってる点、補足などあればお願いします。
あとパフォーマンスチューニングの仕事めっちゃやりたいので案件お待ちしております :)
Twitterやってるのでよかったらフォローしてください🙋♀️ @saboyutaka