Listen up. If you're building modern applications, understanding The Renderer in Three.js 3D WebGL is non-negotiable. This is where simple logic turns into intelligent behavior.
1The Renderer: Completing the Holy Trinity
The final piece of the Holy Trinity is the Renderer. This is the machine that takes your Scene and your Camera and paints it onto the screen.
new THREE.WebGLRenderer() wraps a WebGL rendering context and gives you the object responsible for actually rasterizing your scene graph into pixels. On its own it does nothing visible ā you still need to size it, mount its canvas into the DOM, and call render(scene, camera) to produce a frame.
Unlike the Scene (which just holds data) or the Camera (which just defines a viewpoint), the Renderer is the active component: it walks every mesh, resolves materials and lighting, and issues the actual WebGL draw calls to the GPU.
const renderer = new THREE.WebGLRenderer();3D Scene rendered. Objects: 4, Draw Calls: Optimized.
2Sizing the Renderer and Mounting the Canvas
The Renderer's job is extremely complex: it evaluates all geometry, materials, lights, and shadows, and calculates the final pixel colors.
renderer.setSize(width, height) resizes the underlying <canvas> element (and, unless you pass false as a third argument, its CSS display size too) so the drawing buffer matches your viewport. renderer.domElement is the actual canvas the renderer created internally ā you append that to the page yourself in vanilla Three.js.
Forgetting setSize is a classic first bug: the canvas defaults to a tiny 300x150 buffer, so your scene renders correctly but looks squeezed into a postage-stamp-sized box until you explicitly size it to match window.innerWidth/innerHeight.
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
3The domElement Canvas
The WebGLRenderer creates and manages a <canvas> element internally, exposed as renderer.domElement, which is the HTML element you append to the DOM to actually display the graphics.
You never create the canvas yourself unless you explicitly pass one into the WebGLRenderer constructor (new THREE.WebGLRenderer({ canvas: myCanvasEl })) ā otherwise Three.js instantiates a fresh <canvas> for you and hands it back via domElement.
Everything the GPU draws each frame is painted into this single canvas; there's no separate DOM node per 3D object, which is exactly why 3D content inside it is invisible to the DOM, screen readers, and search engine crawlers.
document.body.appendChild(renderer.???);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
4Building a Render Loop
If you just call renderer.render(scene, camera) once, you get a static image. To get animations, we need a Render Loop ā a function that keeps calling itself and re-rendering the scene on every frame.
The pattern is always the same: mutate some property (like cube.rotation.x), call renderer.render(scene, camera) to draw the updated state, then schedule the next iteration. Because requestAnimationFrame recurses inside animate() itself, the loop keeps running indefinitely without you needing setInterval or manual timers.
If you forget to call animate() at least once to kick off the loop, nothing will ever render past the very first frame ā the scene will appear frozen even though your update logic is technically correct.
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
renderer.render(scene, camera);
}
animate();3D Scene rendered. Objects: 4, Draw Calls: Optimized.
5requestAnimationFrame Explained
requestAnimationFrame is the browser API used to create a smooth, display-refresh-synced render loop, typically around 60 FPS on standard monitors (and higher on 120Hz+ displays).
Unlike setInterval or setTimeout, requestAnimationFrame is synchronized with the browser's own repaint cycle, so it automatically pauses when the tab is backgrounded (saving battery and CPU) and avoids scheduling frames faster than the display can actually show them.
Because it hands you a callback rather than a fixed millisecond interval, frame timing can still vary ā for movement that should look identical regardless of frame rate, multiply per-frame changes by a delta time (via THREE.Clock) instead of relying on a flat increment like rotation.x += 0.01.
???(animate);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
6Antialiasing in WebGLRenderer
The renderer also handles antialiasing. Antialiasing smooths out the jagged, pixelated edges of 3D models by blending edge pixel colors instead of leaving hard, stair-stepped transitions.
Passing { antialias: true } to the WebGLRenderer constructor enables MSAA (multisample antialiasing) at the WebGL context level ā it must be set at creation time, since it configures the underlying WebGL drawing buffer and can't be toggled afterward like most other renderer settings.
Antialiasing has a real GPU cost because it samples each pixel multiple times, so on performance-constrained targets (mobile, VR) it's common to disable it and rely on FXAA/SMAA post-processing passes instead, which are cheaper but slightly softer.
const renderer = new THREE.WebGLRenderer({ antialias: true });3D Scene rendered. Objects: 4, Draw Calls: Optimized.
7R3F's Automatic Renderer Setup
In React Three Fiber, you don't instantiate the renderer yourself. The <Canvas> component creates a WebGLRenderer with antialias: true by default!
The gl prop on <Canvas> lets you pass through any WebGLRenderer constructor option ā antialias, alpha, powerPreference, and so on ā without ever touching THREE.WebGLRenderer directly. R3F also manages the render loop internally via its own requestAnimationFrame-driven scheduler, so you don't write an animate() function by hand.
This is one of R3F's biggest ergonomic wins over vanilla Three.js: sizing, resize handling, pixel ratio, and the render loop are all wired up correctly out of the box, eliminating an entire class of setup bugs.
<Canvas gl={{ antialias: true, alpha: false }}>
{/* Scene */}
</Canvas>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
8MSAA on a High-Poly Sphere
Look at this high-resolution sphere. The renderer is doing the heavy lifting to ensure the edges are perfectly smooth using MSAA (Multisample Anti-Aliasing).
This sphereGeometry uses 64 width and height segments, producing a dense mesh where the silhouette against the background is made of thousands of tiny triangle edges. Without antialiasing, each of those edges would show visible pixel stair-stepping, especially noticeable on a curved surface like a sphere.
MSAA works by sampling each pixel at multiple sub-positions and averaging the result only along geometry edges, which is why it costs comparatively little versus full-scene supersampling while still meaningfully cleaning up silhouette edges like the ones on this sphere.
const App = () => {
return (
<Canvas>
<ambientLight intensity={1} />
<directionalLight position={[5, 5, 5]} />
<mesh>
<sphereGeometry args={[2, 64, 64]} />
<meshStandardMaterial color="#00F0FF" />
</mesh>
</Canvas>
);
};
render(<App />);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
9Handling Pixel Ratio and High-DPI Screens
Another critical setting on the renderer is pixel ratio. High-DPI screens (like Mac Retinas) pack multiple physical pixels into one logical pixel, so a canvas sized purely in CSS pixels will look blurry unless the renderer accounts for that density.
renderer.setPixelRatio(window.devicePixelRatio) tells the renderer to render at the display's actual physical resolution rather than its logical CSS size, producing crisp output on Retina and other high-DPI screens.
Blindly using the raw devicePixelRatio is risky on very high-density mobile devices (ratios of 3 or more), since it multiplies the number of pixels the GPU must shade ā this is exactly why it's common practice to clamp the value rather than use it unbounded.
// Vanilla Three.js
renderer.setPixelRatio(window.devicePixelRatio);
// R3F Canvas
<Canvas dpr={[1, 2]}>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
10Clamping dpr for Performance
We pass an array [1, 2] to the dpr prop in React Three Fiber to clamp the pixel ratio between 1 and 2, rather than letting devicePixelRatio drive rendering resolution unbounded.
R3F reads the array as [minDpr, maxDpr] and internally does Math.min(Math.max(window.devicePixelRatio, min), max) ā so a phone reporting a device pixel ratio of 3 still only renders at 2x, capping the GPU workload while a standard 1x display still renders at its native resolution.
Without this clamp, ultra-high-density devices would force the GPU to shade several times more fragments per frame than a 1x display, which is a common, easy-to-miss cause of dropped frame rate on flagship phones specifically.
<Canvas dpr={[1, 2]}>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
11Renderer Fundamentals Complete
Congratulations! You now understand the Holy Trinity: Scene, Camera, and Renderer. Next, we will start building the actual objects that populate the Scene!
With all three pieces in place ā a Scene holding your objects, a Camera defining the viewpoint, and a Renderer turning that pairing into pixels every frame ā you have everything needed to display anything in Three.js. Every more advanced feature (materials, lighting, post-processing) builds on top of this same render(scene, camera) call.
From here, the natural next step is Geometries and Meshes: the actual 3D shapes that get added to the Scene for the Renderer to draw.
// šØ Renderer activated!3D Scene rendered. Objects: 4, Draw Calls: Optimized.
12Step-by-Step Breakdown
The final piece of the Holy Trinity is the Renderer. This is the machine that takes your Scene and your Camera and paints it onto the screen.
The Renderer's job is extremely complex: it evaluates all geometry, materials, lights, and shadows, and calculates the final pixel colors.
What HTML element does the WebGLRenderer create and append to the DOM to display the graphics?
- ācanvas
- ādomElement
- āelement
If you just call renderer.render(scene, camera) once, you get a static image. To get animations, we need a Render Loop.
What browser API is used to create a smooth, 60-FPS render loop?
- āsetInterval
- āsetTimeout
- ārequestAnimationFrame
The renderer also handles antialiasing. Antialiasing smooths out the jagged, pixelated edges of 3D models.
In React Three Fiber, you don't instantiate the renderer yourself. The <Canvas> component creates a WebGLRenderer with antialias: true by default!
Look at this high-resolution sphere. The renderer is doing the heavy lifting to ensure the edges are perfectly smooth using MSAA (Multisample Anti-Aliasing).
Another critical setting on the renderer is pixel ratio. High-DPI screens (like Mac Retinas) pack multiple physical pixels into one logical pixel.
Why do we pass an array [1, 2] to the dpr prop in React Three Fiber?
- āTo double the speed
- āTo clamp the pixel ratio between 1 and 2 for performance
- āTo render the scene twice
Congratulations! You now understand the Holy Trinity: Scene, Camera, and Renderer. Next, we will start building the actual objects!
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1The Rendered Canvas Is Invisible to Assistive Technology
Everything the WebGLRenderer draws lands inside a single <canvas> element with no DOM structure inside it ā screen readers see one opaque node, not the objects, text, or state within the 3D scene. Give the canvas (or its wrapping container) a descriptive aria-label, and expose any interactive or informational content that matters via real DOM elements alongside the canvas.
<canvas aria-label="3D product viewer showing a rotating sneaker" role="img"></canvas>SEO Implications
- 1
Renderer Output Is Never Indexed by Search Engines
WebGL draw calls produce raster pixels on a canvas, not crawlable DOM or text ā no matter how the renderer is configured, none of it is visible to Googlebot. All SEO value for a page built around a Three.js renderer must come from surrounding HTML: headings, descriptive text, and metadata rendered outside the canvas.
Best Practices
Always Clean Up the Renderer on Unmount
A WebGLRenderer holds a real WebGL context, which is a limited browser resource (most browsers cap concurrent contexts around 8-16). Call renderer.dispose() and remove the canvas when a component unmounts, or navigating between pages that each create a renderer will eventually throw 'Too many active WebGL contexts' errors.
Match Renderer Options to Your Performance Target
antialias: true and a high clamped pixel ratio look great but cost real GPU time. For mobile-first or many-simultaneous-scene use cases, disable antialias and clamp dpr to [1, 1.5] rather than defaulting to the highest-fidelity settings everywhere.
Frequent Bugs
The canvas renders at a tiny, squished 300x150 size, or looks correct but blurry on Retina displays.
Call renderer.setSize(width, height) to match your container, and renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) to render at native resolution without over-shading on very high-DPI screens.
Real-World Examples
Resizable Product Configurator Canvas
An e-commerce 3D product viewer needs to resize its renderer whenever the browser window or its container changes, otherwise the model looks stretched after a layout shift.
window.addEventListener('resize', () => {
const { clientWidth, clientHeight } = container;
camera.aspect = clientWidth / clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(clientWidth, clientHeight);
});