こんにちは。Yahoo!ニュース を担当しているエンジニアの喜楽です。
今回は、Yahoo!ニュースが取り組んでいるブラウザバック時の表示最適化手法について紹介します。
なぜブラウザバック時の挙動に注目するのか
ユーザーがYahoo!ニュースのページを閲覧し、別のページに遷移する方法は大きく分けて以下の2つが考えられます。
- (A) リンクをたどってページを遷移する
- (B) ブラウザーのナビゲーションボタンまたはスワイプ操作によって遷移する
- 「戻る」による遷移(ブラウザバック)
- 「進む」による遷移(ブラウザフォワード)
Yahoo!ニュースでは総PVのうち一定程度が(B)のブラウザバックまたはブラウザフォワードによるページ遷移時のものであることがわかっており、そのようなページ遷移時の体験の最適化はインパクトの大きい施策となりえます。
ブラウザバックとブラウザフォワードの内訳は計測できていませんが、これはブラウザバックのほうが多いと考えています。また、本記事で紹介する事例は特にブラウザバックを意識した改善施策であるため、以降ではブラウザフォワードについて特に触れません。
「(B): ブラウザバックによりページを遷移」 が、「(A): リンクをたどってページ遷移」 と大きく異なるのは、(B)は一度表示したページであるということです。
そのためブラウザバックで再度ページを表示するときに、提示するコンテンツを最適化したり、表示速度を改善したりすることが考えられます。
ユーザーの行動を理解する
まず、ブラウザバック時の挙動を改善しようとしたときに、ユーザーがどのような行動をYahoo!ニュース上で行っているかを理解することが重要です。
以下はブラウザバックを含むユーザー行動の代表的なものを表したものです。
上図の例ではユーザーは以下の行動をしています。
- 記事1を見る
- 記事1の本文下の記事リストで次に閲覧する記事を探す
- 記事2を見る
- ブラウザバックで記事1の本文下の記事リストに戻る
つまり、記事のリストを見るためにブラウザバックをしていると考えられるため、記事のリストに対しての施策が有効そうであることがわかると思います。
記事リストの閲覧を最適にするために行っている、「記事リストのキャッシュ」、「既読記事をリストから削除」、「スクロール位置の復元」、「BFCacheの有効化」という4つの施策を紹介します。
記事リストをキャッシュする
記事下の記事リストはサーバーサイドレンダリングされておらず、クライアントサイドで動的に取得しています。何も対応をしないと、ページを表示するたびに記事リストを取得するAPIリクエストが毎回発生し、APIリクエストの分、表示が遅くなってしまいます。そこで、記事リストを取得したタイミングで、SessionStorageにキャッシュし、ブラウザバックで再度ページを表示した際にはSessionStorageから記事リストを取得するようにしています。
ここで工夫している点は、常にキャッシュから復元するのではなく、ページがリロードされたときはキャッシュを破棄することでAPIリクエストを発生させ最新の状態になるようにしているところです。
以下はperformance.navigation.type
によってリロードかどうかの判定し、SessionStorageに保存されたキャッシュを破棄するコードの例です。
if (
performance.navigation.type === performance.navigation.TYPE_RELOAD
) {
// リロードされた場合はSessionStorageの情報を破棄
sessionStorage.removeItem(SESSION_STORAGE_KEY);
}
ここではページがリロードされたかを判定するのに、performance.navigation.type
を使っていますが、これは非推奨なインターフェースなため、新たに実装する際はperformance.getEntriesByType()
を使うとよいでしょう。
if (performance.getEntriesByType('navigation')[0].type === 'reload') {
// リロード時の処理
}
ただし、IE11やSafariにも対応する必要がある場合は、performance.navigation.type
を使う必要があります。
PerformanceNavigationTiming API | Can I use(外部サイト)
既読記事をリストから削除する
記事リストを表示するときに、既読記事をリストから削除して表示しています。つまり、ブラウザバックで記事リストに戻ったときに、直前に読んでいた記事は記事リストから削除されています。
既読記事の履歴の管理には、LocalStorageを使っており、最新十数件分の既読情報を保持しています。
上記のように既読記事をリストに表示しないパターンと、既読記事でもリストに表示するパターンでABテストしてみたところ、既読記事をリストに表示しないパターンでCTRが+1%以上向上しました。既読記事に再度遷移することはなさそうなこと、また、記事をリストから消した分新たな記事に出会えるようになっているためと考えられます。
スクロール位置を復元する
ブラウザバックをした時に、もともと見ていた部分までスクロール位置が復元されないとユーザーが元の位置まで再度スクロールする必要があり体験が悪くなってしまいます。ブラウザバック時のスクロール位置を復元する挙動はブラウザーによって異なりますが、特にページ内で動的に読み込まれるコンテンツがある場合の挙動には差があります。
ブラウザーがスクロール位置を復元する際、想定しない位置にスクロール位置が復元される場合は、ブラウザーによるスクロール位置の復元を無効化し、スクロール位置を復元する処理を実装することで最適化できます。
Yahoo!ニュースでは検証の結果、Chromeではブラウザーのスクロール位置復元を使い、それ以外のブラウザーではスクロール位置を復元する処理を実装しています。ブラウザー自体が備えるスクロール位置の復元機能を無効化するにはHistory.scrollRestoration
というプロパティーにmanual
を指定します。
scrollRestorationをmanual
にしたときは、ブラウザーが自動でスクロール位置を復元しないため、コード上でスクロールをさせる必要があります。スクロール位置の保存はscroll
イベントハンドラを使い、URLをキーにしてSessionStorageに保存しています。scroll
イベントハンドラ内の処理が重いとパフォーマンスに影響を及ぼすことがあるので、passive: true
を指定したり、lodashのthrottle
を使うなど、工夫をしておくとよいでしょう。
addEventListener(
'scroll',
_.throttle(
() => {
sessionStorage.setItem(`${SESSION_STORAGE_KEY_PREFIX}${location.href}`, JSON.stringify({value: scrollY}));
}, 500),
{ passive: true }
);
ブラウザバックで表示されたときはload
イベントの発火時にスクロール位置の復元をしています。
スクロール位置を復元するコードの例
addEventListener(
'load',
() => {
const sessionStorageKey = `${SESSION_STORAGE_KEY_PREFIX}${location.href}`
const scrollValueItem = sessionStorage.getItem(sessionStorageKey);
const scrollPosition = scrollValueItem ? JSON.parse(scrollValueItem).value : 0;
scrollTo(0, scrollPosition);
sessionStorage.remove(sessionStorageKey);
},
false
);
BFCacheを有効にする
BFCache(Back/Forward Cache)はJavaScriptの実行状態をブラウザーが保持し、ブラウザバックでページを表示したときに、JavaScriptの実行状態も復元する機能です。
BFCacheが有効になるというのはどのようなことか、簡単な例で説明したいと思います。JavaScriptで開閉状態を制御するExpandのUIがあったとします。
このとき、Expandを開いた状態で、別ページに遷移し、ブラウザバックをしたとします。
ブラウザバック時に、BFCacheが有効な場合はExpandが開いた状態で復元されますが、BFCacheが有効ではない場合は初期状態のExpandが閉じた状態で表示されます。
この例ではExpandの開閉状態がBFCacheによって保持・復元されましたが、Expandの開閉状態に限らず、JavaScriptの実行状態を保持・復元できるのがBFCacheです。
BFCacheが有効になると、ブラウザバック時の表示が速くなるだけではなく、前述した、状態をSessionStorageに一時的に保持する対応などが不要になり、実装コスト面でも非常に有益な施策です。ただし、BFCacheは常に有効になるわけではないことには注意しておく必要があります。
SafariやFirefoxではすでにBFCacheをサポートしており、Chromeでもversion96からサポートするため、これにより主要なブラウザーはBFCacheをサポートすることになります。
BFCacheを有効にするためには、まずブラウザーにメインのHTMLリソースをキャッシュさせる必要があります。そのためメインのHTMLのリソース配信時のCache-Controlヘッダを以下のように変更しました。
変更前: ブラウザーにキャッシュされない(Yahoo!ニュースではコンテンツの鮮度やユーザーごとの最適化のためキャッシュしない設定が基本です)
Cache-Control: private, no-cache, no-store, must-revalidate
変更後: ブラウザーに15秒間はキャッシュされるように指定
Cache-Control: private, max-age=15
また、メインのHTMLリソースをブラウザーにキャッシュされる対応に加えていくつか対応が必要になります。詳しくは以下の記事を御覧ください。
Back/forward cache | web.dev(外部サイト)
対応の中でも、unload
イベントハンドラをなくす対応が必要な箇所がありますが、Yahoo!ニュースでは使っているJavaScriptライブラリの中にunload
イベントハンドラを含むものがあり、ChromeやFirefoxではBFCacheが有効にはなりません。
しかし、Safariではunload
イベントハンドラが使われている場合でも、BFCacheが有効になるため、BFCacheがどの程度ユーザー体験を向上せるものなのかを検証するためにCache-Controlヘッダを出し分けるABテストを行いました。
その結果、iOSのSafariではPV/UBが2%以上向上する結果が得られました。BFCacheはブラウザバック時の体験が最適化されるため、PV/UBのような指標もポジティブに動いたのだと推測されます。
BFCacheで注意したいのは、JavaScriptの実行状態が保持されるため、ロギングに影響が出ることがある点です。例えば、BFCacheが無効のときにカウントされていたログがBFCacheが有効になると、ログ送信を行った状態であるというところも含めて復元されるため、必要なログが落ちないということが起こりえます。
Observe when a page is restored from bfcache(外部サイト)
にもあるようにpageshow
イベントのevent.persisted
によりBFCacheより復元されたかをハンドリングできます。
ロガーを複数入れている場合などで、このロジックが点在することを避ける場合はBFCacheLoaded
といった独自のEventを使ってハンドリングするのもよいでしょう。
EventをDispatchする例
addEventListener('pageshow', event => {
if (event.persisted) {
// BFCacheより復元されたとき
dispatchEvent(new Event('BFCacheLoaded'));
}
});
Event Listenerの例
addEventListener('BFCacheLoaded', () => {
// BFCacheより読み込まれたときに、動かしたい処理
});
おわりに
ブラウザバックというと非常に単純な機能という印象がありますが、よく使われるからこそ、ユーザー体験を高める工夫が重要だと感じています。
フロントエンドでの最適化はブラウザーごとに仕様が異なることや、ブラウザーのバージョンアップに随時対応する必要があり、大変なことも多いのですが、ユーザーとの接点を担う分、改善の効果も非常に大きいです。
これまでのフロントエンドでの改善例としては、Core Web VitalsのCLS(Cumulative Layout Shift)指標の改善により、滞在時間やPV数が向上したことがありました。こちらの記事もぜひ合わせてご覧ください。
ユーザー体験を向上!Yahoo!ニュースにおけるCore Web Vitals対応事例
今回はYahoo!ニュースの中での取り組みのうち、ブラウザバックにフォーカスした施策をいくつかを紹介しましたが、今後もさまざまな取り組みを通じて、よりニュースを届けられるように改善を重ねていきたいと思っています。
最後までお読みいただきありがとうございました。
(図: 村田 由香里)
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました