4

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.

2
  • 1
    I edited this to remove the additional, opinion-based/subjective requests. Stack Overflow questions need to be objectively answerable. Commented Oct 28 at 18:01
  • So for the people who don't know what is hydration, Hydration is a process through which a react or other a JavaScript framework attaches js logic to html in client side and make it dynamic (Static html, which was rendered in serverside) Commented Oct 29 at 11:50

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.