perf(Grainient): split useEffect, pause RAF when offscreen/tab hidden#908
Open
mohamed-younes16 wants to merge 1 commit intoDavidHDev:mainfrom
Open
perf(Grainient): split useEffect, pause RAF when offscreen/tab hidden#908mohamed-younes16 wants to merge 1 commit intoDavidHDev:mainfrom
mohamed-younes16 wants to merge 1 commit intoDavidHDev:mainfrom
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
useEffectwith all 22 props in thedependency array. Changing any prop triggered:
cancelAnimationFrame→ RAF stoppedcontainer.removeChild(canvas)→ canvas torn out of the DOMnew Renderer(...)→ fresh WebGL context allocated on the GPUnew Program(...)→ shader recompiled and re-uploadedA 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
requestAnimationFramefired 60 times/sec regardless of visibility. Thefragment 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:[]deps) — creates renderer, canvas, program, mesh exactly once for the component lifetime.valueproperties. Zero GPU cost, no teardown, no flickerA
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-entersvisibilitychange— stops the loop when the tab is hidden, resumes when the user returnsResult
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.