How to Build Smooth 60 FPS Scroll
This is a mid-length guide for developers who want smooth scrolling and animation without jank. The target is simple: animations should feel stable on normal laptops and high refresh displays.
Why scroll effects become laggy
Most lag comes from doing too much work on each scroll event:
- reading layout repeatedly
- writing layout repeatedly
- animating width/height/top/left every frame
- large paint-heavy effects
When this happens, frames drop and interaction feels heavy.
The core rules
Follow these rules first:
- Animate
transformandopacitywhen possible. - Avoid layout-triggering properties inside frequent loops.
- Keep DOM updates small and targeted.
- Use one timing source for animation state.
- Respect reduced-motion preferences.
These five rules solve most performance issues.
A practical stack: Lenis + GSAP
Use Lenis to normalize scroll behavior and GSAP for animation timelines.
- Lenis: smooths wheel/touch movement.
- GSAP: manages interpolation and easing cleanly.
This combo gives good control while keeping code maintainable.
Progress-based animation model
Do not animate directly from raw pixel values every time. Convert scroll into a normalized progress value:
const maxScroll = window.innerHeight * 0.45;
const progress = Math.max(0, Math.min(1, scrollY / maxScroll));
Then map that 0..1 progress into style transitions. This makes behavior predictable across screen sizes.
Avoid layout thrashing
Bad pattern:
window.addEventListener("scroll", () => {
element.style.width = `${100 - window.scrollY * 0.1}%`;
});
Better pattern:
const x = startX + (endX - startX) * progress;
gsap.to(element, { x, duration: 0.2, overwrite: "auto" });
This keeps animation mostly in compositor-friendly properties.
Use will-change carefully
will-change can help, but overusing it wastes memory. Add it only on frequently animated elements, and remove it when no longer needed.
Good targets:
- hero container
- floating nav
- cursor glow
Bad targets:
- every card
- large static sections
Keep visual effects light
Expensive effects can kill FPS, especially on mobile:
- multiple full-screen blurs
- many infinite shadows
- too many particles
Use lightweight alternatives:
- lower blur radius
- static gradients
- fewer animated layers
IntersectionObserver for reveal animations
Reveal items only when they enter viewport. This reduces startup cost and prevents animating hidden content.
Typical flow:
- Set initial hidden state once.
- Observe cards/sections.
- Animate when intersecting.
- Unobserve after first reveal.
This pattern is both smooth and simple.
Mobile and accessibility guards
Always include:
@media (max-width: ...)simplifications@media (prefers-reduced-motion: reduce)fallback
If user prefers reduced motion, disable non-essential animations. This is both accessibility and performance best practice.
Suggested debugging checklist
When scroll feels slow:
- Check if layout properties are animated every frame.
- Inspect heavy shadows/filters.
- Reduce animated element count.
- Verify no duplicate event listeners.
- Profile long tasks in browser devtools.
Fix one variable at a time and re-test.
Production baseline
Before release, test:
- desktop (60Hz and high refresh)
- Android mid-range device
- iOS Safari
- long page with many cards
Also check battery behavior and heat on mobile.
Final notes
Smooth scrolling is not about adding more animation. It is about controlled animation. Use normalized progress, animate cheap properties, and keep effects focused.
Related reading:
- Simple Dark Theme Design System
- Simple Optimistic UI in Real-Time Apps
- Next.js vs Astro: Easy Comparison
Measure before you optimise
Use the browser performance panel to record an actual scroll sequence. Look for long tasks, repeated style recalculation, image decoding, and paint-heavy layers. Do not assume that the animation library is the problem: a large image, a third-party widget, or an unthrottled resize handler can interrupt an otherwise efficient timeline.
Set a performance budget for the experience. For example, limit the number of simultaneously animated layers and make mobile effects simpler than desktop effects. Re-test after deploying production assets, because development builds rarely reveal the same network and decoding costs. Smooth motion is a product requirement that needs measurement, not just visual taste.
A final implementation check
Before release, write down the expected behaviour for a slow connection, a rejected request, and a refresh during an in-progress action. Then test those cases on a real phone as well as a desktop browser. Keep the visible state, the server response, and the log entry connected by one identifier. This small check catches the gaps that polished demos usually hide: stale information, duplicate actions, unclear loading feedback, and recovery paths that work only on a developer machine. Document the decision, keep the interface honest about pending work, and review the result after the first real users interact with it. Reliable software is usually the result of these deliberate, repeatable details rather than one large technical feature.
For scroll work specifically, record one baseline before changes and compare frame pacing after each adjustment. A smoother average frame rate is useful, but stable frame timing matters just as much: occasional long frames are what users feel as jank. Keep that comparison in the project notes so future visual changes do not quietly undo the performance work.