Build buttery-smooth, Apple-style image sequence animations on canvas + the tiny redraw hack that saves up to 80% GPU/battery power
Hey there, fellow web dev enthusiasts! ๐๏ธ
Remember those childhood flipbooks where you'd scribble a stick figure on the corner of your notebook pages, flip through them furiously, and suddenly โ bam โ your doodle was dancing?
That's the nostalgic spark behind one of the coolest tricks in modern web design: image sequence animations.
You've seen them on slick landing pages โ those buttery-smooth, scroll-driven "videos" that feel alive as you navigate the site. But here's the plot twist: they're not videos at all. They're just a clever stack of images, orchestrated like a symphony on a <canvas> element.
In this post, we're diving into how these animations work, why they're a game-changer for interactive storytelling, and โ drumroll please ๐ฅ โ a tiny optimization that stops your user's device from chugging power like it's training for a marathon.
Let's flip the page and get started! โจ

Classic flipbook magic โ the inspiration behind modern web wizardry
Quick access (want to play right away?)
โ Live Demo
โ Full source code on GitHub
Now let's get into how this magic actually works...
Chapter 1: The Flipbook Reborn โ What Is Image Sequence Animation?
Picture this: You're on a high-end e-commerce site, scrolling down a product page. As your finger glides, a 3D model spins seamlessly, or a background scene morphs from day to night.
It looks like high-def video, but peek at the network tab โ no MP4 in sight. Instead, it's a barrage of optimized images (usually 100โ200 WebP files) doing the heavy lifting.
At its core, image sequence animation is digital flipbook wizardry:
- Export a video/animation as individual frames
- Preload them into memory as
HTMLImageElementobjects - Drive playback with scroll position (0% = frame 1, 100% = last frame)
- Render the right frame on
<canvas>
Why choose this over <video>?
- ๐ฎ Total control โ perfect sync with scroll, hover, etc.
- โก Lightweight hosting โ images cache beautifully on CDNs, compress with WebP/AVIF
- ๐ No encoding drama โ skip codecs, bitrates, and cross-browser video nightmares
But every hero has a weakness: lots of network requests + heavy repainting = GPU sweat & battery drain on big/retina screens.
We'll fix that soon.
Smooth scroll-triggered image sequence in action
Chapter 2: Behind the Curtain โ How the Magic Happens (With Code!)
Here's the typical flow in a React-ish world (pseudocode โ adapt to vanilla/Vue/Svelte/whatever you love):
// React-style pseudocode โ hook it up to your scroll listener!
const FRAME_COUNT = 192; // Your total frames
const targetFrameRef = useRef(0); // Scroll-driven goal
const currentFrameRef = useRef(0); // Current position
const rafRef = useRef<number | null>(null);
// Update target on scroll (progress: 0-1)
function onScrollChange(progress: number) {
const nextTarget = Math.round(progress * (FRAME_COUNT - 1));
targetFrameRef.current = Math.clamp(nextTarget, 0, FRAME_COUNT - 1); // Assuming a clamp util
}
// The animation loop: Lerp and draw
useEffect(() => {
const tick = () => {
const curr = currentFrameRef.current;
const target = targetFrameRef.current;
const diff = target - curr;
const step = Math.abs(diff) < 0.001 ? 0 : diff * 0.2; // Close the gap by 20% each frame
const next = step === 0 ? target : curr + step;
currentFrameRef.current = next;
drawFrame(Math.round(next)); // Render the frame
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, []);
function drawFrame(index: number) {
const ctx = canvasRef.current?.getContext('2d');
if (!ctx) return;
// Clear, fill background, and draw image with contain-fit
const img = preloadedImages[index];
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ...aspect ratio calculations and drawImage() here...
}
Elegant, right? But here's the villain...
The Plot Twist: Idle Repaints = Battery Vampires ๐งโโ๏ธ
When users pause to read copy or admire the product, that requestAnimationFrame loop keeps churning 60 times per secondโฆ redrawing the exact same frame over and over.
On high-DPI/4K retina screens?
โ Massive canvas clears
โ Repeated image scaling & smoothing
โ Constant GPU compositing
The result: laptop fans kick into overdrive, the device heats up, and battery life tanks fast.
I've seen (and measured) this in real projects โ idle GPU/CPU spikes that turn a "premium" experience into a power hog. Time for the hero upgrade!
Here are real before/after screenshots from my own testing using Chrome DevTools with Frame Rendering Stats enabled (GPU memory + frame rate overlay visible):
Before: ~15.6 MB GPU idle
After: ~2.4 MB GPU idle
| Before Optimization | After Optimization |
|---|---|
![]() |
![]() |
| Idle state with constant repaints โ 15.6 MB GPU memory used | Idle state post-hack โ only 2.4 MB GPU memory used |
See the difference?
- Before: ~15.6 MB GPU memory in idle โ heavy, wasteful repainting
- After: ~2.4 MB GPU memory โ zen-like efficiency
This tiny check eliminates redundant drawImage() calls and can drop idle GPU usage by up to 80% in heavy canvas scenarios (your mileage may vary based on resolution, DPR, and image size).
Pro tip: Enable Paint flashing (green highlights) + Frame Rendering Stats in DevTools โ scroll a bit, then pause. Watch the green flashes disappear and GPU stats stabilize after applying the fix.
Battery saved = happier users + longer sessions ๐โก
Chapter 3: The Hero's Hack โ Redraw Only When It Matters
Super simple fix: track the last drawn frame index and skip drawImage() if nothing changed.
useEffect(() => {
let prevFrameIndex = Math.round(currentFrameRef.current);
const tick = () => {
// ... same lerp logic ...
currentFrameRef.current = next;
const nextFrameIndex = Math.round(next);
// โ
The magic line โ
if (nextFrameIndex !== prevFrameIndex) {
drawFrame(nextFrameIndex);
prevFrameIndex = nextFrameIndex;
}
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => cancelAnimationFrame(rafRef.current!);
}, []);
Why this feels like a superpower
- Scrolling โ still buttery-smooth (draws only when needed)
- Idle โ zen mode (just cheap math, no GPU pain)
- Real-world wins โ up to 80% less idle GPU usage in my tests
Pro tip: Use Paint Flashing + Performance tab in DevTools to see the difference yourself.
Try it yourself!
Here's a minimal, production-ready demo you can fork and play with:
โ Live Demo
โ Full source code on GitHub
Extra Twists: Level Up Your Animation Game
- โ๏ธ DPR Clamp โ cap
devicePixelRatioat 2 - ๐ผ๏ธ Smart contain-fit drawing (calculate once)
- ๐ WebP/AVIF + CDN caching
- ๐ IntersectionObserver +
document.hiddenโ pause when out of view - ๐ผ Smart preloading โ prioritize first visible frames
The Grand Finale: Flipbooks for the Future
Image sequence animations are the unsung heroes of immersive web experiences โ turning static pages into interactive stories without video baggage.
With this tiny redraw check, you're building cool and efficient experiences. Your users (and their batteries) will thank you.
Got questions, your own hacks, or want to share a project? Drop them in the comments โ let's geek out together! ๐
Happy coding & happy low-power animating! โก





Top comments (0)