How to Optimize Performance for Large Numbers of Animated Elements in Anime.js
Optimize performance in Anime.js by leveraging staggered start times to distribute workload across frames, maintaining the default millisecond time unit to keep the tick threshold low, and batching transform updates while letting the engine auto-pause on hidden tabs.
When animating thousands of DOM elements simultaneously, the JavaScript execution cost can quickly bottleneck the main thread. The Anime.js library provides specific architectural optimizations to handle massive animation workloads efficiently, as implemented in juliangarnier/anime.
Understanding the Anime.js Engine Architecture
The engine schedules work using requestAnimationFrame in browsers or setImmediate on Node, running the main loop only while active tickables exist. In src/engine/engine.js (lines 44-53), the tick loop manages all active tweens, timelines, and animations on each frame, ensuring work only occurs when necessary.
The engine uses a global tick threshold to avoid unnecessary renders. In src/engine/engine.js (lines 34-37), the globals.tickThreshold is calculated as 200 multiplied by the time unit scale. When the elapsed time jump exceeds this threshold, the engine forces a manual render; otherwise, it skips recomputation to save CPU cycles.
Time Unit Configuration
Keep the default engine.timeUnit = 'ms' (milliseconds) rather than seconds. A larger scale increases the threshold value, forcing more manual renders and reducing performance. The threshold check occurs inside the engine's timing logic to determine whether tween values need recalculation on any given frame.
Batched Rendering and Transform Optimization
In src/core/render.js (lines 66-74), the engine batches CSS transform updates into a single string write. Instead of setting translateX, rotate, and scale separately—which would trigger multiple layout calculations—the engine constructs one compound transform string per element per tick.
Manual Render Control
The render() function in src/core/render.js evaluates whether to update tween values based on elapsed time and the tick threshold. While you can modify globals.tickThreshold directly for sparse update patterns, the default settings optimize for general high-volume usage by skipping renders when time deltas are insignificant.
Distribution Strategies with Stagger
The most effective way to reduce per-frame processing is staggering animation start times. The stagger engine in src/utils/stagger.js (lines 82-100) calculates offset delays and caches the resulting values array on first execution, avoiding O(N) recomputation every frame.
The examples/timeline-50K-stars/index.js demonstrates this approach with 50,000 simultaneous animations. By spreading start times using anime.stagger(), only a fraction of the total tween count processes on any single frame, converting O(N) per-frame work into distributed work across the animation duration.
Engine Configuration for High-Volume Animation
Several global settings impact performance when scaling to thousands of elements.
Precision and DOM Access
Set engine.precision = 3 instead of the default 5 when sub-pixel accuracy isn't required, reducing rounding overhead. Pass DOM element references directly (e.g., document.querySelectorAll('.box')) rather than CSS selectors inside animation loops to avoid repeated registerTargets calls.
Background Tab Optimization
The engine automatically pauses when the document is hidden via engine.pauseOnDocumentHidden = true (default in src/engine/engine.js, line 53). This prevents wasteful computation on inactive tabs without requiring manual visibility API handling.
Additive Animation Mode
Enable additive tweens via additive.update() (see src/animation/additive.js) to let multiple animations affect the same property without additional DOM write overhead. This mode composes values mathematically before the final render pass.
Code Examples
Staggering 10,000 Elements
import { animate, stagger } from 'animejs';
// Create 10,000 elements
const container = document.getElementById('container');
for (let i = 0; i < 10000; i++) {
const el = document.createElement('div');
el.className = 'box';
container.appendChild(el);
}
// Distribute animation starts across frames
animate('.box', {
translateX: 300,
opacity: [0, 1],
easing: 'easeOutQuad',
duration: 2000,
delay: stagger(5) // 5ms offset per element
});
Timeline Batch Processing
import { createTimeline, stagger } from 'animejs';
const tl = createTimeline({ autoplay: true });
tl.add('.star', {
translateY: [-200, 0],
rotate: [0, 360],
duration: 1500,
easing: 'easeOutCubic',
delay: stagger(8)
});
tl.add('.circle', {
scale: [0, 1],
opacity: [0, 1],
duration: 1000,
easing: 'easeOutBack'
}, '-=500');
Engine Precision Tuning
import { engine } from 'animejs';
// Reduce precision for better performance with large counts
engine.precision = 3;
engine.timeUnit = 'ms'; // Maintain low tick threshold (default)
Summary
- Use staggered delays via
stagger()to distribute the O(N) workload across multiple frames rather than processing all elements simultaneously on the first frame. - Maintain the default millisecond time unit to keep
globals.tickThresholdlow (200ms), minimizing forced manual renders insrc/engine/engine.js. - Leverage transform batching by using built-in
translate,rotate, andscaleproperties, whichsrc/core/render.jscombines into single string writes to reduce layout thrashing. - Enable auto-pause on hidden tabs (default
engine.pauseOnDocumentHidden = true) to prevent background processing when users switch applications. - Reduce precision to 3 decimal places when sub-pixel accuracy isn't required to decrease computational overhead per tween.
- Use timelines to batch-update child animations in a single
tick()pass rather than managing individual animation loops separately.
Frequently Asked Questions
How does Anime.js handle 50,000 animated elements without lag?
The library processes only active tweens on each frame. By using stagger() (implemented in src/utils/stagger.js), you offset start times so only a subset of the 50,000 elements calculates per frame. The example in examples/timeline-50K-stars/index.js demonstrates this pattern, while the engine's tick loop in src/engine/engine.js (lines 44-53) skips inactive animations entirely, ensuring O(N) complexity is distributed across the animation duration rather than concentrated on initialization.
What is the tick threshold and how does it affect performance?
The globals.tickThreshold (set in src/engine/engine.js, lines 34-37) determines when the engine forces a manual render based on elapsed time jumps. Calculated as 200 multiplied by the time unit scale, a lower threshold (achieved with millisecond time units) allows the engine to skip unnecessary recomputations when time deltas are small, reducing CPU usage during large-scale animations by avoiding redundant tween calculations.
Should I use individual animations or timelines for thousands of elements?
Use timelines for batch control. According to src/core/render.js (lines 341-382), timelines process all child animations in a single pass during tick(), whereas individual animations require separate loop iterations. For massive element counts, timelines reduce overhead by centralizing the update logic and ensuring the engine evaluates all children together rather than through multiple independent entry points.
Does Anime.js automatically optimize when the user switches tabs?
Yes. The engine sets pauseOnDocumentHidden = true by default (see src/engine/engine.js, line 53), automatically pausing the requestAnimationFrame loop when the document visibility changes to hidden. This prevents wasteful computation on background tabs without requiring manual Page Visibility API handling, preserving battery life and CPU resources when the animation is not visible.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →