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 タブ でプリロードが発火しているか確認できます。
- DevTools → Network タブを開く
- 別のリンクにホバー
-
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ではなく実ユーザーのページ遷移速度を改善できる