Orbit
RAWIN Logo
HomeAboutProjectsBlogUsesResumeContact
Orbit
Back to Articles
Architecture Note
FEATURED

Engineering Silky 60 FPS Web Experiences: Beyond Basic CSS Transforms

A deep dive into browser composite layers, avoiding layout reflows, and throttling high-frequency pointer events with requestAnimationFrame.

By Rushan Siddiqui·September 1, 2026·5 min read
#Performance#CSS#Browser Internals
Engineering Silky 60 FPS Web Experiences: Beyond Basic CSS Transforms

Engineering Silky 60 FPS Web Experiences

Maintaining a solid 60 frames per second on modern web applications requires understanding how browsers turn DOM nodes into pixels on screen. In complex user interfaces with cursor reactivity and dynamic particle systems, small rendering mistakes can trigger cascading frame drops.

The Critical Rendering Path

When the browser updates a frame, it passes through three stages:

  1. Layout (Reflow): Calculates geometric coordinates and sizes for every element.
  2. Paint: Fills pixels for colors, text, shadows, and borders.
  3. Composite: Assembles painted layers on the GPU and outputs to the display.

Triggering layout or paint during continuous interactions (such as mouse movement or smooth scrolling) forces the main thread to recalculate hundreds of nodes per frame.

typescript
// Anti-pattern: Reading and writing layout properties in a loop
elements.forEach((el) => {
  const height = el.offsetHeight; // Forces synchronous layout
  el.style.height = (height + 10) + 'px';
});

// Optimized: Partition reads and schedule writes via requestAnimationFrame
requestAnimationFrame(() => {
  elements.forEach((el) => {
    el.style.transform = 'translate3d(0, 10px, 0)';
  });
});

Leveraging GPU Composited Properties

To ensure zero-jank interaction, restrict animated properties strictly to transform and opacity. These bypass both Layout and Paint phases entirely, operating directly on GPU layers.

PropertyLayout PhasePaint PhaseComposite Phase
width / heightTriggersTriggersYes
top / leftTriggersTriggersYes
transformSkipsSkipsGPU Accelerated
opacitySkipsSkipsGPU Accelerated

Handling High-Frequency Pointer Streams

Browser pointermove and mousemove events fire at hardware polling rates (often 120Hz to 1000Hz on gaming mice). Attempting state updates or canvas recalculations on every single event will overwhelm the main thread.

Always decouple input sampling from rendering:

typescript
let pendingX = 0;
let pendingY = 0;
let rafScheduled = false;

window.addEventListener('pointermove', (e) => {
  pendingX = e.clientX;
  pendingY = e.clientY;
  
  if (!rafScheduled) {
    rafScheduled = true;
    requestAnimationFrame(renderFrame);
  }
}, { passive: true });

function renderFrame() {
  rafScheduled = false;
  // Apply transformations using latest captured pointer coordinates
}

By decoupling input ingestion from the browser refresh cycle, animations remain deterministic, fluid, and battery-friendly.

Written by Rushan Siddiqui

RAWIN · DEV LOG
Return