How to ship a WebGL hero without failing Core Web Vitals: HTML-first LCP, zero layout shift, lazy and compressed 3D assets, async shaders, honest INP and fallbacks.
Product teams want cinematic 3D on the landing page, and search engines and impatient users reward pages that paint quickly, stay still and respond instantly. Those goals look opposed, because a Three.js scene is usually the heaviest, most CPU-hungry thing a page can carry. In practice the conflict is mostly about ordering. If meaningful HTML arrives first, the canvas's space is reserved, and the 3D is downloaded, compiled and animated only after the page is usable, an immersive page can meet the same Core Web Vitals targets as a plain one. This is the blueprint we follow, step by step.
The tension between spectacle and speed
Core Web Vitals are three field metrics. Largest Contentful Paint measures when the biggest piece of content in the viewport renders, and is rated good at 2.5 seconds or less. Interaction to Next Paint, which replaced First Input Delay in March 2024, measures how long the page takes to paint a response after a tap, click or key press, and is good at 200 milliseconds or less. Cumulative Layout Shift scores unexpected movement of content, and is good at 0.1 or less. Each threshold is judged at the 75th percentile of real-user visits, not on a developer's laptop.
A WebGL hero threatens all three at once. Its bundle competes with the headline for bandwidth, so LCP waits. Shader compilation and geometry generation create long main-thread tasks, so the first taps after load feel sluggish and INP suffers. And a canvas that sizes itself only when JavaScript runs pushes the content below it down the page, which is exactly what CLS punishes.
Google's thresholds are the floor, not the goal. On marketing and commerce pages we set a stricter internal budget: LCP under one second on a mid-range phone over a decent mobile connection, INP comfortably below 200 milliseconds, and CLS as close to zero as the layout allows. None of that requires giving up the 3D. It requires deciding what the browser must do first, and pushing everything else behind it.
Let HTML win the LCP race and hold the canvas's place
The single most useful fact here is that a canvas is not an LCP candidate. The browser considers text blocks, images, video posters and CSS background images, so your LCP element will be the headline, the hero copy or a still image, never the 3D scene itself. The mistake we see most often is a page that hides its headline until the scene is ready, or fades it in with JavaScript. That ties LCP to the slowest thing on the page for no visual gain.
In the Next.js App Router, render the headline, supporting copy and primary call to action in a Server Component so they arrive as HTML in the first response. Anything slow, such as personalised pricing or a live stock count, goes behind a Suspense boundary and streams in afterwards rather than holding back the whole page. Load the display font with next/font, which self-hosts the files and generates a size-matched fallback, so the headline paints immediately and does not reflow when the web font arrives.
If the design calls for a still frame of the scene behind the text, export it as a poster image and render it with next/image. When it is the LCP element, preload it or give it a high fetch priority, size it for each breakpoint, and serve a modern format. The poster paints within the LCP budget, and the live canvas later fades in on top of it. Users see the scene immediately; the metric sees a well-optimised image.
Then reserve every pixel the canvas will occupy. Layout shift from 3D almost always comes from the container: a wrapper with no intrinsic size collapses during server rendering, then jumps to full height when the client component mounts. Give it its final dimensions in CSS from the first paint, with aspect-ratio or explicit heights per breakpoint, position the canvas absolutely inside it, and size the renderer from that box with a ResizeObserver rather than from the window. Spinners and 'drag to rotate' hints belong inside the same box, never in the document flow, so the hero contributes nothing to CLS.
Load the scene late and make it small
Three.js is a substantial chunk of JavaScript before you add loaders, post-processing or physics, and none of it is needed to paint the headline. Keep it out of the initial bundle. In Next.js, the 3D scene belongs in a client component that is imported with next/dynamic and ssr set to false. That option only works when the dynamic import itself lives in a client component, so a small client wrapper that owns the import is the usual pattern. The poster renders on the server; the scene arrives later in its own chunk.
Late does not have to mean slow. Start the import after hydration, when the browser is idle, or when an IntersectionObserver reports that the container is about to enter the viewport, and preload the model file at the same moment so code and assets download in parallel. For scenes below the fold, wait for intersection; for a hero, an idle callback with a short timeout is usually the right balance. Then shrink what you download. Raw glTF exports from design tools are routinely many times larger than they need to be, and the savings from compression are usually the biggest single win in a 3D project.
- Compress geometry with Draco or Meshopt through the glTF extensions, and register the matching decoder on GLTFLoader; Meshopt decodes faster, Draco often produces smaller meshes.
- Convert textures to KTX2 with Basis Universal so they stay compressed in GPU memory, which saves both download size and video memory on phones.
- Run every model through a build step such as glTF Transform to deduplicate, prune unused data, resize oversized textures and quantise vertex attributes.
- Set a hard asset budget per scene and fail the build when a model exceeds it, rather than discovering the regression in field data a month later.
- Serve models and decoders from a CDN with long cache lifetimes and content-hashed file names, so repeat visitors pay the cost only once.
Compile shaders off the critical path and budget every frame
Shader compilation is the hidden cost. By default, WebGL compiles a material's program the first time an object using it is drawn, and on mobile GPUs that can take long enough to freeze the page for a noticeable moment. Three.js offers renderer.compileAsync, which prepares the programs for a scene ahead of time and, where the KHR_parallel_shader_compile extension is available, lets the driver compile without blocking the main thread. Call it once the scene graph is built, await it, and only then start the render loop and fade the canvas in.
Fewer programs also means less to compile. Keep materials to a small shared set, reuse them across meshes, and avoid creating materials inside per-frame code. Repeated geometry such as particles, foliage or product variants belongs in an InstancedMesh, which draws thousands of copies in a single call instead of thousands of separate draw calls.
After the first frame, the budget is per frame. A display at 60 Hz gives you about 16 milliseconds per frame for everything the page does, not just the scene, so the render loop must leave headroom for input and layout. These rules keep the loop inside that budget on the devices that matter.
- Cap the pixel ratio with renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); rendering at 3x on a flagship phone multiplies fragment work for detail nobody can see.
- Pause rendering when an IntersectionObserver reports the canvas is offscreen or the document becomes hidden, and resume only when it is visible again.
- Choose a quality tier at start-up from signals such as navigator.hardwareConcurrency and, in Chromium browsers, navigator.deviceMemory, then scale particle counts, shadows and post-processing to match.
- Render on demand when the scene is static between interactions, instead of drawing 60 identical frames per second.

Keep INP honest
INP covers three phases: input delay, while the main thread finishes whatever it was doing; processing time, while your event handlers run; and presentation delay, until the next frame is painted. 3D pages tend to fail on the first and second. A long task from hydration, model parsing or shader compilation sits in front of the user's first tap, and a pointer handler that recalculates geometry or raycasts against a dense mesh turns a simple click into a visible stall.
The pattern that works is to make handlers record intent and nothing else. A pointermove handler writes the latest coordinates to a ref; the animation loop reads them on the next frame and does the work there. Clicks that change a material or colour set a flag or update a small piece of state, and the loop applies the change. Raycasting runs against simplified proxy meshes rather than the full-detail model, and only on the frame after the input, never once per event.
Anything genuinely heavy, such as procedural geometry, large JSON parsing or physics, runs once at start-up or in a Web Worker. Where it is supported, OffscreenCanvas lets the whole renderer move to a worker, although it complicates DOM-driven interaction and is worth it only for the heaviest scenes. Finally, remember that hydration itself is main-thread work: keep the client component boundary tight around the canvas and its controls, so the rest of the page stays as server-rendered HTML that needs no hydration at all.

Static posters for low-end devices and reduced motion
Not every visitor should get the live scene. Some devices lack WebGL2, some report a single CPU core or very little memory, and some users have asked their operating system for less motion. For all of them, the poster image from the HTML-first step is already a complete, attractive experience. The decision is simply whether to upgrade it.
Respect prefers-reduced-motion by checking the media query before importing the scene at all. For those users, either keep the poster or render a single still frame and stop the loop, and listen for changes to the setting so the page responds without a reload. This is an accessibility requirement first, and a performance win second: it is the fastest possible version of the page. Make the fallback a first-class design deliverable rather than an error state. The poster should be art-directed, the same product angle and lighting as the live scene, so that a visitor on an older phone does not feel they are seeing a broken page.
- Detect WebGL support and context creation failures, and keep the poster if the renderer cannot start.
- Skip the live scene when quality-tier signals or the Save-Data preference suggest a constrained device or connection.
- Handle webglcontextlost events by showing the poster again instead of leaving a blank rectangle.
- Offer an explicit control to start or pause the 3D, which helps users on metered data and anyone sensitive to motion.
Measure in the field, not just in Lighthouse
Lighthouse and lab runs are a useful starting point, but a page-load audit involves no real interaction, so it reports Total Blocking Time as a stand-in for INP, and it tests one simulated device on one network. The numbers that count are real-user measurements. In Next.js, the useReportWebVitals hook or the web-vitals library sends LCP, INP and CLS from production to your analytics, and the Chrome User Experience Report shows how Google sees the same pages at the 75th percentile.
Segment the data before reading it. Average scores hide the very visitors a 3D page is most likely to hurt, so split by device class, connection type and page template, and record which element was the LCP candidate and which interaction produced the worst INP. Attribution data like this turns 'the site feels slow on Android' into a specific handler or asset you can fix. Then make it part of the release process: A regression at the 75th percentile is a failed release, treated the same way as a broken checkout.
- Track asset and JavaScript budgets in CI so oversized models and bundles fail before they ship.
- Test on a real mid-range Android phone on every release, not only on recent laptops and flagship devices.
- Alert when any Core Web Vital crosses its threshold at the 75th percentile for a key template.
- Review the slowest interactions monthly and fix the top offender, because INP regressions accumulate quietly.
How we would approach an immersive build
We start with the budget and the poster, before any 3D code exists: target metrics per template, a still frame that carries the design on its own, and a decision about which devices get the live scene. Design and engineering agree that list together, because the fallback is as much a design problem as a technical one. Then the scene is built behind it, with compression, async compilation and field measurement in place from the first deployment.
That is how our Modern Web Development team works with Three.js and Next.js, and how the HyperLux project, our Next-Gen 3D Generative Product Configurator case study, combines compressed glTF streaming with real-time material and lighting changes in the browser. UI/UX & Product Design covers the art direction, motion specifications and fallback states. If you are planning an immersive site, the Budget Calculator gives an early cost range, and a scoping call is the quickest way to agree performance targets before design begins.
- Core Web Vitals
- Three.js
- Next.js
- WebGL Performance
- Interaction to Next Paint
Written by
Ananya Deshmukh
Principal Frontend Engineer, JP INFOTECH









