Fixing TextScrollSection Flicker at Extreme Scale (GSAP ScrollTrigger)
I ran into a nasty visual bug in a scroll-driven “sticky text” scene: after scrolling deep down the page once and then reverse-scrolling back, the third screen’s background/text would occasionally flash, show white blocks, or briefly disappear at the moment the text was at its maximum stretch.
This post documents the root cause and a robust fix that keeps the vibe of the animation while eliminating the compositor instability.
Symptoms
- The glitch was easiest to reproduce with the following flow:
- Scroll to near the bottom of the page (long distance).
- Reverse scroll back into the third “sticky text” screen.
- The flicker starts around the point where the third screen is close to its maximum scale.
- The flicker looked like:
- one-frame background flash,
- random white blocks,
- text/background popping out and back.
Root cause (why it flickers)
The original implementation scaled the entire full-screen container (including gradient background + background image + huge text) to a very large factor (e.g. 10×).
When a pinned element is transformed into a massive surface, browsers may:
- promote/demote the element’s layer,
- re-tile and re-rasterize it,
- aggressively reclaim GPU memory after long scroll distance,
- and rebuild the surface on reverse scroll.
At the extreme scale (and especially with pin + scrub), these layer transitions can produce a 1-frame hole (flash) or partial tiles (white blocks).
Fix strategy
Instead of fighting the browser with more “force GPU” hints, the most reliable approach was to reduce the pathological case:
- Cap the maximum scale of the full-screen container
- Reducing from 10× to ~3× avoids huge surfaces and keeps raster tiles in a stable range.
- Start the fade transitions earlier to keep the same drama
- If the container can’t go to 10×, begin fading the gradient/text earlier so the overall “disappear while expanding” feel stays.
- Contain the painted region
- Apply
contain: layout style paintto limit the impact of painting/compositing work.
- Apply
- Stabilize multi-pin pages (order matters)
- Create pinned ScrollTriggers in
useLayoutEffectso layout/spacers are applied before calculating start/end. - Use
refreshPriorityto ensure upper pinned sections refresh before lower ones (Cosmic/Goals), preventing “start/end not moving” style bugs.
- Create pinned ScrollTriggers in
Implementation notes
The fix lives in:
src/components/ui/text-scroll-section.tsx
Key changes:
- The third screen’s scale-up stage uses
MAX_SCALE = 3(instead of 10). - Stage timings were re-mapped to keep the narrative:
- Scale up: 0–60%
- Gradient fade: 20–45%
- Text fade: 40–65%
- Header reveal: 70–90%
contain: layout style paintis applied to the scaled container.- The third screen’s pinned ScrollTrigger is created in
useLayoutEffectand given a higherrefreshPriorityso downstream pinned sections don’t miscalculate their start/end.
Takeaways
- Scaling a pinned, full-viewport element to extreme factors is a common way to hit compositor edge cases.
- If you see flicker only after long scroll distance + reverse scroll, suspect layer recycling.
- The “best fix” is often to avoid creating a gigantic surface, then compensate the visuals with earlier fades and better anchoring.