Key answer: Fast 3D in Next.js comes from isolating the 3D scene into a small client island, loading its bundle lazily with next/dynamic and ssr: false so Three.js never blocks the initial HTML, and adapting quality (device pixel ratio, effects, geometry detail) to the device at runtime. Keep the server-rendered page light and text-first, ship the WebGL code only where a canvas actually appears, and defer it until the section is in view. The goal is a page that is fast to first paint and interactive, with the heavy GPU work arriving after the content the user came for.
Table of contents
- The core problem
- Isolate the 3D into a client island
- Lazy-load the 3D bundle
- Adapt DPR and quality
- Code splitting and keeping HTML light
- Avoid hydration bloat
- Common mistakes
- FAQ
- Conclusion
The core problem
Next.js is built around server rendering and shipping minimal JavaScript. A 3D scene is the opposite: it is client-only (WebGL needs a real canvas and GPU), and its dependency tree — Three.js, React Three Fiber, loaders — is large. If you import that directly into a page, you drag the whole bundle into the initial load, delay interactivity, and try to server-render something that cannot be server-rendered. Every Next.js 3D performance technique is about containing that cost so it does not touch the rest of the page.
Isolate the 3D into a client island
Keep the 3D as a small, self-contained Client Component and let the rest of the page stay a Server Component. The scene becomes an island of interactivity in an otherwise static, text-first page. Mark only the 3D file with 'use client':
// components/Scene.jsx
'use client';
import { Canvas } from '@react-three/fiber';
export default function Scene() {
return (
<Canvas dpr={[1, 2]} camera={{ position: [0, 0, 5] }}>
<ambientLight intensity={0.6} />
<mesh>
<icosahedronGeometry args={[1, 2]} />
<meshStandardMaterial color="#7c5cff" />
</mesh>
</Canvas>
);
}
Your app/page.jsx stays a Server Component that renders headings and copy immediately, and only mounts the island where the canvas belongs. This keeps the "use client" boundary as small as possible, which is the foundation everything else builds on.
Lazy-load the 3D bundle
Even as a client island, a statically imported scene still ships in the initial JavaScript. Load it with next/dynamic and ssr: false so Three.js is split into its own chunk, excluded from server rendering, and fetched only in the browser:
// app/page.jsx (Server Component)
import dynamic from 'next/dynamic';
const Scene = dynamic(() => import('@/components/Scene'), {
ssr: false,
loading: () => <div className="scene-placeholder" aria-hidden="true" />,
});
export default function Page() {
return (
<main>
<h1>Product name</h1>
<p>Copy that renders instantly from the server.</p>
<Scene />
</main>
);
}
For a scene below the fold, go one step further and defer mounting until it is near the viewport with an IntersectionObserver, so the chunk is not even requested until the user scrolls toward it:
'use client';
import { useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic';
const Scene = dynamic(() => import('@/components/Scene'), { ssr: false });
export default function LazyScene() {
const ref = useRef(null);
const [show, setShow] = useState(false);
useEffect(() => {
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { setShow(true); io.disconnect(); }
}, { rootMargin: '200px' });
if (ref.current) io.observe(ref.current);
return () => io.disconnect();
}, []);
return <div ref={ref} style={{ minHeight: 400 }}>{show && <Scene />}</div>;
}
Adapt DPR and quality
The same scene should not render identically on a phone and a desktop GPU. Cap the device pixel ratio and scale quality to the device. React Three Fiber's Canvas accepts a dpr range ([1, 2]) that clamps how many pixels you render; a plain Three.js renderer uses renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)).
Beyond DPR, reduce geometry detail, disable expensive post-processing, and lower shadow resolution on weaker devices. A simple heuristic based on screen width and pointer type is a reasonable starting point:
const isMobile = typeof window !== 'undefined'
&& window.matchMedia('(max-width: 768px)').matches;
const detail = isMobile ? 1 : 3; // fewer subdivisions on mobile
const dpr = isMobile ? [1, 1.5] : [1, 2];
Adapting quality is what keeps a 3D Next.js page smooth across the range of devices that actually visit it, rather than only on the machine you built it on. The 3D scenes in AETumi's library ship with a device-aware DPR cap and quality heuristic like this baked in, so mobile visitors are not forced to render desktop-grade pixels.
Code splitting and keeping HTML light
next/dynamic already splits the scene into its own chunk. Push it further by dynamically importing heavy add-ons — post-processing, physics, large loaders — only inside the scene, so they are not pulled into the main scene chunk unless used. Load big 3D assets (glTF models, KTX2 textures) at runtime rather than importing them into the bundle, and serve them compressed.
The server-rendered HTML should contain the real content — headings, copy, links — so the page is meaningful and indexable before any WebGL runs. The canvas is an enhancement layered on top, not the thing the first paint waits for. This is the difference between a fast 3D site and a blank screen with a spinner. AETumi's Next.js 3D templates ship with this split already wired — the dynamic import, the ssr: false boundary, and a light server shell — so you start from the tuned baseline rather than retrofitting it.
Avoid hydration bloat
Hydration cost scales with how much interactive React you send. Two habits keep it low: keep the 'use client' boundary around the 3D only (do not mark whole layouts as client), and never try to server-render the canvas — mismatches between server and client output cause hydration errors and wasted work. Because the scene loads with ssr: false, there is nothing to hydrate for it on the server; it simply mounts in the browser after the static page is already interactive. Keep global providers lean too, since anything wrapping the tree hydrates on every page.
Common mistakes
Do not statically import Scene from '...' into a page — that defeats splitting and ships Three.js everywhere. Do not attempt ssr: true on a WebGL component; there is no GPU on the server. Do not leave dpr uncapped, or a Retina phone will render several times the pixels it needs. And do not put 'use client' at the top of a shared layout to "make things work" — it turns your whole app into a client bundle. For a full build walkthrough, see how to build a Next.js 3D website; the AETumi templates encode each of these guardrails so they are hard to get wrong.
FAQ
Should a 3D scene be server-rendered in Next.js? No. WebGL requires a browser canvas and GPU, so render the scene client-side with dynamic(..., { ssr: false }). Server-render the surrounding content — text and layout — so the page is fast and indexable, and mount the canvas on top.
Does dynamic import hurt SEO? Not when the meaningful content is in the server-rendered HTML. Search engines index the text, headings, and links your Server Components output. The 3D canvas is decorative enhancement, so deferring it does not remove indexable content.
How do I keep the initial load fast with Three.js? Split the scene into its own chunk with next/dynamic, defer it until in view for below-the-fold scenes, load 3D assets at runtime rather than bundling them, and cap the device pixel ratio. Keep the client boundary tight so hydration stays cheap. If you want these defaults handed to you, AETumi's Next.js and React 3D templates come with the splitting and lazy-mount patterns in place.
Is React Three Fiber slower than plain Three.js in Next.js? The runtime rendering cost is essentially the same — R3F is a React reconciler over Three.js. The performance story in Next.js is dominated by bundling and loading strategy, not by R3F versus imperative Three.js.
Conclusion
Next.js 3D performance is a containment strategy: isolate the scene into a small client island, load it lazily with ssr: false, defer it until it is needed, adapt quality to the device, and keep the server HTML light so the page is fast and indexable before any WebGL runs. Apply those patterns and a 3D Next.js site can feel as quick as a plain one, with the GPU work arriving only where and when it earns its place. See our Three.js hub for the rendering fundamentals, and the starter at https://github.com/AETumiApp/nextjs-threejs-starter for a working baseline.
If you would rather begin from a tuned foundation, AETumi — an AI-native 3D web platform — ships Next.js and React 3D templates you buy once and own for life, with full source code and the AETumi MCP included in the Full Stack plan. Browse the collection and plans at aetumi.app/pricing.
More from the AETumi library
Real, production-ready assets — preview the motion, grab the source.

