PerformanceJune 2026 · 8 min read

Scroll animation performance.
Native CSS vs JavaScript.

Most scroll animation libraries run JavaScript on every scroll event. svg-scroll-draw takes a different approach: use the browser's native animation-timeline: view()when possible — zero JS, pure compositor — and fall back to a lean JS engine when it's not.

The problem with JS scroll listeners

Traditional scroll animation libraries work by listening to the scroll event, reading the scroll position, and then updating element styles in response. This runs on the main thread, competing with layout, paint, and user input.

Even with passive: true and requestAnimationFrame, any JS on the scroll path adds latency. On low-end mobile, this is the difference between smooth 60fps and visible jank.

The native CSS fast path

CSS Scroll-Driven Animations (animation-timeline: view()) run entirely on the compositor thread. The browser handles the scroll → animation mapping with zero JavaScript involvement. No main thread. No GC pauses. Just the GPU.

Supported in Chrome 115+, Edge 115+, and Firefox 110+ (behind a flag). ~85% of browsers as of 2025.

native-css.css
/* What the browser generates under the hood */
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(32px); }
  to   { opacity: 1; transform: translateY(0); }
}

.animate {
  animation-name:            fadeUp;
  animation-duration:        auto;         /* scroll-driven */
  animation-timing-function: ease-out;
  animation-fill-mode:       both;
  animation-timeline:        view();       /* tied to scroll */
  animation-range:           cover 0% cover 100%;
}

How svg-scroll-draw picks the right engine

svg-scroll-draw checks a set of eligibility criteria on every scrollAnimate call. If all pass, it injects a <style> tag and adds a class — no JS scroll listeners at all.

ConditionNative pathJS engine
Browser supports animation-timeline: view()fallback
axis: "y" (vertical)
No custom scrollContainer
String easing (ease-out etc.)
No callbacks (onEnter, onComplete…)
Default trigger (top bottom → bottom top)
speed: 1
once: false
Safe CSS prop (opacity, transform, color…)
app.js
import { scrollAnimate } from 'svg-scroll-draw';

// ✅ Native CSS path — zero JS scroll listeners
scrollAnimate('#hero', {
  props:  { opacity: [0, 1], transform: ['translateY(32px)', 'translateY(0)'] },
  easing: 'ease-out',
  // native: true is the default
});

// ⚡ Falls back to JS engine — callbacks require main-thread JS
scrollAnimate('#section', {
  props:   { opacity: [0, 1] },
  onEnter: () => nav.setActive('section'),  // needs JS
});

// Force JS engine explicitly
scrollAnimate('#card', {
  props:  { opacity: [0, 1] },
  native: false,
});

The JS engine — when it runs

When the native path isn't eligible, svg-scroll-draw uses an IntersectionObserver to start/stop a requestAnimationFrame loop only while the element is in the viewport. The rAF loop stops completely when the element scrolls out — no wasted work.

CSS property updates are batched per frame via el.style.setProperty() — a single style recalc per element per frame.

Performance tips

  • Stick to native-eligible configsopacity + transform are compositor-only properties. Adding them to native CSS path = zero main thread work. Avoid animating layout-triggering properties (width, height, top, left) even in the JS engine.
  • Use once: true for reveal animationsReveal animations that reverse on scroll back keep the rAF loop running. once: true freezes the final value and stops the loop after completion.
  • prefers-reduced-motion is respected automaticallysvg-scroll-draw checks window.matchMedia("(prefers-reduced-motion: reduce)") and applies the final state immediately — no animation, full accessibility.
  • Trigger windows matterA trigger from top-bottom to bottom-top means the animation is active the entire time the element is in view. Narrow triggers (e.g. top 80% to top 40%) run the rAF loop for less time.