← All postsEngineering8 April 2026 · 7 min read

Spline 3D in Next.js without killing performance: what we learned shipping it

In short

You can run a Spline robot, a WebGL shader hero and a canvas particle field on one Next.js page without melting laptops, but none of it is free by default. The three changes that mattered most for us: pause every render loop when its canvas is offscreen, gate the first paint behind real load signals, and respect prefers-reduced-motion with actual static frames.

Our own site runs a Spline scene in the hero, an animated shader in the closing section and a canvas entropy field inside a card, all on the same single scroll page. This post is the checklist we wish we'd had, written after shipping it. Everything below is from our production code, not theory.

Rule one: nothing renders offscreen

A WebGL canvas keeps drawing at 60fps whether or not anyone can see it. With three animated surfaces on one page, that's three render loops competing for the GPU while the user reads a paragraph in the middle of the page, touching none of them.

The fix is one IntersectionObserver per animated component: when the canvas leaves the viewport (we use a margin so it wakes slightly before arriving), cancel the requestAnimationFrame loop; when it re-enters, resume. It's roughly fifteen lines per component and it's the single largest performance win on the page. Idle GPU work dropped to near zero on long reads.

useEffect(() => {
  const el = canvasRef.current;
  if (!el) return;
  const io = new IntersectionObserver(
    ([entry]) => { runningRef.current = entry.isIntersecting; },
    { rootMargin: "120px" }
  );
  io.observe(el);
  return () => io.disconnect();
}, []);
// the rAF loop early-returns (and stops scheduling) while runningRef is false

Rule two: load screens should watch real signals

A page heavy with 3D has a genuinely heavy first load, and a fake percentage bar that finishes before the robot exists makes it worse. Our loader watches three real signals with weights: document.fonts.ready, the window load event, and Spline's own onLoad callback (we re-dispatch it as a DOM event so any component can listen). The displayed percentage eases toward the weighted real progress.

Two safeguards matter as much as the signals. A minimum display time (about 700ms) so the loader never strobes on fast connections, and a hard failsafe (we use seven seconds) that dismisses the loader even if a signal never fires. A loading screen that can hang is worse than no loading screen.

Rule three: reduced motion means a real static frame

prefers-reduced-motion users should get the composition, not the choreography. For each animated surface we render one static frame and stop: the shader draws once, the particle field draws once, entrance animations collapse to their final state. A global CSS kill switch catches the long tail of small transitions. This is also free insurance against the cohort of users whose laptops simply don't like WebGL.

The measurement bug that cost us an evening

If a canvas lives inside an element with a 3D transform (ours sat inside a card with rotateX perspective), getBoundingClientRect() returns the foreshortened, projected size, not the layout size. We sized a canvas from it and got a field that quietly rendered at two thirds height. The fix: size canvases from offsetWidth and offsetHeight, which report layout dimensions regardless of transforms.

Smaller lessons that still matter

  • Mount Spline on the client only. The runtime is browser only; keep it in an isolated client component so the server rendered shell stays fast
  • Match your page background to the scene's render colour exactly. A near miss (we shipped #080808 against #0e0e0e once) shows up as a visible seam on phones
  • CSS filters work on the Spline container. We desaturate our robot with a plain filter: grayscale(100%), no scene edit required
  • Spline's watermark is baked into the canvas. No DOM element exists to hide, so plan for it: a CSS mask on the wrapper works if your plan permits removal
  • One canvas per purpose. Resist merging effects into one mega canvas; independent loops you can pause beat one loop you can't

Frequently asked questions

Does Spline work with Next.js App Router and React Server Components?

Yes, with the Spline component isolated in a client component. The scene itself can't render on the server, but the rest of the page should. Keep the import inside the client boundary so the runtime never lands in the server bundle.

How much does a Spline scene hurt Core Web Vitals?

The scene mainly threatens LCP and main thread time during load. Gating with a loader driven by real signals, lazy mounting 3D below the fold, and pausing offscreen loops kept our page responsive. Measure with the performance panel, not vibes.

Should I pause WebGL animations when they're offscreen?

Always. A canvas outside the viewport still renders every frame unless you stop it. An IntersectionObserver per animated component, cancelling its requestAnimationFrame loop, was the biggest single performance win on our production page.

How do you handle prefers-reduced-motion with 3D scenes?

Render one static frame per surface and stop the loop, keep the composition visible, and add a global CSS kill switch for small transitions. Users asked for less motion, not a blank page.

If you'd rather someone else carries this checklist for you, that's literally our job.

© 2026 Dinimiciuil Labs. All rights reserved. Written on the build floor in Dublin. You are welcome to quote a short excerpt with a link back; please do not republish the full article without permission.

Keep reading
EngineeringPWA vs native for Ai first apps: what we chose for our own product and whyStarbucks' PWA is 99.84% smaller than its iOS app. Twitter Lite cut bounce 20%. Here is the 2026 state of PWA vs native for AI products, and why our own app ships as an installable web app first.Read →TeardownWhat building five Ai products at once taught usReal numbers from a five product Ai portfolio: $0.0013 per scan, a $7.60 ad pipeline, one killed Samsung app, and the Ai slop trap we hit three separate times.Read →Offline techBuilt for concerts, drafted by democracy: why offline messaging matters every time the internet goes darkMesh messaging apps are designed for festivals, flights and mountain treks. Then a government cuts the internet around a protest and the same technology becomes civic infrastructure. How shutdowns work, what mesh can honestly do, and why we are building Beepeto for the boring cases.Read →

Build somethingAi nativeagenticroboticreal

We design, build and deploy Ai products end to end, from Dublin.

Start a projectSee how we ship
© 2026 Dinimiciuil Labs. All rights reserved.Dublin · Ireland