「画面のちらつき」は一見1つのバグに見えますが、実は原因がまったく違う2種類に分かれます。まずこの2つを見分けられれば、解決は9割終わったようなものです。

Step 1: ちらつきはどちらのタイプ?

  • A. 読み込み時に一度だけ光る: ページが表示された瞬間に白い(間違った)画面が見えて、その後で切り替わる — 例えばダークモードが効く前に一瞬明るく光る、または本来の画面が出る前にログアウト状態の画面が一瞬映る。 → たいていは hydration mismatchtheme flash です。
  • B. 止まらないちらつき: 画面が揺れ続ける・ずっと再読み込みしているように感じる、タブが重くなる、ファンが回り出す。 → たいていは infinite re-render loop です。

確実に見分けるには、ブラウザのDevToolsを F12 で開き → Console タブで赤いエラーを読みます:

  • Hydration failed / Text content did not matchA
  • Maximum update depth exceededB

---

B. 止まらないちらつき: infinite re-render loop(最も多い)

コンソールに Maximum update depth exceeded と出ていれば、それが原因です。理由はほぼ毎回同じで、useEffect の中で state を更新し、その新しい state が同じ useEffect を再び呼び出す、というのが延々と続いています。

AI(Cursor / v0 / Bolt / Lovable)にそのまま貼る文

> コンソールに Maximum update depth exceeded が出ていて、画面が止まらずちらつきます。おそらく useEffect が間違った依存配列で setState を呼び、無限ループになっています。原因の useEffect を特定して直してください — 依存配列を正しくするか、更新関数の形(setCount(c => c + 1))を使ってください。警告を隠すのではなく、根本原因を直してください。

コードが読めるなら(Reactの公式な直し方2つ)

1) 前の値をもとに更新する — 依存を外す

js

// Problem: count changes every run, so the effect re-runs forever
useEffect(() => {
  setCount(count + 1);
}, [count]);

// Fix: updater function + empty deps
useEffect(() => {
  setCount(c => c + 1);
}, []);

2) オブジェクトや関数を依存に渡さない — effectの中で作る

js

// Problem: a new options object every render → infinite loop
const options = { serverUrl, roomId };
useEffect(() => {
  createConnection(options);
}, [options]);

// Fix: build it inside the effect
useEffect(() => {
  const options = { serverUrl, roomId };
  createConnection(options);
}, [roomId, serverUrl]);

手早いチェック: まず 依存配列がまったく無い useEffect(() => { ... }) を疑ってください — [] が無いと毎回のレンダーで実行され、簡単にループします。

---

A. 読み込み時に一度だけ光る: hydration mismatch / theme flash

Next.jsのようなフレームワークでは、サーバーがHTMLを先に描画し、ブラウザが最初の描画でもう一度レンダリングします。この2つが一致しないと、画面が一度だけガクッと切り替わります。コンソールには Text content did not match / Hydration failed が出ます。

よくある原因(Next.js公式ドキュメントより)

  • windowlocalStorageDate() をレンダリングのコード内で直接使っている
  • typeof window !== 'undefined' のようなチェックでUIを分岐している
  • ダーク/ライトのテーマをブラウザでしか読んでおらず、最初の描画が間違ったテーマになる
  • ブラウザ拡張機能(翻訳、ダークモード拡張)がHTMLを書き換えている → シークレットウィンドウで確認 すると切り分けられます

Fix 1: ブラウザ限定の部分を useEffect で後回しにする

サーバーとクライアントの最初の描画で同じものを出し、そのあとで切り替えます。

jsx

import { useState, useEffect } from 'react'

export default function App() {
  const [isClient, setIsClient] = useState(false)
  useEffect(() => { setIsClient(true) }, [])
  return <h1>{isClient ? 'Browser-only content' : 'Prerendered'}</h1>
}

Fix 2: 特定のコンポーネントだけサーバーレンダリングを止める(地図・グラフ・ブラウザ限定ウィジェット)

jsx

import dynamic from 'next/dynamic'
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })

Fix 3: どうしても値が変わってしまう場合(時計・日付)は、その要素だけ警告を止める

jsx

<time dateTime="2016-10-25" suppressHydrationWarning />

注意: これは 1階層だけ 効く逃げ道で、Reactは食い違ったテキストを修正しません — 多用しないでください。

ダークモードのちらつき(白→ダーク)の場合

next-themes を使っているなら、最上位の <html> タグに suppressHydrationWarning を付け、テーマに依存するUIはコンポーネントがマウントされた後にだけ描画します。

jsx

// layout.jsx (app directory)
<html lang="en" suppressHydrationWarning>

// A component that renders differently by theme
const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) }, [])
if (!mounted) return null

next-themesは ページが描画される前に 実行されるスクリプトを差し込むので、標準ではちらつきは起きないはずです。それでもちらつく場合は、上の2つ(suppressHydrationWarning、またはマウント後に描画)のどちらかが抜けている可能性が高いです。

---

AIにそのまま渡せる万能メッセージ

> 画面が[読み込み時に一度だけ光る / 止まらずちらつき]ます。コンソールのエラーは[エラー文をそのまま貼る]です。原因のコンポーネントを見つけて、公式なやり方で直してください。警告を隠すのではなく、根本原因を直してください。

まだ確実な解決策が無いとき(今できること)

  • ブラウザ拡張機能(翻訳、ダークモード系)を無効にして、シークレットウィンドウ で確認してください。止まれば拡張機能が原因でした。
  • コンソールのエラーを 全文 コピーして、そのままAIに貼ってください — 最初の行がたいてい該当ファイルと行番号を指し示します。