Skip to content

perf(Grainient): split useEffect, pause RAF when offscreen/tab hidden#908

Open
mohamed-younes16 wants to merge 1 commit intoDavidHDev:mainfrom
mohamed-younes16:feat/grainient-performance
Open

perf(Grainient): split useEffect, pause RAF when offscreen/tab hidden#908
mohamed-younes16 wants to merge 1 commit intoDavidHDev:mainfrom
mohamed-younes16:feat/grainient-performance

Conversation

@mohamed-younes16
Copy link

perf(Grainient): eliminate WebGL teardown on prop changes + pause RAF when offscreen

Problem

Two performance issues discovered during real-world usage:

1. Full WebGL context teardown on every prop change

The entire component lived inside a single useEffect with all 22 props in the
dependency array. Changing any prop triggered:

  • cancelAnimationFrame → RAF stopped
  • container.removeChild(canvas) → canvas torn out of the DOM
  • new Renderer(...) → fresh WebGL context allocated on the GPU
  • new Program(...) → shader recompiled and re-uploaded

A full GPU pipeline rebuild for something as minor as tweaking a color value.
This is most noticeable in the interactive demo — every slider drag caused a
visible flicker and a full context reset.

2. RAF loop ran unconditionally — even when completely offscreen

requestAnimationFrame fired 60 times/sec regardless of visibility. The
fragment shader (running noise(), sin(), mix(), pow() on every pixel,
every frame) kept burning GPU cycles even when scrolled off screen or in a
background tab. In practice: audible fan spin-up when Grainient is used as a
section background on a scrollable page.

Fix

Split into two useEffects:

  • Effect 1 ([] deps) — creates renderer, canvas, program, mesh exactly once for the component lifetime
  • Effect 2 (prop deps) — only writes to uniform .value properties. Zero GPU cost, no teardown, no flicker

A WeakMap<HTMLDivElement, GrainientCtx> bridges the two effects without strong references that would leak on unmount.

Pause RAF when not needed:

  • IntersectionObserver (threshold: 0) — stops the loop the moment the canvas leaves the viewport, resumes when it re-enters
  • visibilitychange — stops the loop when the tab is hidden, resumes when the user returns

Result

The interactive demo now updates every parameter (colors, warp, grain, etc.) instantly and completely flicker-free — no context reset, no canvas remount, just a uniform value write. GPU usage drops to zero when the component is offscreen.

  • All 4 variants updated (JS-CSS, JS-TW, TS-CSS, TS-TW). Build passes ✓
  • Not A breaking change.

Problems in the original:
- Single useEffect with all 22 props as dependencies caused a full WebGL
  context teardown and canvas remount on every prop change — GPU pipeline
  rebuilt from scratch for something as minor as a color tweak.
- requestAnimationFrame ran unconditionally at 60fps even when the element
  was scrolled completely offscreen, burning GPU cycles with no visible output.
- No awareness of browser tab visibility — shader kept executing even in
  background tabs.

Changes (applied to all 4 variants: JS-CSS, JS-TW, TS-CSS, TS-TW):

1. Split into two useEffects:
   - Effect 1 ([] deps): creates renderer, canvas, geometry, program, mesh
     exactly once for the lifetime of the component.
   - Effect 2 (prop deps): writes directly to uniform values — zero GPU cost,
     no context recreation, no canvas remount.

2. WeakMap<HTMLDivElement, GrainientCtx> bridges the two effects without
   creating strong references that would leak on unmount.

3. IntersectionObserver (threshold: 0) pauses the RAF loop the moment the
   canvas scrolls offscreen and resumes when it re-enters the viewport.

4. visibilitychange listener pauses the RAF loop when the browser tab is
   hidden and resumes when the user returns to it.

Result: no unnecessary GPU work, dramatically lower CPU/GPU usage on pages
where the component is not in view, and instant prop updates without flicker.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant