Three.js Cheatsheet

Resizing and Responsiveness

Use this Three.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Core Resize Handler

function onResize() {
  const w = window.innerWidth;
  const h = window.innerHeight;

  // Update camera
  camera.aspect = w / h;
  camera.updateProjectionMatrix(); // REQUIRED after changing aspect/fov/near/far

  // Update renderer
  renderer.setSize(w, h);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}

window.addEventListener('resize', onResize);

Always call camera.updateProjectionMatrix() after changing aspect, fov, near, or far. Forgetting this is the most common resize bug.

Canvas-Fit Patterns

Full Window (most common)

// CSS
// body { margin: 0; overflow: hidden; }
// canvas { display: block; }

renderer.setSize(window.innerWidth, window.innerHeight);

Fit a Container div

const container = document.getElementById('canvas-container');

function onResize() {
  const w = container.clientWidth;
  const h = container.clientHeight;

  camera.aspect = w / h;
  camera.updateProjectionMatrix();

  renderer.setSize(w, h);
}

const resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(container);

// Initial size
onResize();

Fixed Aspect Ratio (e.g., always 16:9)

const targetAspect = 16 / 9;

function onResize() {
  let w = window.innerWidth;
  let h = window.innerWidth / targetAspect;

  if (h > window.innerHeight) {
    h = window.innerHeight;
    w = window.innerHeight * targetAspect;
  }

  renderer.setSize(w, h);
  // camera.aspect stays fixed
  camera.aspect = targetAspect;
  camera.updateProjectionMatrix();

  // Center the canvas
  renderer.domElement.style.marginLeft = ((window.innerWidth - w)  / 2) + 'px';
  renderer.domElement.style.marginTop  = ((window.innerHeight - h) / 2) + 'px';
}

Pixel Ratio (DPR / Retina)

// Cap at 2 — above 2 is wasteful on modern displays
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

// Get current DPR
const dpr = renderer.getPixelRatio();

// Physical pixel size of the canvas
const physicalW = Math.floor(window.innerWidth  * dpr);
const physicalH = Math.floor(window.innerHeight * dpr);

renderer.setSize(w, h) automatically factors in the pixel ratio set by setPixelRatio() — the canvas CSS size is w × h, the physical resolution is w*dpr × h*dpr.

Orthographic Camera Resize

// Re-derive the frustum from the current window size
function onResize() {
  const aspect = window.innerWidth / window.innerHeight;
  const frustumH = 10; // keep a fixed world-space height

  camera.left   = -frustumH * aspect / 2;
  camera.right  =  frustumH * aspect / 2;
  camera.top    =  frustumH / 2;
  camera.bottom = -frustumH / 2;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

Render Targets on Resize

// WebGLRenderTarget must be explicitly resized
const rt = new THREE.WebGLRenderTarget(
  window.innerWidth  * window.devicePixelRatio,
  window.innerHeight * window.devicePixelRatio
);

function onResize() {
  const w = window.innerWidth;
  const h = window.innerHeight;
  const dpr = Math.min(window.devicePixelRatio, 2);

  renderer.setSize(w, h);
  renderer.setPixelRatio(dpr);

  rt.setSize(w * dpr, h * dpr); // must match renderer physical size

  camera.aspect = w / h;
  camera.updateProjectionMatrix();
}

Post-Processing (EffectComposer) on Resize

import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass }     from 'three/addons/postprocessing/RenderPass.js';

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));

function onResize() {
  const w = window.innerWidth;
  const h = window.innerHeight;

  camera.aspect = w / h;
  camera.updateProjectionMatrix();

  renderer.setSize(w, h);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  composer.setSize(w, h);   // also resize composer and all passes
  composer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}

Demand Rendering (Only Render on Change)

Save GPU on idle scenes by rendering only when something changes.

let renderRequested = false;

function requestRender() {
  if (!renderRequested) {
    renderRequested = true;
    requestAnimationFrame(render);
  }
}

function render() {
  renderRequested = false;
  renderer.render(scene, camera);
}

// Trigger on control change
controls.addEventListener('change', requestRender);

// Trigger on resize
window.addEventListener('resize', () => {
  onResize();
  requestRender();
});

// Initial render
requestRender();

Fullscreen API

function toggleFullscreen() {
  if (!document.fullscreenElement) {
    renderer.domElement.requestFullscreen();
  } else {
    document.exitFullscreen();
  }
}

// Handle resize when entering/exiting fullscreen
document.addEventListener('fullscreenchange', onResize);

// Keyboard shortcut
window.addEventListener('keydown', (e) => {
  if (e.code === 'KeyF') toggleFullscreen();
});

Checking Renderer Size

const size = new THREE.Vector2();
renderer.getSize(size);
console.log(size.x, size.y); // CSS size in logical pixels

// Physical size (factoring DPR)
const physW = Math.round(size.x * renderer.getPixelRatio());
const physH = Math.round(size.y * renderer.getPixelRatio());

ResizeObserver vs window resize

// window resize fires for any window size change
window.addEventListener('resize', onResize);

// ResizeObserver fires for a specific element (preferred for canvas in a div)
const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
    renderer.setSize(width, height);
  }
});
observer.observe(renderer.domElement.parentElement);

Common Responsive Gotchas

Forgot updateProjectionMatrix() — the most common bug. Camera projection is cached and does not update automatically.

Canvas is 0×0 — if the parent element has no explicit size (e.g., height: 0 or no defined block context), clientWidth / clientHeight return 0. Ensure the container has a defined height via CSS.

Blurry canvas — forgetting setPixelRatio() or setting it to 1 on a Retina/HiDPI display makes the canvas render at half resolution.

DPR > 2 wastes GPU — cap at 2. Math.min(window.devicePixelRatio, 2) is the standard pattern.

Render target size mismatch — post-processing composers and render targets must be resized manually (they don't track the renderer size automatically).

Fullscreen on iOSrequestFullscreen() is not supported on iOS Safari. Use webkitRequestFullscreen or handle it differently for mobile.

> Debounce heavy resize operations — if resizing triggers expensive work (rebuilding textures, etc.), debounce the handler:

let resizeTimer;
window.addEventListener('resize', () => {
  clearTimeout(resizeTimer);
  resizeTimer = setTimeout(onResize, 100);
});