Key answer: the production architecture for Next.js + Three.js keeps the page server-rendered and treats the WebGL scene as a client island mounted below it. Server-render the semantic content — headings, copy, prices, links — so it ships in the initial HTML and stays crawlable, then load the Three.js scene in a client component with SSR disabled, lazily, below the fold. Never render Three.js on the server (it needs a browser canvas), never block first paint on the scene, and keep meaning in HTML rather than the canvas. This architecture gives you a fast, indexable, accessible Next.js page with cinematic 3D layered on top. This guide lays out where the canvas mounts, the SSR-safe loading pattern, performance, and the SEO layer, with real code.
Table of contents
- The core architecture
- Why Three.js cannot server-render
- The server-rendered content layer
- Mounting the scene as a client island
- SSR-safe dynamic import
- Lazy-loading below the fold
- Cleaning up on unmount
- Performance in Next.js
- The SEO layer
- React Three Fiber or vanilla?
- How AETumi structures Next.js 3D
- Technical proof on GitHub
- FAQ
- Related resources
- Conclusion
The core architecture
The production architecture for a Next.js and Three.js site is a two-layer model: a server-rendered document underneath, a client-mounted WebGL scene on top. The document carries all the meaning — headings, copy, product data, links — and ships in the initial server response. The scene is a client island that hydrates and mounts a canvas after the page is interactive.
This separation is what lets a Next.js 3D site be fast and indexable while still being cinematic, and it is the standard behind every build on the Three.js and 3D websites hubs. Get the two layers right and everything else — performance, SEO, accessibility — follows from the structure rather than being retrofitted.
Why Three.js cannot server-render
Three.js renders into a WebGL canvas, which only exists in a browser. There is no canvas, no WebGL context, and no window on the server, so any attempt to run a Three.js scene during server rendering throws. This is not a limitation to work around — it is the reason the architecture puts the scene in a client island in the first place.
The practical consequence is firm: the scene must be a client component, and it must be imported in a way that never runs on the server. Fighting this — trying to render 3D server-side — is the most common cause of build errors and hydration crashes in Next.js 3D projects. Accept that the scene is client-only and the architecture becomes clean, which is why React Three Fiber and vanilla Three.js alike mount only on the client.
The server-rendered content layer
Build the page as a normal server-rendered Next.js document first. The headings, copy, product data and links are server components that ship in the initial HTML, fully crawlable, before any 3D exists.
// app/product/page.tsx — server component, ships meaningful HTML
export default function ProductPage() {
return (
<section className="product">
<Scene /> {/* client island, mounts canvas */}
<div className="buy">
<h1>AESPORT carbon running shoe</h1>
<p className="price">$189</p>
<a href="/cart/add/aesport">Add to cart</a>
</div>
</section>
)
}
The heading, price and link render on the server and reach the crawler immediately. The <Scene /> island is where the client-only 3D lives. This is the content layer that makes the page indexable regardless of the WebGL, the same crawlable foundation used across interactive websites.
Mounting the scene as a client island
The scene is a client component marked with the client directive, owning the canvas and the render loop. It reads any inputs it needs — scroll, variant state — and renders, but it holds no meaning the page depends on.
'use client'
import { useEffect, useRef } from 'react'
export default function SceneClient() {
const ref = useRef(null)
useEffect(() => {
const renderer = new THREE.WebGLRenderer({ canvas: ref.current, antialias: true })
renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
// ...build scene, start loop...
return () => renderer.dispose() // clean up on unmount
}, [])
return <canvas ref={ref} aria-hidden="true" />
}
The canvas is aria-hidden because it carries no semantic content. The island hydrates on the client and mounts the scene without ever touching the server, which keeps the WebGL cleanly separated from the crawlable document.
SSR-safe dynamic import
To guarantee the scene never runs on the server, import it with Next.js dynamic import and SSR disabled. This is the single line that prevents server-side crashes.
import dynamic from 'next/dynamic'
const Scene = dynamic(() => import('./SceneClient'), { ssr: false })
With ssr: false, Next.js excludes the scene from server rendering entirely and mounts it only in the browser. This is the canonical pattern for putting Three.js in Next.js — the server ships the content, the client loads the scene. Skipping it is the usual cause of the window is not defined errors that plague Next.js 3D projects, and the docs cover the same pattern in depth.
Lazy-loading below the fold
The heavy Three.js bundle and models should not compete with the content for the initial load. Load the scene lazily so first paint is the fast server-rendered HTML, and the 3D arrives after. Combine the dynamic import with a loading placeholder and, where the scene is below the fold, defer mounting until it approaches the viewport.
const Scene = dynamic(() => import('./SceneClient'), { ssr: false, loading: () => <div className="scene-skeleton" /> })
Reserving space with a skeleton prevents layout shift when the canvas mounts, protecting cumulative layout shift. Lazy loading is what keeps the largest contentful paint an HTML element rather than the WebGL scene, the key to green Core Web Vitals on a Next.js 3D page.
Cleaning up on unmount
Next.js client navigation mounts and unmounts components as the user moves between routes, so a Three.js scene that does not clean up leaks GPU memory on every navigation. Dispose the renderer, geometries, materials and textures, and cancel the animation frame, in the effect cleanup.
useEffect(() => {
let raf
const loop = () => { raf = requestAnimationFrame(loop); renderer.render(scene, camera) }
loop()
return () => { cancelAnimationFrame(raf); renderer.dispose() }
}, [])
Explicit cleanup on unmount is essential in a single-page app, where the same scene may mount and unmount many times in one session. Skipping it produces the slow memory climb that eventually crashes the tab, the most common Next.js 3D bug in production.
Performance in Next.js
Next.js gives you the tools to keep a 3D page fast, but you have to use them. Cap devicePixelRatio to 2, code-split the scene so it is not in the main bundle, lazy-load models, render on demand, and pause the loop when the canvas is off-screen or the tab hidden. Keep the largest contentful paint a server-rendered element. Because the content ships server-side and the scene loads as a deferred client island, the page is interactive and readable long before the 3D appears — the architecture doing the performance work for you across 3D websites.
The SEO layer
The SEO story in this architecture is strong precisely because the meaning is server-rendered. Use the Next.js metadata API for the title, description and canonical, ship structured data in the document, and keep the canvas aria-hidden. The crawler receives the full content in the initial HTML, independent of the client-only scene.
export const metadata = {
title: 'AESPORT carbon running shoe | AETumi',
description: 'A 184-gram racing shoe with a full-length carbon plate. $189.',
alternates: { canonical: 'https://aetumi.app/product/aesport' }
}
Because the scene never renders on the server and carries no meaning, it cannot hurt indexing. The Next.js server-rendered content layer is what makes a 3D page rank, exactly as covered in Three.js SEO guidance.
React Three Fiber or vanilla?
Within this architecture, both React Three Fiber and vanilla Three.js work — the client-island boundary is the same. React Three Fiber fits naturally in a React codebase, expressing the scene as components and integrating with React state, at the cost of a dependency and its own render model. Vanilla Three.js gives you full control and no abstraction, at the cost of more manual wiring. Choose R3F when the scene is React-shaped and you want state integration; choose vanilla when you want direct control or minimal dependencies. Either way the scene stays a client island imported with SSR disabled, a decision explored in depth on the React Three Fiber hub.
How AETumi structures Next.js 3D
AETumi is an AI-native 3D web platform for production Three.js and WebGL sites, React and Next.js components, and MCP workflows for assistants such as Claude Code. AETumi Next.js 3D pages follow this architecture exactly: server-rendered content, the scene as a client island imported with SSR disabled, lazy loading with reserved space, explicit unmount cleanup, and the metadata and structured-data SEO layer. Because the scene is a componentized island, a Next.js 3D page can be scaffolded through an MCP workflow with the architecture already correct rather than debugged into place.
Technical proof on GitHub
The AETumiApp/threejs-scroll-animation and AETumiApp/aetumi-3d-components repositories show the client-island scene, the capped renderer and the cleanup pattern this architecture depends on, in runnable code. Use them for the implementation and this article for the Next.js architecture around them.
FAQ
Can Three.js be server-rendered in Next.js? No. Three.js needs a browser canvas and WebGL context, which do not exist on the server, so the scene must be a client component imported with SSR disabled. Server-render the content layer and mount the scene as a client island.
How do I add Three.js to Next.js without SSR errors? Import the scene component with next/dynamic and ssr: false, so Next.js never runs it on the server. This is the canonical fix for window is not defined and hydration errors in Next.js 3D projects.
Where should the canvas mount in a Next.js app? In a client component below the server-rendered content, marked aria-hidden. The headings, copy, price and links render on the server for crawlability; the canvas mounts on the client as a decorative island.
Does a Next.js Three.js site hurt SEO? No, if the content layer is server-rendered and the canvas is decorative. Use the metadata API and structured data, keep meaning in HTML, and the crawler indexes the page regardless of the client-only scene.
Should I use React Three Fiber or vanilla Three.js in Next.js? Both work as a client island. Use React Three Fiber when the scene is React-shaped and you want state integration; use vanilla Three.js for direct control and fewer dependencies. The SSR-disabled client-island boundary is identical either way.
Related resources
Continue with Three.js, React Three Fiber, 3D websites, the docs, reusable 3D components, interactive websites, and options for agencies.
Conclusion
The production architecture for Next.js + Three.js is two clean layers: a server-rendered document that carries all the meaning, and a client-island scene mounted below it with SSR disabled. Never render 3D on the server, never block first paint on the scene, clean up on unmount, and keep the content in HTML. Build on that structure and a Next.js 3D site is fast, indexable and accessible by construction — cinematic on top of a page that stands on its own.
More from the AETumi library
Real, production-ready assets — preview the motion, grab the source.

