「画面のちらつき」は一見1つのバグに見えますが、実は原因がまったく違う2種類に分かれます。まずこの2つを見分けられれば、解決は9割終わったようなものです。
Step 1: ちらつきはどちらのタイプ?
- A. 読み込み時に一度だけ光る: ページが表示された瞬間に白い(間違った)画面が見えて、その後で切り替わる — 例えばダークモードが効く前に一瞬明るく光る、または本来の画面が出る前にログアウト状態の画面が一瞬映る。 → たいていは hydration mismatch か theme flash です。
- B. 止まらないちらつき: 画面が揺れ続ける・ずっと再読み込みしているように感じる、タブが重くなる、ファンが回り出す。 → たいていは infinite re-render loop です。
確実に見分けるには、ブラウザのDevToolsを F12 で開き → Console タブで赤いエラーを読みます:
Hydration failed/Text content did not match→ AMaximum update depth exceeded→ B
---
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) 前の値をもとに更新する — 依存を外す
// 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の中で作る
// 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公式ドキュメントより)
window、localStorage、Date()をレンダリングのコード内で直接使っているtypeof window !== 'undefined'のようなチェックでUIを分岐している- ダーク/ライトのテーマをブラウザでしか読んでおらず、最初の描画が間違ったテーマになる
- ブラウザ拡張機能(翻訳、ダークモード拡張)がHTMLを書き換えている → シークレットウィンドウで確認 すると切り分けられます
Fix 1: ブラウザ限定の部分を useEffect で後回しにする
サーバーとクライアントの最初の描画で同じものを出し、そのあとで切り替えます。
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: 特定のコンポーネントだけサーバーレンダリングを止める(地図・グラフ・ブラウザ限定ウィジェット)
import dynamic from 'next/dynamic'
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })
Fix 3: どうしても値が変わってしまう場合(時計・日付)は、その要素だけ警告を止める
<time dateTime="2016-10-25" suppressHydrationWarning />
注意: これは 1階層だけ 効く逃げ道で、Reactは食い違ったテキストを修正しません — 多用しないでください。
ダークモードのちらつき(白→ダーク)の場合
next-themes を使っているなら、最上位の <html> タグに suppressHydrationWarning を付け、テーマに依存するUIはコンポーネントがマウントされた後にだけ描画します。
// 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に貼ってください — 最初の行がたいてい該当ファイルと行番号を指し示します。
