Contexto
Al implementar dark mode en un portfolio Next.js 16, el sitio flasheaba blanco por una fraccion de segundo antes de aplicar el tema guardado en localStorage. El problema: React hydration ocurre despues de que el browser ya pinto el primer frame.
Lo que aprendi
La solucion es inyectar un <script> sincrono directamente en <head> usando dangerouslySetInnerHTML. Este script se ejecuta antes de que el browser pinte, asi que el usuario nunca ve el flash.
El truco clave es que este script NO puede ser un componente React ni usar next/script con strategy="afterInteractive" -- esos se ejecutan demasiado tarde. Tiene que ser inline y sincrono.
// app/layout.tsx
<html lang="es" className={fontVariables} suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `(function(){
var t = localStorage.getItem("site-theme") || "editorial";
var m = localStorage.getItem("site-mode") || "dark";
document.documentElement.classList.add("theme-" + t);
if (m === "dark") document.documentElement.classList.add("dark");
})()`,
}}
/>
</head>
Puntos importantes:
suppressHydrationWarningen<html>es necesario porque el server no sabe que clases agrego el script, y React se quejaria del mismatch- El script lee de localStorage (que es sincrono) y agrega clases al
documentElement - Se envuelve en IIFE para no contaminar el scope global
- El default es
"dark"porque es el modo preferido del sitio
Por que no usar next/script
next/script con strategy="beforeInteractive" suena como la solucion, pero en App Router tiene limitaciones. El approach mas confiable sigue siendo dangerouslySetInnerHTML directo en <head> dentro del layout.