Web表示スピード研究会

【事例・Yelp社】First Contentful Paintを45%ほど削減、読み込みを25%、コンバージョン率を15%改善!

UXパフォーマンス向上させ、コンバージョンを高める

ページの読み込み時間が反映されていない限り、誰もが右肩上がりのグラフを好むものです。このブログ記事では、ページロード時間を短縮する方法をご紹介します。Yelp for Business Ownersでは、ビジネスオーナーがリスティングの管理、レビューへの対応、ビジネス情報の編集、ビジネス写真のアップロードなどを行うことができます。また、Yelp広告やプロフィール商品を購入して、地元のオーディエンスをターゲットにし、Yelp上でのビジネスの存在感を高めたりすることもできます。このブログ記事では、ロード時間を大幅に短縮することで、広告購入フローのUXパフォーマンスを向上させた方法をご紹介します。同じ戦術を自分のフローに適用し、うまくいけば私たちと同じような結果を得ることができるでしょう。

背景

当社の中核となる広告購入フローは、PythonベースのバックエンドサービスとGraphQLを搭載した単一ページのReactアプリケーションです。ここ数年で、4ステップのプロセスから7ステップのプロセスへと成長し、より優れた広告キャンペーンコントロールを提供する新機能が追加されました。しかし、機能を追加するにつれ、パフォーマンスが低下しました。ページロードの75%タイル値でのタイミングは、デスクトップユーザーの場合、3秒から6秒に増加しました。この速度低下は、ネットワークの速度や信頼性の制約が増えたことにより、モバイルユーザーではさらに顕著になりました。

ページの読み込みを高速化することは、ユーザーのコンバージョンに直接役立つことは周知の事実です。私たちは、パフォーマンスの高速化が収益にどれだけ影響するかを測定したいと考え、パフォーマンスとユーザーのコンバージョンの関係を測定するために、軽量の実験を行いました。バックエンドを最適化してページロード時間を1秒短縮したところ、すぐにコンバージョン率が相対的に12%上昇しました。この初期段階での勝利は、製品チームからの全面的な賛同とサポートとともに、将来の投資に対する自信を与えてくれました。

パフォーマンス指標

パフォーマンス向上のための最初のステップは、すべてのフローにわたってメトリクスとロギングを標準化するためのフレームワークを設定することでした。私たちは、2つの特定の指標をターゲットにすることに決めました。

First Contentful Paint (FCP)

FCPとは、ページロードリクエストを送信した後、画像やテキストをレンダリングに費やされるブラウザの時間です。FCPは、Webページのパフォーマンスを測る重要な指標として業界で広く認められています。FCPをターゲットにすることは、ページがロードされ始めていることをユーザーに示すために重要でした。実験の結果、ユーザーがページをロードしている間にサイトを離れる可能性は、コンテンツを見た後よりもはるかに高いことがわかりました。ページロードイベントは複数のシステム(Webブラウザ、ルーティング層、認証プロキシなど)に依存するため、さらにFCPを以下の単位に細分化して、作業の分類に役立てています。

  1. Redirect time(リダイレクト時間):ブラウザがリダイレクト(HTTP 303)に要した時間。
  2. Request time(リクエスト時間):Yelp のサーバー内でのメインリクエストに対して、リクエストとレスポンスのサイクルにかかった時間。
  3. Rendering time(レンダリング時間):最初のレスポンスを受け取ってから、ブラウザが最初の contentful paint をレンダリングするのにかかった時間。

下の図は、上記単位でのタイミングの内訳を示しています。

Time to Interactive (TTI)

この指標は、ブラウザがページを完全に読み込むのに要した時間を測定します。完全なユーザーエクスペリエンスを実現するために必要な、クライアント側のレンダリングロジックと非同期データフェッチをキャプチャします。Yelpでは、この指標をYelp Page Complete(YPC)と呼んでいます。当社のアプリケーションの多くは、最初のページロード後に画面が描画され、ページ内容が表示されており、各コンポーネントがデータを取得するため、TTIをキャプチャすることが重要です。TTIは、ユーザーエクスペリエンス全体のタイミングを把握するのに役立ちます。

biz siteには、独自のデータ・フェッチ戦略を持つ同様のフローが他にもいくつかあります。すべてのフローの統合を容易にするために、共有のJavaScriptパッケージを開発し、ロギング、ポリフィル、ロギング関連のAJAXコールのバッチ処理/スロットリングなどに関連するすべてのロジックを統合しました。最終的には、すべてのパフォーマンス指標のロギングを開始するために、数行を追加するだけで統合が完了しました。

パフォーマンス改善ツールについて

私たちの取り組みに不可欠な多くのツールを利用しましたが、ここではそれを紹介します。

Zipkin

OpenZipkinは、Yelpに設置されたオープンソースの分散型トレーシングシステムです。このツールは、Yelpのサーバー内でのリクエスト・ライフサイクルのボトルネックの特定に役立ちました。我々のリクエストは複数のサービスを経由しており、バックエンドでの潜在的な最適化を特定するのに不可欠でした。次に、Zipkinトレースの例を示します。

Webpack Bundle Analyzer

Webpack Bundle Analyzer は、JavaScript バンドルのコンテンツを、ズーム可能なインタラクティブなツリーマップで視覚化してくれます。このツールは、後述するフロントエンドのアセットの最適化を特定するために重要でした。以下は、このプラグインのGithubリポジトリにあるツリーマップのサンプルです。

Source: https://github.com/webpack-contrib/webpack-bundle-analyzer

Splunk

すべてのパフォーマンス指標を Redshift データベースのテーブルに取り込み、Splunk ダッシュボードとして可視化しました。これにより、変更をデプロイしながらリアルタイムで進捗状況を把握することができました。ダッシュボードの例を次に示します。

Chrome DevTools

Chrome のツールは、フロントエンドのパフォーマンス問題について素晴らしい洞察を与えてくれました。具体的には「パフォーマンス」タブのフレームグラフを利用して、ブラウザのメインスレッドがどこでブロックされているか、アセットの読み込み、解析、評価にどれだけの時間がかかっているかを確認しました。また、Google Lighthouseは実用的な機会と診断情報を提供してくれました。

さらに改善を追求

収集したメトリックから学んだ後、バックエンド、フロントエンド、インフラストラクチャのすべてのフロントエンドでパフォーマンスの問題に取り組むことを計画しました。以下、いくつかのポイントをご紹介します。

フロントエンドの最適化

ここ数年の継続的な機能追加により、当社の JavaScript バンドルは徐々に増加していました。Yelpの社内ツールは、gzip圧縮、バンドルの縮小、デッドコードの排除などの一般的なベストプラクティスをすでに実施していたので、ほとんどの問題はアプリケーションの設定にありました。上記のツールを使ってバンドルを分析した後、以下のような様々なテクニックを駆使して、gzip圧縮されたバンドルのサイズを576KBから312KBに約50%削減しました。

コード分割

最初のページロード時に、購入フローの7ページすべてにコードを提供することは、間違いなく無駄でした。ロード可能なコンポーネントを使用して、必要に応じてロードされるステップごとに別々のチャンクを作成することにしました。このチャンク化により、バンドルサイズを15%削減することができました。しかし、これらのアセットをオンデマンドでロードすると、ページをロードするたびにわずかな遅延が発生します。そこで、有用なrequestIdleCallback関数を使用してすべてのチャンクをプリロードするヘルパー関数を作成し、UX動作の変更を回避しました。

Tree Shaking

最近のYelp の Webpack デフォルト設定では、デッドコードの排除が可能になっています。バンドルのツリーマップを見てみると、古いパッケージの中には古いビルド設定を使用しているものがあり、Tree Shakingが機能しないことがわかりました。そこで、そのようなパッケージをすべて洗い出し、ビルドをアップグレードするだけで、バンドルサイズをさらに30%削減することができました。

パッケージを大量のフットプリントで置き換える

コードの中で使用頻度が低く、バンドルに占める割合が大きいパッケージがいくつかありました。その代表例がmoment.jsで、2回しか使われていないのにバンドルの5%を占めていました。これを tree-shakeable の date-fns で置き換えることができました。面白いことに、momentjsのプロジェクトステータス自体が代替品の使用を推奨しています。

パッケージの重複除外

当社では依存関係管理にYarnを使用していますが、(Yarn V2以前は)重複する範囲のパッケージを重複除外していませんでした。大規模なアプリケーションの場合、重複除外がバンドルサイズに顕著な影響を与えていました。Yarn V2は、この問題を解決してくれました。

コンポーネントの再レンダリングの削減

Reactプロファイラは、ナビゲーションバーなどの特定のコアページコンポーネントが、ページロード中に無駄な再レンダリングを行っていることを特定しました。この再レンダリングによりメインスレッドがブロックされ、FCPを遅らせる原因となっていたため、これらのコンポーネントの上に処理の最適化ロジックを追加することで解決しました。

サーバーサイドの最適化

Yelpでは、サービスアーキテクチャの拡大に伴い、興味深い障害が発生していました。リクエストが複数のサービス(モノリスを含む)を経由すると、そのライフサイクルは複雑になります。例えば、ページロードのリクエストは3つのサービスを経由し、データを取得するために最大で5つのダウンストリームサービスに依存していました。以下のような取り組みにより、リクエストのタイムアウトを短縮することができました。

プロキシ・レイヤーの削除

biz siteのリクエストは、Yelpのモノリスを介してプロキシされていました。ログインしたビジネスオーナーの認証と許可を処理するためです。このプロキシは高価でした。今年の初めに、認証と認可ビジネスロジックを再利用可能なPythonパッケージにまとめました。今回の最適化では、このパッケージと統合し、ルーティング層から直接トラフィックを受け入れるようにサービスを設定し、ダーク・ローンチを介して慎重に展開することが必要でした。レガシーコードを排除することで、リクエスト時間を250ms短縮することができました。

ネットワークコールの並列化

ページロード中にデータをフェッチするために、いくつかのダウンストリームサービスに頼っています。Zipkinは、ネットワークコールのいくつかがブロック方式でレイアウトされており、それがリクエスト全体を遅くしていたことがわかりました。Yelpでは、Bravadoで構築されたFuturesを使用しており、ネットワークリクエストを同時に送信できます。ビジネスロジックの最上位ですべてのネットワーク要求を処理するように要求コードを書き直し、コードの後半で新しいネットワーク要求を開始することを避けました。これにより、リクエストのタイミングを300ms短縮することができました。この問題は解消される可能性がありますが、この動作のベストプラクティスを文書化して、将来の問題の防止に役立てています。

リダイレクトの排除

レガシーページ、古いフロー、サードパーティのブログ記事などが、ユーザーが最終的なURL/ページに到達する前にリダイレクトを発生させていました。これらのリダイレクトは、モバイルトラフィックの場合、数秒かかることもありました。HTTP Refererヘッダーを使ってすべてのリダイレクトを記録し、それに基づいて対処しました。

サーバーサイドレンダリング

今回の取り組み以前、フローは完全にクライアントサイドでレンダリングされていました。つまり、リクエストのレスポンスでHTMLを一切送信していませんでした。JavaScriptのバンドルを送っただけで、ページのコンテンツを提供するためのHTMLの生成はブラウザとReactアプリに完全に依存していました。これは、特にCPUやメモリが限られているモバイルクライアントでは、FCPに悪影響を及ぼすことがわかりました。Yelpでは、Hypernovaをベースにした(React)コンポーネントレンダリングサービスがすでに設定されていました。そのサービスと統合し、サーバから最初のページのマークアップのレンダリングを開始しました。すぐに、すべてのクライアントに大きなメリットがありました。下のグラフにあるように、レンダリングの負荷をサーバーに移しましたが、レンダリング時間は急激に短縮され、FCPの時間も短縮されました。また、読み込み時の揺らぎもなくなりました。

Pre-warming Cache

リクエストの中には、ディスクから構成を読み込んで作成するカテゴリーツリーオブジェクトの構築など、計算量の多いタスクがいくつかあります。これらのオブジェクトはメモリ上にキャッシュされていますが、レイテンシーの高いP90リクエストでは、常にキャッシュミスが発生するため、まだ問題が残っていました。そこで、すべてのキャッシュをウォームアップし、高価なキャッシュ可能なオブジェクトを作成することを唯一の役割とする内部エンドポイントを作成しました。ワーカーが作成されるたびに呼び出される uWSGI フックを使用して、この内部エンドポイントを呼び出しました。これにより、すべてのクライアントで95%タイル値を約2秒短縮することができました。

Vertical Scaling

最後に、私たちのサービスとそれに依存するサービスを、パフォーマンスの高いAWSのz1d.6xlarge EC2インスタンスにデプロイしてみました。その結果、ページロード時間ではわずかな改善(最大100msec)が見られましたが、他の計算負荷の高いAJAX APIではより大きな改善が見られました。例えば、製品を購入を担当するPOSTエンドポイントは20%高速化され、タイムアウトの減少につながりました。

最終結果

専門のエンジニアリングチームが4ヶ月間集中して取り組んだ結果、この投資に見合うだけの成果を得ることができました。これは、当社のコンバージョン指標だけでなく、ページの読み込みが大幅に速くなったことで、お客様にとってもメリットがありました。

重要なのは、広告購入フローで達成した成果です。
・75%タイル値のFCPを3.25秒から1.80秒に短縮し、45%改善しました。
・75%タイル値のYPCを4.31秒から3.21秒に短縮し、25%の改善を実現しました。
・コンバージョン率が最大で15%向上しました。

以下は、時系列での進捗状況を示すグラフです。

謝辞

・このプロジェクトのチームメイトには感謝しています。Thibault Ravera氏、Bobby Roeder氏、Frank She氏、Austin Tai氏、Yang Wang氏、Matt Wen氏。

・テクニカルレビューと編集を担当したDennis Coldwell氏、Aaron Gurin氏、Blake Larkin氏、Alex Levy氏に感謝します。

Shubham Gupta, Software Engineer

※この記事は”https://engineeringblog.yelp.com/2021/01/boosting-user-conversion-with-ux-performance-wins.html” の英文情報を元に、内容を分かりやすく編集、翻訳した記事です。

PSI、表示スピード、コアウエブバイタル INPの各対策については