I came across surprising hydration mismatch in a Next.js 15 App Router app when I synchronously instantiate a small WebAssembly module during the render of a client component.
Server builds fine.
On the client during first load I get:
Warning: Text content did not match. Server: "items: 0" Client: "items: 7" Hydration failed because the initial UI does not match what was rendered on the server.
The rendered DOM differs because the client-run synchronous WebAssembly instantiation produces different DOM (count of items) than the server-rendered HTML.
app/layout.tsx (server component)
import './globals.css';
import ClientWasmWidget from '@/components/ClientWasmWidget';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ClientWasmWidget />
{children}
</body>
</html>
);
}
components/ClientWasmWidget.tsx (client component)
'use client';
import React from 'react';
/**
* For test purposes I embedded a tiny WASM module as base64.
* (Replace `WASM_BASE64` with a real base64-encoded tiny wasm module
* that exports `getCount(): i32`.)
*/
const WASM_BASE64 = 'AGFzbQEAAA...'; // placeholder
function base64ToUint8Array(base64: string) {
const raw = atob(base64);
const arr = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; ++i) arr[i] = raw.charCodeAt(i);
return arr;
}
export default function ClientWasmWidget() {
// NOTE: this instantiates the module synchronously during rendering.
// (Using WebAssembly.Module / Instance directly from bytes.)
let count = 0;
try {
const bytes = base64ToUint8Array(WASM_BASE64);
// synchronous compile / instantiate (may throw if not allowed)
const mod = new WebAssembly.Module(bytes);
const inst = new WebAssembly.Instance(mod, {});
// assume wasm exports `getCount` returning an i32
// TS-ignore for simplicity:
// @ts-ignore
count = inst.exports.getCount();
} catch (err) {
// fallback: server might render this branch, or if wasm instantiation fails,
// we render fallback value 0
console.warn('wasm sync instantiate failed:', err);
count = 0;
}
return (
<div>
<p>items: {count}</p>
<ul>
{Array.from({ length: count }).map((_, i) => (
<li key={i}>item #{i + 1}</li>
))}
</ul>
</div>
);
}
Why does doing synchronous WebAssembly work during render cause a hydration mismatch?
Is this considered a “side effect” React forbids during render, even though it’s synchronous and deterministic?
Environment
- Next.js 15 (App Router)
- React 18+
- Node 20 (dev server / SSR)
- Browser: Chrome 120 (client)
The Wasm module is trivial (export getCount() → small i32). It sits at an intersection of SSR, hydration semantics, bundling, and WebAssembly instantiation timing — not a common SO question.