ゲスト
📢 PR Claude Opus 4.7 公開! 新機能・破壊的変更・移行手順を 5 分で確認 →

Next.js 静的ビルドで全ページ最上部表示が崩れる scrollRestoration の罠

2026-04-28

#Next.js #Static Export #UX #デバッグ

Next.js + output: "export" 構成で運用していて、遷移後にページが途中から表示される事案が出た。記事ページだけでなくトップ・ログイン・カテゴリ一覧など全ページで散発的に再現する。

症状の整理:

ブラウザの history.scrollRestoration がデフォルトで auto になっていることが真因。Next.js のクライアント遷移は History API ベースなので、ブラウザは「同じセッション内の遷移」として扱い、前ページのスクロール位置をそのまま継承してしまう。

最小修正は 2 段構え:

Step 1: 描画前に scrollRestoration を manual に固定

layout.tsx<head> に inline script を埋め込み、pre-paint で設定する。React のライフサイクルより前に走らせるのがポイント。

// app/layout.tsx
<head>
  <script
    dangerouslySetInnerHTML={{
      __html: `if ('scrollRestoration' in history) history.scrollRestoration = 'manual';`,
    }}
  />
</head>

Step 2: pathname 変更時に paint 直前で scrollTo(0,0)

// components/ScrollToTop.tsx
"use client";

import { useLayoutEffect, useEffect } from "react";
import { usePathname } from "next/navigation";

const useIsoLayoutEffect =
  typeof window !== "undefined" ? useLayoutEffect : useEffect;

export default function ScrollToTop() {
  const pathname = usePathname();
  useIsoLayoutEffect(() => {
    if (typeof window === "undefined") return;
    if (window.location.hash) return; // ハッシュリンクは尊重
    window.scrollTo(0, 0);
  }, [pathname]);
  return null;
}

useEffect ではなく useLayoutEffect を使うのは、paint 後に scrollTo すると一瞬前ページの位置が見えるから。SSR で useLayoutEffect 警告が出るので、サーバー時は useEffect にフォールバックする isomorphic イディオムにする。

ハッシュ URL(/articles/foo/#section)は早期 return で除外。これを忘れると、目次クリックで section に飛んだ瞬間に最上部に弾き返されるという二次被害が起きる。

検証は手動で 6 経路を踏む:

  1. 記事 → トップ(ロゴクリック)→ 最上部?
  2. 記事 → カテゴリ → 最上部?
  3. 記事 → 別記事(関連リンク)→ 最上部?
  4. 記事 → ハッシュリンク内移動 → 該当 section?(最上部に飛ばないこと)
  5. ブラウザ戻る → 前回位置復元?(これは Next.js が別経路で対応)
  6. ブラウザリロード → 最上部?

trailingSlash 罠と並んで、Next.js 静的ビルドで「気づきにくい」UX 不具合の典型。コンポーネント 1 つと inline script 1 行で解決できるので、新規プロジェクトの初期テンプレに最初から入れておくべき。