Next.jsのSSRにおいてコンポーネントローディング中にスケルトンを表示する

公開日: 

スケルトンとは

Next.jsSSR を利用してコンポーネントを表示する場合は、dynamic インポート時にssr: falseのオプションをつけることで、ssr せずにクライアントでコンポーネントをレンダリングするように設定することができます。

https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr

僕がインターン先で開発しているGROWIという OSS の wiki サービスでは現在 Next.js の導入が進んでおり、

  • wiki 本文のようなすぐに表示されてほしいコンポーネントはサーバーサイドでレンダリング
  • その他のすぐに表示される必要のないコンポーネントはクライアントサイドでレンダリング

このような方針で Next.js の導入が進められています。

上の gif 画像が SSR されるコンポーネントの例です。家のアイコンがあるページのパス部分はすぐに表示されてほしいのでサーバーサイドでレンダリングされていますが、その他のボタンなどは後からクライアントサイドでレンダリングが走っています。

このようにレンダリングを出し分けると、表示タイミングの差によって画面にカクつきが出てしまいます。これを防ぐのがスケルトンです。

実装したいもの

上の gif 画像のようにクライアントサイドでのレンダリングが走るまでの時間、本物のコンポーネントと同じ高さと幅を持ったスケルトンコンポーネントを表示します。 こうすることでコンポーネントがカクっと表示されるのを防ぎ、レンダリング待機中の UX 向上を目指します。

実装方法

スケルトン

Skelton.tsx
import React from 'react';

type SkeltonProps = {
  additionalClass?: string;
  roundedPill?: boolean;
};

export const Skelton = (props: SkeltonProps): JSX.Element => {
  const { additionalClass, roundedPill } = props;

  return (
    <div className={`${additionalClass}`}>
      <div
        className={`grw-skelton h-100 w-100 ${
          roundedPill ? 'rounded-pill' : ''
        }`}
      ></div>
    </div>
  );
};

このようなスケルトン用の React コンポーネントを作成します。

  • 高さと幅は css で指定するために、クラスを外から指定できるようにする
  • round-pill を props に持つことで角丸の有無を利用側が指定できる
  • 本物のコンポーネントよりも細いスケルトンを実装したいことを考慮して、div タグの 2 層構造にする

上の三つのことを意識してコンポーネントを作成しました。詳細については後術しますが、grw-skeltonというクラスはスケルトンの背景色を指定するためだけのクラスなので特に気にしなくても大丈夫です。

スケルトンのスタイル

スケルトンのスタイルは出来るだけ、高さと幅を数値でハードコーディングするのはやめて、本物のコンポーネントと同じ高さを利用するようにしました。

TagLabels.module.scss
@use '~/styles/bootstrap/init' as bs;

$grw-tag-label-font-size: 12px;

.grw-tag-labels :global {
  .grw-tag-label {
    font-size: $grw-tag-label-font-size;
    font-weight: normal;
    border-radius: bs.$border-radius;
  }
}

.grw-tag-labels-skelton :global {
  width: 137px;
  height: calc(#{$grw-tag-label-font-size} + #{bs.$badge-padding-y} * 2);
  font-size: $grw-tag-label-font-size; // set font-size to use the same em value in bs.$badge-padding-y(https://getbootstrap.jp/docs/5.0/components/badge/#variables)
}

上はスタイルの実装の一例です。上のgrw-tag-labelsというのが本物のコンポーネントが持つクラスで、下のgrw-tag-labels-skeltonというのがスケルトンコンポーネントが持つクラスです。

  • 高さのハードコーディングはしない
  • 本物のコンポーネントのスタイルと同一ファイルにまとめる

この 2 点を意識することで、本物のコンポーネントのスタイルが変更された時に、スケルトンのコンポーネントのスタイルも変更しやすいようになっています。また、UI がカクつく原因になるのはコンポーネントの高さなので、幅に関してはよしなにスタイルを当てています。

ローディング中にスケルトンを表示する

いよいよ、スケルトンコンポーネントをローディング中に表示する部分を実装していきます。Next.js が用意している、dynamic インポートのloadingオプションを利用します。

https://nextjs.org/docs/advanced-features/dynamic-import#example

hoge.jsx
const TagLabels = dynamic(() => import('../Page/TagLabels'), {
  ssr: false, // ssrしない
  loading: () => (
    <Skelton
      additionalClass={`${TagLabelsStyles['grw-tag-labels-skelton']} py-1`}
    />
  ), // クライアントサイドでレンダリング中に表示するコンポーネント
});

上のように loading オプションにスケルトンコンポーネントを渡すことで、レンダリング中のスケルトン機能を実装します。この例では、grw-tag-labels-skeltonクラスによって高さと幅は決まっています。また、デザイン的に少し細めのスケルトンにしたかったのでpy-1クラスを付与することで bootstrap で上下に padding を追加しています。

この画像のようなイメージです。このようなことを実現するために、先ほど書いたように、スケルトンコンポーネントを div タグの 2 重構造にしています。

最後に

本物のコンポーネントと高さを揃えたりと実装に割と苦戦した節はありますが、やはりスケルトンがあるとないでは UX が違うような気がします。割と応用が効くコンポーネントだと思うので、よかったら活用してみてください。

では、Bye