🚄

Next.js&SSGで作成したHPのロード速度をGoogle PageSpeed Insightsで95点以上にするための方法

2023/12/07に公開

TL;DR

長年放置してい個人たサイトを Googleの PageSpeed Insights で90点超えを目指して改良し続けた結果、動画やiframeや自動翻訳などのそれなりにリッチな機能がありながらロード速度が大幅に向上しミームになっている阿部寛さんのHPの速度を超えることに成功しました。

https://takanomasanori.tech

http://abehiroshi.la.coocan.jp/

以下は阿部寛さんのHPと webpagetest でのロード速度の比較です。

基本的な戦略はアクセスして最初に表示される領域(Above the hold)の画像などのリソースを最適化しつつそれより下を全て遅延ロードにすることなのですが、90点を超えるにはサーバーサイドのキャッシュの期間を変更したり一見無害そうなGoogleAnalyticsやiframeが予想外のタイミングでロードされてメインスレッドが占有されてしまわないようjavascriptで無理やり後回しにするような荒技が求められます。

背景とHPのスペック

用があって数年前にNext.jsのSSG(静的サイト出力)で作ったあと放置していた個人サイトをテコ入れすることになったのですが、ふと普段業務で用いているGoogleのPageSpeed Insightsに一切ダメだしをされなくなるまでページを直し続けたらどうなるのだろう?と思い、当初37点という低スコアだったサイトが安定して90点以上を出せるまで改良し続けることにしました。

Next.jsのバージョン: 10.2.3(改修前) -> 13.5.5(改修後)
Next.jsの出力方法: SSG
サーバー: AWS Cloudfront
レスポンシブデザイン
Google Analyticsあり
日英語切り替えあり
画像あり
動画あり
iframeあり

基本的な最適化戦略

PageSpeed Insightsでは、最初に表示されるページのエリアのロード速度、特にFirst Contentful Paint (FCP)が重要視されます。
このため、アクセス直後に表示する領域に存在するリソースはファイルサイズの縮小や即時ロードを行って極限まで最適化を施しつつ、表示に必要のないリソースはユーザーの眼に触れた瞬間など必要になったときにロードを行う遅延ロードで行うようにすることが点数をあげる上で効果的です。

画像の遅延ロードの準備:Next.jsのImage

Next.jsのImageは一般的にはSSR向けの機能だと思われていますが、SSGで利用する場合も画像の遅延ロードやプリロードを簡単に実装でき、画像の表示サイズやalt要素の指定し忘れをチェックすることができて便利です。
SSGでImageを利用する場合はnext.config.jsでunoptimizedをtrueにする必要があります。

module.exports = {
  images: {
    unoptimized: true
  }
}

遅延ロードの対象を定める

Next.jsのImageはpriorityを指定しない場合は遅延ロードで画像を読み込む設定(false)になりますが、アクセス直後にユーザーの目に触れる画像も遅延ロードにしてしまうと 美観を損ねてPageSpeed Insightsに怒られてしまいます ので、ページのトップで直ちに表示する画像はpriority属性を設定し、HTMLのロード時に画像も同時に読み込まれるようにする必要があります。

# 画面トップに表示する顔の部分
<Image priority width={200} height={200} alt="Takano Masanori" src='/images/takano.webp' />

また、jpgやpngなどで作成した画像をWebP形式にすることで画像のデータを40%程度削減できますので、アクセス直後に目に触れる部分の画像は画像変換サイトなどで全てwebpにしてしまいましょう。

https://tiny-img.com/webp/

外部リソースの内包化

パフォーマンス向上のためには、外部から読み込んでいるリソースをできる限り削減することが重要です。その一例として、外部リンクで読み込んでいたCSSファイルをローカルにインポートするよう変更しました。

before

<link
  rel="stylesheet"
  href="https://unpkg.com/claymorphism-css/dist/clay.css"
/>

after

import 'claymorphism-css/dist/clay.css';

エラーを(握りつぶしてでも)全部なくす

ハンドリングできていないjavascriptのエラーをなくすこともページのロード速度を上げる上で重要です。
私のHPではユーザーのブラウザの使用言語にあわせて表示する文言を動的に切り替える処理を行っているのですが、SSGで出力したサイトでこれを行うとサーバーサイドで想定している要素の内容とクライアントサイドで表示されるそれに差が発生してがエラー多発するため、それを無視する設定を行うことにしました。

# suppressHydrationWarningで当該部分のエラーを握りつぶす
 <td suppressHydrationWarning>{t("スマホアプリ開発")}</td>

[中級編]サーバーサイドでキャッシュを長めに指定する

私は当初自分のHPを無料のgithub pagesで公開していたのですが、github pagesはキャッシュを10分にしか設定できないため10分ごとに画像などのリソースへのアクセスが発生してしまい非効率です。

https://qiita.com/tiwu_dev/items/ff1629c813aeb59b6b21

なので、awsのs3にアップロードして静的webホスティングをしたのちにそれをCloudfrontで配信し、さらにCloudfrontにカスタムのレスポンスヘッダーを設定してそこでキャッシュ時間を長めに設定することにしました。

[上級編]動画の遅延ロード

自動再生をする動画はpreloadが効かずそのままではアクセス時にロードの対象になってしまいますので、独自に遅延ロードを施す必要があります。以下のような独自定義クラスを作ることで実現することでユーザーの目に触れた瞬間にロードと再生を行うことができます。


  const LazyVideo = ({ src, style }) => {
    const [isInView, setIsInView] = useState(false);
    const videoRef = useRef(null);
  
    useEffect(() => {
      const observer = new IntersectionObserver((entries, observer) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setIsInView(true);
            observer.unobserve(videoRef.current);
          }
        });
      }, {
        threshold: 0.5
      });
  
      observer.observe(videoRef.current);
  
      return () => observer.disconnect();
    }, []);
  
    return (
      <div ref={videoRef} style={{ ...style, display: 'inline-block', overflow: 'hidden' }}>
        {isInView && (
          <video autoPlay loop muted playsInline style={{ width: '100%', height: '100%' }}>
            <source src={src} type="video/mp4" />
          </video>
        )}
      </div>
    );
  };
  
  
# 動画を遅延ロード
<LazyVideo
  src="/images/6d745MuseumMovie.mp4"
  style={{borderRadius: '5px', width: '300px', height: '139px', marginRight: '10px'}}
/>

また動画を自動再生する場合はGIFファイルはサイズが大きいため、圧縮効率が高くiOSのSafariでも再生可能なmp4に変換しましょう。

https://ezgif.com/gif-to-mp4

[上級編]iframeの遅延ロード

外部からリソースを読み込むiframeはページ全体の描画に大きな影響を与えますので可能な限りロードを遅延すべきであり、loading=lazyを指定して遅延ロードを命じる機能がありますが、ブラウザによってかなりばらつきがあり、ユーザーが目にしている地点からかなり離れたところにあってもロードされてしまうケースが多々あります。

 <iframe loading="lazy" title="お問い合わせ" src="https://docs.google.com/forms/d/e/xxxxx/viewform?embedded=true" width="640" height="935" >読み込んでいます…</iframe>

ですので、こちらも以下のような独自定義クラスを作成してユーザーの目に触れるまでロードを遅延しましょう。

  const LazyIframe = ({ src, title, width, height, className }) => {
    const iframeRef = useRef(null);
    const isInView = useIntersectionObserver(iframeRef, {
      threshold: 0.1
    });
  
    return (
      <div ref={iframeRef} className={className}>
        {!isInView ? (
          <div className={styles.skeleton} style={{ width: `${width}px`, height: `${height}px`, marginLeft: 'auto', marginRight: 'auto', backgroundColor: '#ccc' }}></div>
        ) : (
          <div className={styles.skeleton} style={{ width: `${width}px`, height: `${height}px`, marginLeft: 'auto', marginRight: 'auto', backgroundColor: '#ccc' }}>
            <iframe
              className={styles.award_iframe}
              loading="lazy"
              width={width}
              height={height}
              src={src}
              title={title}
              frameBorder="0"
              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
              allowFullScreen
            ></iframe>
          </div>
        )}
      </div>
    );
  };

[上級編]GoogleAnalyticsというラスボス

ここまでの改修を行うだけでもPageSpeed Insightsでおおよそ80点までいくことはできますが、そこから安定して90点以上のスコアを達成する前に立ちはだかる最後の障壁があります。それはPageSpeed Insightsを提供しているGoogle社自身のサービス、GoogleAnalyticsです。

GoogleAanalyticsは簡単に導入できてHPへの来客を可視化してくれる便利なサービスであり、またそのコードは極力サイトの他の挙動に影響を与えないようには出来てはいますが、そこで用いられるtagmanagerの処理は(非同期で読み込んでも)ブラウザの メインスレッド で処理を行うためその間nextjsの描画を行うコードをブロックしてしまい100~200msec程度の遅延を発生させてしまいます。

ですので、PageSpeed Insightsで90点以上を叩き出し阿部寛さんのHPの速度の向こう側へ行くためにはロードが完了するかユーザーのアクションが発生するまでtagmanagerのロードを遅延させる荒技を行う必要があります。

before

      <Head>
          <>
            <script async src="https://www.googletagmanager.com/gtag/js?id=G-4BXXXXXXXX"></script>
            <script
              dangerouslySetInnerHTML={{
                __html: `
                  window.dataLayer = window.dataLayer || [];
                  function gtag(){dataLayer.push(arguments);}
                  gtag('js', new Date());
                  gtag('config', 'G-4BQQ1LP68K');
                `,
              }}
            />
          </>
      </Head>


after


  useEffect(() => {
    const handleLoadOrInteraction = () => {
      // Google Analyticsのスクリプトをロードする関数
      const script = document.createElement('script');
      script.src = 'https://www.googletagmanager.com/gtag/js?id=G-4BXXXXXXXX';
      script.async = true;
      document.head.appendChild(script);
      script.onload = () => {
        window.dataLayer = window.dataLayer || [];
        function gtag() { dataLayer.push(arguments); }
        gtag('js', new Date());
        gtag('config', 'G-4BQQ1LP68K');
      };
  
      // イベントリスナーをクリーンアップ
      window.removeEventListener('load', handleLoadOrInteraction);
      window.removeEventListener('click', handleLoadOrInteraction);
      window.removeEventListener('scroll', handleLoadOrInteraction);
    };
  
    // ページロード後、またはユーザーインタラクション後にスクリプトをロードするイベントリスナーを追加
    window.addEventListener('load', handleLoadOrInteraction);
    window.addEventListener('click', handleLoadOrInteraction);
    window.addEventListener('scroll', handleLoadOrInteraction);
  
    // クリーンアップ関数
    return () => {
      window.removeEventListener('load', handleLoadOrInteraction);
      window.removeEventListener('click', handleLoadOrInteraction);
      window.removeEventListener('scroll', handleLoadOrInteraction);
    };
  }, []);

まとめ

この記事を通じて、Next.jsとSSGを使用したサイトのパフォーマンス最適化の実際のステップを詳しく紹介しましたが、はっきり言って人間が体感できる快適なロード速度を実現するためにはPageSpeed Insightsで70点くらいあれば十分だと思います。
しかし、その学びを含め今回の試みは普段目にしているWebサイトの動作原理やGoogleが目指しているものについて多くの知見が得られました。
あなたも自分のHPを極限までチューニングして阿部寛さんのHPの速度超えにチャレンジしてみませんか?

Discussion