高速化のためにフロントエンドですべきこと
宍戸俊哉氏(以下、宍戸):フロントエンドの話をしていきます。「高速化のためにフロントエンドですべきこと」を4つあげさせてもらいました。「あとでいいことはあとでやる」「必要なことは先にやる」「使い回せるものは使いまわす」「クライアントにとって最適なものを配信する」。だいたいこの辺の原則に則って今のフロントエンドの高速化のプラクティスってあるのかなと思っています。
「あとでいいことはあとでやる」というところで、1つはクリティカルレンダリングパスですね。これを減らしていくというところですね。
クリティカルレンダリングパスはもう説明いらないかなと思うんですけど、CSS、JavaScriptのファイルが読み込みにいくときにレンダーブロッキングを発生させているので、なるべく非同期化したり、インラインしたりしようというところですね。
JavaScriptは基本的に全部非同期で読み込むようにしています。普通にasync/deferをつけてscriptタグから読み込むという方針でおおむねよいと思うんですけど、r.nikkei.comの場合はpolyfill serviceというライブラリを使っています。このライブラリはブラウザ間によってはまだ実装されていないAPIなどの差分を吸収してくれるサービスです。
こいつを使っているのでほかのコードというのは全部polyfillが存在する前提で書いているんですね。なので、polyfillをロードしてからほかのスクリプトを実行するような書き方をしています。DOMContentLoadedは一切使わずに、ほとんどのものがpolyfillのonloadイベントをキャプチャして初期化するようになっています。
CSSはどうかというと、Critical CSSという概念があると思うんですけど、一番最初に画面に映るスタイル以外は全部あとから読み込もうというようなアプローチですね。
これやるとCSSのクリティカルレンダリングパスをなくすことができるんですけど、r.nikkei.comでは、いろいろ試行錯誤はしたんですけど、まだ入っていないです。なんでかというと、会員種別によって画面が違うだとか、レスポンシブ対応が必要だとか、そういった要素が入ってきてまだうまく行ってないですね。
ライブラリとしては自動生成するものが、npmの「critical」というやつと、あとはGoogleのPageSpeed ModuleというのがNginxのプラグインを出していて、そいつが動的にCritical CSSをインラインに埋め込むというようなライブラリがあったりします。
たぶんこれでも、いろいろトライした結果、「最終的にはマニュアルで全部メンテしていく勇気と覚悟が必要そう」という結論に今はいたっています。
必要なことは先にやる
次はセクションの遅延ロードですね。画像は遅延ロードするのはみんなやっているかなと思うんですけど、r.nikkei.comだとセクションごとに遅延ロードを行ったりしています。
「コンテンツのサイズ」というのはDOMノードの大きさとか深さですね。これはパフォーマンスにすごい大きく影響してきて、モバイルだとさらに顕著だったりします。
今のPCの日経電子版ってトップページだけだと60記事以上リンクがあって、ビジネスサイド的には全部出してほしいというような要望があったので対応しました。最上部除いてセクションごと全部遅延読み込みさせるような仕組みで、初回レンダリングのコストを下げる工夫をしています。
セクションのLazy Loadingをやるために「スケルトンスクリーン」というのを実装していました。これはなにかというと、Intersection Observerを2回に分けて非同期で読み込むような処理ですね。
最初に長めのthresholdでIntersection ObserverでスケルトンだけのセクションをDOMに追加します。そのあとスクロールが進んできたら実体で入れ替える。
そうすることで、このIntersection Observerの長めの1回目の読み込みでこのスケルトンスクリーンに出しているDOMサイズを減らす処理をしています。これによってリフローもあんまり起きないかなという感じですね。
それと「必要なことは先にやる」というところで、HTTP/2のServer Pushですね。画面の表示に必要になるリソースというのはなるべく速く読み込むようにする必要があって、これもFastlyのHTTP/2の機能を有効化して実現しています。
Curlでちょっと見づらいんですけど、Curlでr.nikkei.comのトップページを叩いてみると、中は死ぬほどLinkヘッダが入っているんですね。
効率最適化できているかというと自信ないんですけど、トップページの表示に必要になるリソースを基本的にリンクヘッダに一緒につけてあげて、ブラウザのPush Cacheの中に入れておく。そうすると、index.htmlを読み込んだあとに、例えばCSS取りに行ったりJS取りに行ったりでネットワークリクエストを発生させずに返すことができる仕組みになっています。
キャッシュの対策として行っていること
次はResource Hintsですね。日経電子版だと、朝夕刊といってその日の紙の新聞とまったく同じ構成の記事を表示するページがあります。
ほとんどユースケースとしては、毎日紙の新聞を読む人が1面からなんとか面までずっとガーって読んでいくのと同じように、各面をバッと舐めるように閲覧するユーザーが多いんですね。次のページはResource Hintsでキャッシュしてしまおうという処理ですね。これはprefetchというものを使っています。
prerenderとdns-prefetchというのも使っていて。prerenderは検索への導線ですね。マウスカーソルがアイコンに近づいたらリンクタグを動的に挿入します。あとは広告のサードパーティドメインに対してdns-prefetchをするなどして先読みを行っています。
あとはService Workerを使った事前キャッシュですね。トップページやその画面のページからPostMessageを投げて、Service Workerの中でAPIから記事の一覧を取りに行って、その返ってきた記事の一覧から実際に記事のオブジェクトを取り出すというような処理ですね。その返ってきたオブジェクトをService WorkerのCache APIの中に入れてあげる感じですね。
だいたいこれを使うと記事のページというのは20〜30msぐらいで返せるようになっています。あとはオフラインで使えるようになるなどいろいろな恩恵があります。
サブリソースは、先ほどのService Workerのレスポンスからだと記事のページのレスポンスは取れるんですけど、必要になるJSとかCSSとかそういうサブリソースが取れないんですね。
それはService Workerの中でレスポンスヘッダをパースしてあげて必要なリンク、必要なサブリソースのURL一覧を取ってきて、それをService WorkerのaddAllを使ってキャッシュすることで実現しています。
キャッシュ同期の問題というのがありまして、ログインしたとき、あとは非ログインのとき、古いキャッシュがどうしても残ってしまうというのがあります。有料会員のはずなのに「この記事は有料会員限定です」という画面が表示されてしまいました。
古いキャッシュがストレージに残っているというのも問題というところで、それにどう対応してるかというと、ログイン・ログアウトのURLをService Worker内でキャプチャして対応しています。そのログイン・ログアウトのURLをキャプチャして該当のURLが来たらキャッシュ削除する処理ですね。
もう1つはpostMessageですね。Service Worker内、Service Workerから返されたレスポンスに含まれるユーザーの会員属性、それとCookie内に保持している最新の会員属性。それが違えばクライアントからService Workerにメッセージを投げてキャッシュ削除するという形ですね。
古いキャッシュの削除どうやってやっているかというと、キャッシュしたURLとタイムスタンプをIndexedDBに入れています。そのIndexedDBに入れたデータとCache Storage内のリソースを同期させることで実現しています。
だいたいこんな感じですね。
BackgroundSyncを使ってService Workerにキャッシュ要求を投げて、Service Workerは新しいキャッシュ要求を行う前にIndexedDBを参照して、そして古いキャッシュが残っていればそれを削除するといった処理ですね。
「使いまわせるものは使いまわす」は一般的な話なんですけど、静的ファイルにはハッシュ値をつけて長時間キャッシュするようにしています。リロードの対策としてimmutableをつけてあげると、リロード時もキャッシュの再読み込みがなくてよいなという感じですね。
サイト高速化の基本はUX改善
デザインシステムですね。UIコンポーネント。「NIKKEI UI」というコンポーネントベースのスタイルガイドを作成していて、それをr.nikkei.comでは使うようにしています。
どういうメリットがあるかというと、マークアップの開発コスト削減やスタイル定義が重複しないように開発ができます。UIに一貫性が出ることと、アクセシビリティ、このあたりを担保できます。
いろいろキャッシュの話をしてきたかなと思うんですけど、あなたのキャッシュはどこから? 私は鼻から……。あ、これ言いたかっただけです(笑)。
(会場笑)
どこからキャッシュしたのかけっこうわからなくなるんですけど、これは現状アクセスログ、Chrome Dev ToolsのNetworkパネルのInitiator、あとはsizeの部分から判断するしかないかなと思っています。あとはApplicationタグを使うとService Workerの設定でBypass for networkというチェック項目があるので、それを使ってService Workerを無効化する。
このあたりはサイト高速化の教科書とも言っていい『超速! Webページ速度改善ガイド』、これを読むといいかなと思っています。
「最適なリソースを配信する」というところでpictureタグですね。pictureタグを使ってクライアントの画面幅に合わせて最適な大きさの画像を設定しています。
これはHandlebarsのヘルパーを使って実装しています。
これつい先週のやつなんですけど、画像配信を少し見直したんですね。今までjpeg画像のクオリティはけっこう高めに設定してたんですけど、見た目に大きな変更のない範囲で、見た目に大きな劣化のない範囲で変更して、だいたいコンテンツのサイズが半分ぐらい改善することができました。
Performance Budgetを定義しておくと、定期的にBudgetオーバーしてるから改善しなきゃねというきっかけになるので便利です。
「Network Information APIが使えるよ」というところで、通信の種別環境の種類を取得できるAPIなので、Wi-Fiかcellularなのか、3Gなのか4Gなのか、Round-Trip Timeなのか、こういったものが取得可能になります。
Service Workerで大きめなリソースをガッとキャッシュすることがあるんですけど、そういうのはWi-Fi接続時のみに限定したりしています。
最後まとめになります。高速化は一番根本的なUX改善になるのかなと思っています。
まずは分析です。Lighthouseを使ったりwebpagetestを使って分析をしていきましょう。SpeedCurveなどを使って継続的にモニタリングできる仕組みを作ると、一気にパフォーマンス改善というのはやりやすくなります。フロンドエンドだけじゃなくてCDNやサーバサイドのパフォーマンスというのも重要になっています。
あとはキャッシュの効率化、クリティカルレンダリングパスの削減、そういう手がつけられるわかりやすいところから始めるといいのかなと思ってます。さっきのこの画像と同じように、パフォーマンスの改善というのは定期的にやっていくとよいです。そのときにBudgetを定義しておくとやりやすいです。
速くすることを考えるよりも、遅くなる要素を減らしていくことが一番大事かなと思っています。
以上です。ありがとうございました。
(会場拍手)