fix: pause animation when shader element leaves the viewport#268
Conversation
|
@kimiaShahbaghi is attempting to deploy a commit to the Paper Design Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 23f6ab7. Configure here.
| private parentDevicePixelHeight = 0; | ||
| private devicePixelsSupported = false; | ||
|
|
||
| private intersectionObserver: IntersectionObserver | null = null; |
There was a problem hiding this comment.
Viewport default allows pre-check animation
Medium Severity
Shaders mounted off-screen animate unnecessarily until the IntersectionObserver's first asynchronous callback. This is because isInViewport defaults to true when setSpeed is called during initialization.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 23f6ab7. Configure here.
|
Hey @kimiaShahbaghi |


What and why
Animated shaders ran their
requestAnimationFrameloop continuously, evenwhen the element was completely scrolled out of view. On a page with a
MeshGradienthero section, for example, scrolling down to content meantthe shader kept burning GPU cycles and battery the whole time — the user
could never escape it short of switching tabs.
The library already paused shaders when the browser tab was hidden
(
visibilitychange). This PR extends that same pattern one step further:also pause when the element itself is not visible in the viewport.
Closes #188.
How it works
A single file changed:
packages/shaders/src/shader-mount.ts.An
IntersectionObserveris set up in the constructor alongside the existingResizeObserver. When the element leaves the viewport it callssetCurrentSpeed(0)— the same codepath already used by the tab-visibilitypause. When the element re-enters the viewport it calls
setCurrentSpeed(this.speed)and the animation resumes from exactly whereit left off (
currentFrameis preserved, so there's no visual jump).setSpeed()andhandleDocumentVisibilityChange()are updated to check bothconditions together:
```ts
this.setCurrentSpeed(this.ownerDocument.hidden || !this.isInViewport ? 0 : this.speed);
```
dispose()disconnects the observer to avoid memory leaks.Behaviour summary
speed=0)setFrame()scrubbing off-screenrender()directly)typeof IntersectionObserverguard)Why this approach
automatically, same as the existing
visibilitychangepause.every browser that supports WebGL2.
crossing events, not on every frame.
visibilitychangepause isalready unconditional; this is a direct extension of that design decision.
Note
Low Risk
Single-file behavior change in the render loop; mirrors existing tab-visibility pause and uses a standard browser API with a no-op when unavailable.
Overview
Animated shaders no longer keep their
requestAnimationFrameloop running when the mount element is scrolled off-screen.ShaderMountnow watches intersection with the viewport (using the element’s ownwindowfor iframe/PiP) and pauses the same way as when the tab is hidden.setSpeed,visibilitychange, and the new observer all funnel throughupdateCurrentSpeed, which sets effective speed to 0 when the document is hidden or the element is not intersecting, otherwise restoresthis.speed.currentFrameis unchanged, so animation resumes without a time jump. The observer is disconnected indispose. No new public API.Reviewed by Cursor Bugbot for commit 6f67953. Bugbot is set up for automated code reviews on this repo. Configure here.