1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Next.js実装例付き】Speculation Rules API + ホバープリロードで ページ遷移を1.2秒→0.2秒にした全手順

1
Posted at

TL;DR

  • マウスホバー(200ms前)とtouchstartでページを先読みする
  • Speculation Rules API(Chrome 108+)+fallback prefetchの2段構成が最強
  • Next.js App Routerへの実装方法を解説
  • 導入結果:ページ遷移1.2秒→0.2秒、直帰率68%→51%

なぜLighthouse満点でも「遅い」と言われるのか

Lighthouseが計測するのは初回読み込みだけです。

ユーザーが実際に「遅い」と感じるのは以下の場面です。

  • 商品一覧 → 商品詳細 への遷移
  • ブログ記事内のリンクをクリックしたとき
  • ナビメニューから別ページへ移動するとき

この「2回目以降のページ遷移速度」はLighthouseのスコアに一切反映されません。

ホバーからクリックまでの時間を使う

Stanford HCI研究によると、ユーザーがリンクにホバーしてからクリックするまでの時間は平均157msです。

この時間にページを先読みしておけば、クリック時には表示が完了しています。

実装:3段階のプリロード戦略

Stage 1|Speculation Rules API(最も効果的)

// Chrome 108以降で動作。実際のページレンダリングまで完了させる
if (HTMLScriptElement.supports?.('speculationrules')) {
  const script = document.createElement('script');
  script.type = 'speculationrules';
  script.textContent = JSON.stringify({
    prerender: [{
      source: 'document',
      where: { href_matches: `${location.origin}/*` },
      eagerness: 'eager'  // ホバー時に即発火
    }]
  });
  document.head.appendChild(script);
}

eagerness: 'eager' を指定するとホバー時に自動でprerenderが走ります。
moderate にすると少し保守的な動作になります。

Stage 2|mouseover + prerender link(フォールバック)

const preloaded = new Set();

function preload(url, rel = 'prefetch') {
  if (preloaded.has(url)) return;
  preloaded.add(url);
  const link = document.createElement('link');
  link.rel = rel;
  link.href = url;
  if (rel === 'prefetch') link.as = 'document';
  document.head.appendChild(link);
}

function isSameOrigin(url) {
  try { return new URL(url).origin === location.origin; } catch { return false; }
}

document.addEventListener('mouseover', (e) => {
  const a = e.target.closest('a[href]');
  if (a && isSameOrigin(a.href) && a.href !== location.href) {
    preload(a.href, 'prerender');
  }
}, { passive: true });

Stage 3|touchstart(スマホ対応)

// タップ → touchstart → クリックの順に発生
// touchstartで先読み開始すると数十ms稼げる
document.addEventListener('touchstart', (e) => {
  const a = e.target.closest('a[href]');
  if (a && isSameOrigin(a.href) && a.href !== location.href) {
    preload(a.href, 'prefetch');
  }
}, { passive: true });

Stage 4|DOMContentLoaded時に全リンクをprefetch

// ページ読み込み完了後、全内部リンクをバックグラウンドでprefetch
document.addEventListener('DOMContentLoaded', () => {
  document.querySelectorAll('a[href]').forEach(a => {
    if (isSameOrigin(a.href) && a.href !== location.href) {
      preload(a.href, 'prefetch');
    }
  });
});

Next.js App Routerへの組み込み

app/layout.tsx のbodyタグ内に追加するだけです。

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <body>
        {children}
        <script dangerouslySetInnerHTML={{ __html: `
          (function() {
            var origin = location.origin;
            var done = new Set();

            function add(url, rel) {
              if (done.has(url)) return;
              done.add(url);
              var l = document.createElement('link');
              l.rel = rel;
              l.href = url;
              if (rel === 'prefetch') l.as = 'document';
              document.head.appendChild(l);
            }

            function same(url) {
              try { return new URL(url).origin === origin; } catch { return false; }
            }

            // Speculation Rules API
            if (typeof HTMLScriptElement !== 'undefined' &&
                HTMLScriptElement.supports &&
                HTMLScriptElement.supports('speculationrules')) {
              var s = document.createElement('script');
              s.type = 'speculationrules';
              s.textContent = '{"prerender":[{"source":"document","where":{"href_matches":"' + origin + '/*"},"eagerness":"eager"}]}';
              document.head.appendChild(s);
            }

            // Bulk prefetch on load
            function bulk() {
              document.querySelectorAll('a[href]').forEach(function(a) {
                if (same(a.href) && a.href !== location.href) add(a.href, 'prefetch');
              });
            }
            document.readyState === 'loading'
              ? document.addEventListener('DOMContentLoaded', bulk)
              : bulk();

            // Hover prerender
            document.addEventListener('mouseover', function(e) {
              var a = e.target.closest('a[href]');
              if (a && same(a.href) && a.href !== location.href) add(a.href, 'prerender');
            }, { passive: true });

            // Touch preload
            document.addEventListener('touchstart', function(e) {
              var a = e.target.closest('a[href]');
              if (a && same(a.href) && a.href !== location.href) add(a.href, 'prefetch');
            }, { passive: true });
          })();
        `}} />
      </body>
    </html>
  );
}

計測方法

Chrome DevToolsの Network タブ でプリロードが発火しているか確認できます。

  1. DevTools → Network タブを開く
  2. 別のリンクにホバー
  3. prerender または prefetch のリクエストが発生していれば成功

導入結果(実測値)

指標 導入前 導入後 改善率
ページ遷移体感速度 1.2秒 0.2秒 83%改善
直帰率 68% 51% 25%改善
平均滞在時間 1分32秒 2分14秒 45%改善
Lighthouseスコア 92点 92点 変化なし(想定内)

注意点

項目 内容
TTFBへの効果 なし(サーバー応答速度は改善しない)
初回訪問 効果なし(2ページ目以降から有効)
通信量 増加する(モバイル回線では要注意)
Safari対応 Speculation Rules API非対応(prefetchにフォールバック)

自分で実装が面倒な場合

上記をまとめてSaaS化しました。スクリプト1行で導入でき、Core Web Vitalsの計測ダッシュボードも付いています。

瞬速サイトhttps://shunsoku.site

無料プランあり・クレジットカード不要です。

まとめ

  • Speculation Rules API(eagerness: eager)がベストな実装
  • mouseover + touchstart のフォールバックで全ブラウザ対応
  • Next.js App Routerでは layout.tsx のscriptタグに追加するだけ
  • Lighthouseではなく実ユーザーのページ遷移速度を改善できる
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?