🐈

React SuspenseによるWeb Vitalsの改善

2022/03/05に公開

React SuspenseによるWeb Vitalsの改善

はじめに

  • こんにちは。普段はReact×Typescriptで開発をしているmzdです。
  • 今回は優れたUXを提供するためには、知っておきたいWeb Vitalsという指標とReact Suspenseについて話していきます。
  • React Suspenseを駆使することで、Web Vitalsの改善にどのように良い影響があるのかについて書きたいと思います。

目次

  • Web Vitalsとは
  • LCP/FMP
  • React Suspense(Render-as-You-Fetch)
    • React Suspenseとは
    • Fetch-on-RenderRender-as-You-Fetchの比較
      • useEffectを使ったFetch-on-Renderの実装例
      • React Suspenseを使ったRender-as-You-Fetchの実装例
    • React Suspenseのメリット
  • [結論]React Suspenseを使うことでFMP・LCPが改善される

Web Vitalsとは

  • Web 上で優れたUXを提供する上で欠かすことのできないとされる指標のことで、Googleが2020年5月に発表した指標です。
  • 既にGoogleはSEOによる検索結果の順位にWeb Vitalsを用いており、Web上でコンテンツを提供している人にとってWeb Vitalsの改善は必須かもしれません。
  • 今回は、そのWeb Vitalsの中のLCPとFMPReact Suspenseによって改善できるという話を書きたいと思います。
  • 中でもLCPCore Web Vitalsと言われている指標であり、Web Vitalsの中でも特に重要な3つの指標のうちの一つです。
  • その3つの指標というのは「LCP(Largest Contentful Paint)」「FID(First Input Delay)」「CLS(Cumulative Layout Shift)」の3つを指します。詳しくはこちらをご参照ください。

LCP/FMP

次に今回の説明にあたって必要となるWeb Vitalsの中のLCP/FMPの2つの指標について説明していきます。既に知っているという方はスキップしてもらっても大丈夫です。

LCP(Largest Contentful Paint)

LCPとはページ内のメインコンテンツ(最も大きな動画や画像、コンテンツブロック)の読み込みにかかるまでの時間を表す指標を指します。

具体例を見ていきます。

  • 下記の画像は左から右へ時間経過によってコンテンツの読み込みが進んでいく様子が表されています。緑色になっている箇所がページ内のメインコンテンツを表しており、LCPの対象となっているコンテンツです。
  • この画像を見ればわかる通り、読み込みが進むにつれて、メインコンテンツが変わっていきます。(初めはヘッダーのタイトルだったが読み込み完了時は画像になっている)
  • 今回の例で言うと最終的なメインコンテンツである画像が読み込まれるまでの時間 = 最大視覚コンテンツの表示時間 = LCPとなります。
  • どのような条件でメインとなるコンテンツが選ばれ、変更されるのかについての詳しい説明は省きますので、詳細はこちらをご覧ください。

  • LCPは結果によって下記の3つの評価に分けられます。
  • 優れたUXのためには2.5秒以下のGoodを目指すべきとされています。
2.5秒以下 → Good(良好)

2.5秒から4.0以下 → Needs Important(改善が必要)

4.0秒以上 Poor(不良)

FMP(First Meaningful Paint)

FMPとはページ内にユーザーにとって「意味のあるコンテンツ」が表示されるまでの時間を表す指標です。

  • 「意味のあるコンテンツ」にはローディングなどの読み込み中のコンテンツは含まず、読み込み完了後のコンテンツのことを指しています。
  • また初めに表示された「意味のあるコンテンツ」が、LCP対象の「メインコンテンツ」と同一の場合は、FMP = LCPとなります。
  • FMPは結果によって下記の3つの評価に分けられます。
  • 優れたUXのためには2秒以下のGoodを目指すべきとされています。
2秒以下 → Good(良好)

2秒から4以下 → Needs Important(改善が必要)

4秒以上 Poor(不良)

(他にも指標が沢山あるので詳しく知りたい方はこちらをご参照ください)

ここまでWeb Vitalsについて説明してきましたが、ここから実際にReact Suspenseを駆使することで、なぜWeb Vitalsが改善されるかについて見ていきます。

React Suspense(Render-as-You-Fetch)

React Suspenseとは

データを取得中であり、コンポーネントが呼び出そうとしているデータが準備中であることをReactに伝え、代わりにローディング中のコンポーネントを表示するための仕組み」です。

それは、データの取得が完了するまでレンダリングしないのではなく、レンダリングした上でデータが必要な箇所に来た時にSuspense状態になります。

このようにデータをfetchしながらrenderするレンダリングパターンをRender-as-You-Fetchと言います。

この後、React Suspenseについて説明していきますが、React Suspenseを使用したRender-as-You-Fetchが「どのような実装になるか」や「どのようなレンダリングの流れになるか」をお話する前に、useEffectを使った従来のFetch-on-Renderという別のレンダリングパターンについて見ていき、それらを比較していきたいと思います。

Fetch-on-RenderとRender-as-You-Fetchの比較

useEffectを使ったFetch-on-Renderの実装例

import { useEffect, useState } from "react"

export const ProfilePage = () => {
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetchUser().then(u => setUser(u))
  }, [])

  if (!user) return <p>Loading profile...</p>

  return (
    <>
      <h1>{user.name}</h1>
      <ProfileTimeline />
    </>
  );
}

const ProfileTimeline = () => {
  const [posts, setPosts] = useState(null)

  useEffect(() => {
    fetchPosts().then(p => setPosts(p))
  }, []);

  if (!posts) return <h2>Loading posts...</h2>

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

Fetch-on-Renderにおいて、ProfilePageが呼ばれてからレンダリングが完了するまでの流れは下記のようになります。

  1. ProfilePageコンポーネントが呼び出される。
  2. userのデータが取得できていない場合「Loading profile...」がレンダリングされる。
  3. 初回のレンダリングが完了したので、useEffectが実行されuserの取得が始まる。
  4. userの取得が完了したらuser.name<ProfileTimeline />がレンダリングされる。
  5. ProfileTimelineが呼び出される。
  6. postsのデータが取得できていない場合「Loading posts...」がレンダリングされる。
  7. 初回のレンダリングが完了したので、useEffectが実行されpostsの取得が始まる。
  8. postsの取得が完了したらpostsのtext一覧がレンダリングされる。

このようにコンポーネントがrenderされたfetchが始まる、このレンダリングパターンをFetch-on-Renderと言います。

このFetch-on-Renderの欠点は、親コンポーネント(今回だとProfilePage)のレンダリングが完了するまで、子コンポーネント(今回だとProfileTimeline)が呼び出されないことです。そのため上記のレンダリングの流れ3のuserのデータ取得が遅くなる分だけ、ProfileTimelineが呼び出されないのでpostsのデータ取得開始も同時に遅くなってしまいます。

React Suspenseを使ったRender-as-You-Fetchの実装例

export const ProfilePage = () => {
    const resource = fetchProfileData();
    
  return (
    // Suspenseで囲んだコンポーネントでデータ取得待ち(promiss)がある場合にfallbackに指定したコンポーネントが代わりに表示されます。
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

const ProfileDetails = () => {
  const user = resource.user.read();

  return <h1>{user.name}</h1>;
}

const ProfileTimeline = () => {
  const posts = resource.posts.read();

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

React Suspense(Render-as-You-Fetch)において、ProfilePageが呼ばれてからレンダリングが完了するまでの流れは下記のようになります。

  1. ProfilePageが呼ばれprofileData(userとposts)のデータ取得が始まる。
  2. ProfileDetailsProfileTimeline呼ばれる。
  3. userの取得が完了していない場合はSuspense状態になり「Loading profile...」がレンダリングされる。
  4. postsの取得が完了していない場合はSuspense状態になり「Loading posts...」がレンダリングされる。
  5. userの取得が完了したらProfileDetailsuserがレンダリングされる。
  6. postsの取得が完了したらProfileTimelinepostがレンダリングされる。

useEffectを使ったFetch-on-Renderでは、レンダリングされるまでuserやpostsの取得を始められませんでしたが、React Suspenseを使ったRender-as-You-Fetchではレンダリングが始まる前に、まずuserやpostsの取得が始まっています。

React Suspenseのメリット

以上のことをまとめるとReact Suspenseには、次のようなメリットがあると考えられます。

  1. データの取得開始が速くなることでレンダリングが速くなりUXが向上させられる。
  2. if (!user) return <h2>Loading posts...</h2>というようなコードが減るのでコンポーネントが宣言的になる。(if (error) return <h2>エラー</h2>のようなエラーハンドリングもerrorBoundaryを使えば記述不要になるので更にすっきりする)

[結論]React Suspenseを使うことでFMP・LCPが改善される

FMPは、ユーザーにとって「意味のあるコンテンツ」が表示されるまでの時間の指標でした。
メリット1のようにデータの取得が速くなることで「意味のあるコンテンツ」の表示も速くなるためFMPを向上させることが出来ます。

LCPは、メインコンテンツ(最も大きな動画や画像、コンテンツブロック)の読み込みにかかるまでの時間の指標でした。
これも同様にデータ取得が速くなれば、動画や画像などのメインコンテンツの読み込みも速くなるためLCPを向上させることが出来ます。

おわりに

ここまで読んでいただきありがとうございました。
React Suspenseの使い方は、なんとなくわかるけど、どういうメリットがあるの?という風に自分自身が理解できていなかったので、今回の記事を書くにあたって調査をすることで理解を深めることができました。
この記事を通して皆さんのReact Suspenseの理解の助けに少しでもなればと思います。

参考にさせていただいた記事

Discussion