Skip to content

Fix gradient “tearing” on fast scroll by moving body gradient to a composited overlay#389

Open
malbaugh wants to merge 2 commits intobrandur:masterfrom
malbaugh:master
Open

Fix gradient “tearing” on fast scroll by moving body gradient to a composited overlay#389
malbaugh wants to merge 2 commits intobrandur:masterfrom
malbaugh:master

Conversation

@malbaugh
Copy link
Copy Markdown

Closes #388.

Moves the background gradient off <body> and onto a fixed overlay isolated with contain: paint. An inner element (.scroll-gradient-bg__inner) carries the background and is translated each frame (via requestAnimationFrame with DPR-aware rounding). This keeps the gradient in lockstep with content at high scroll velocities and eliminates the visible “tear”.

Why the tear happened

I believe the issue was due to the body’s background being painted on the main thread while page content was scrolled via the compositor. Under load those paths can drift out of phase, causing a seam during fast scrolls.

What this change does

  • Creates a fixed, input-transparent overlay (.scroll-gradient-bg) that we hint should be its own layer.
  • Uses an inner element (.scroll-gradient-bg__inner) that holds the background and is moved by transform: translateY(-scrollY).
  • Copies the body’s computed background onto the overlay, then clears the body’s background to avoid double painting.
  • Ensures content stacks over the background (see note below).
  • Applies updates in requestAnimationFrame and snaps to device pixels to avoid subpixel seams.
  • Handles dynamic changes: ResizeObserver (height recompute + small buffer), MutationObserver for class/theme toggles, and pagehide/pageshow for BFCache.
  • Feature-detects contain and bails cleanly on unsupported browsers.

Performance notes

  • GPU/compositor available: transform updates are composite-only (no layout/repaint) and are synchronized with scroll.
  • CPU-only / limited compositing: Repaints are confined to the overlay thanks to contain: paint. rAF timing + DPR rounding prevent seams. Main-thread work is reduced by removing the body background paint.

Safari rendering note (why the stacking rule is necessary)

WebKit’s accelerated compositing can promote position: fixed elements into their own layers. In Safari, a fixed element with an explicit z-index (even 0) may composite above non-positioned siblings during scrolling, because layer/compositing order can differ from DOM paint order. Without explicit stacking, the overlay can intermittently occlude content or appear to “bleed through.”

Manual verification

Test Result
Load page Overlay initializes; body background disabled; no flicker
Scroll fast Gradient stays aligned; no tearing
Change theme/class (e.g., dark mode) Overlay background updates accordingly
Resize window / content grows Overlay height adjusts; 4px buffer prevents gaps
Navigate away/back (BFCache) Re-initializes cleanly; no artifacts
Disable requestAnimationFrame Falls back to synchronous, DPR-rounded updates; no errors
Disable ResizeObserver Still works; loses auto height optimization
20× CPU slowdown + GPU accel off Could not reproduce tearing in local testing

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.

Screen tearing during fast scrolling when background gradient is applied

2 participants