Animation and Performance APIs
1. requestIdleCallback for Background Tasks
| Feature | API | Browser Support | Use Case |
|---|---|---|---|
| requestIdleCallback() | requestIdleCallback(callback, options) |
Modern browsers | Schedule low-priority work |
| IdleDeadline | deadline.timeRemaining() |
Returns time left in idle period | Check available time |
| didTimeout | deadline.didTimeout |
Boolean flag | Timeout exceeded check |
| cancelIdleCallback() | cancelIdleCallback(handle) |
Cancel pending callback | Cleanup scheduled work |
| timeout option | { timeout: 2000 } |
Force execution after delay | Maximum wait time |
Example: requestIdleCallback polyfill
// requestIdleCallback polyfill
(function() {
if ('requestIdleCallback' in window) {
return; // Native support
}
var requestIdleCallback = function(callback, options) {
var start = Date.now();
var timeout = (options && options.timeout) || 1;
return setTimeout(function() {
callback({
didTimeout: false,
timeRemaining: function() {
// Estimate 50ms frame budget
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
};
var cancelIdleCallback = function(id) {
clearTimeout(id);
};
window.requestIdleCallback = requestIdleCallback;
window.cancelIdleCallback = cancelIdleCallback;
})();
// Usage example
function processLargeDataset(items) {
var index = 0;
function processChunk(deadline) {
// Process items while time remains
while (index < items.length &&
(deadline.timeRemaining() > 0 || deadline.didTimeout)) {
processItem(items[index]);
index++;
// Process at least one item per callback
if (deadline.didTimeout) break;
}
// Schedule next chunk if more items remain
if (index < items.length) {
requestIdleCallback(processChunk, { timeout: 1000 });
} else {
console.log('Processing complete');
}
}
function processItem(item) {
// Simulate work
console.log('Processing:', item);
}
// Start processing
requestIdleCallback(processChunk, { timeout: 1000 });
}
// Process 1000 items during idle time
processLargeDataset(Array.from({ length: 1000 }, (_, i) => i));
Example: Enhanced polyfill with frame budget tracking
// Enhanced requestIdleCallback with better timing
var IdleCallbackPolyfill = (function() {
var channel = null;
var frameDeadline = 0;
var pendingTimeout = null;
var idleCallbackId = 0;
var idleCallbacks = new Map();
// Use MessageChannel for better timing
if (typeof MessageChannel !== 'undefined') {
channel = new MessageChannel();
channel.port1.onmessage = function() {
var currentTime = performance.now();
var didTimeout = false;
// Process all pending callbacks
idleCallbacks.forEach(function(callback, id) {
var timeRemaining = Math.max(0, frameDeadline - currentTime);
if (timeRemaining > 0 || didTimeout) {
callback({
didTimeout: didTimeout,
timeRemaining: function() {
return Math.max(0, frameDeadline - performance.now());
}
});
idleCallbacks.delete(id);
}
});
};
}
function scheduleIdleCallback() {
// Aim for 50ms per frame (60fps = 16.67ms, idle = 50ms)
frameDeadline = performance.now() + 50;
if (channel) {
channel.port2.postMessage(null);
} else {
setTimeout(function() {
processIdleCallbacks(false);
}, 0);
}
}
function processIdleCallbacks(didTimeout) {
var currentTime = performance.now();
idleCallbacks.forEach(function(callback, id) {
callback({
didTimeout: didTimeout,
timeRemaining: function() {
return Math.max(0, frameDeadline - performance.now());
}
});
idleCallbacks.delete(id);
});
}
function requestIdleCallback(callback, options) {
var timeout = (options && options.timeout) || Infinity;
var id = ++idleCallbackId;
idleCallbacks.set(id, callback);
// Schedule callback
scheduleIdleCallback();
// Handle timeout
if (timeout < Infinity) {
setTimeout(function() {
if (idleCallbacks.has(id)) {
var cb = idleCallbacks.get(id);
idleCallbacks.delete(id);
cb({
didTimeout: true,
timeRemaining: function() { return 0; }
});
}
}, timeout);
}
return id;
}
function cancelIdleCallback(id) {
idleCallbacks.delete(id);
}
return {
requestIdleCallback: requestIdleCallback,
cancelIdleCallback: cancelIdleCallback
};
})();
// Apply polyfill if needed
if (!('requestIdleCallback' in window)) {
window.requestIdleCallback = IdleCallbackPolyfill.requestIdleCallback;
window.cancelIdleCallback = IdleCallbackPolyfill.cancelIdleCallback;
}
Note: requestIdleCallback is useful for non-critical tasks like
analytics, preloading, and background processing. Polyfills approximate idle time but don't match native
precision.
2. Web Animations API (element.animate) Polyfills
| Feature | API | Browser Support | Polyfill |
|---|---|---|---|
| element.animate() | element.animate(keyframes, options) |
Modern browsers | web-animations-js |
| Animation object | animation.play(), pause(), cancel() |
Control playback | Polyfill provides full API |
| Keyframes | Array or object syntax | Define animation states | CSS @keyframes fallback |
| Timing options | duration, easing, delay, iterations |
Animation configuration | requestAnimationFrame |
| playbackRate | animation.playbackRate = 2 |
Speed control | Adjust timing in polyfill |
| finished Promise | animation.finished.then() |
Completion callback | Custom Promise implementation |
Example: Basic Web Animations API polyfill
// Simple Web Animations API polyfill (basic features)
(function() {
if ('animate' in Element.prototype) {
return; // Native support
}
var Animation = function(element, keyframes, options) {
this.element = element;
this.keyframes = this._normalizeKeyframes(keyframes);
this.options = this._normalizeOptions(options);
this.startTime = null;
this.currentTime = 0;
this.playState = 'idle';
this.playbackRate = 1;
this._rafId = null;
this._finishedPromise = null;
};
Animation.prototype._normalizeKeyframes = function(keyframes) {
// Convert object format to array format
if (!Array.isArray(keyframes)) {
var keys = Object.keys(keyframes);
return keys.map(function(key) {
var frame = {};
frame[key] = keyframes[key];
return frame;
});
}
return keyframes;
};
Animation.prototype._normalizeOptions = function(options) {
if (typeof options === 'number') {
return { duration: options };
}
return {
duration: options.duration || 0,
delay: options.delay || 0,
easing: options.easing || 'linear',
iterations: options.iterations || 1,
fill: options.fill || 'auto'
};
};
Animation.prototype.play = function() {
if (this.playState === 'running') return;
this.playState = 'running';
this.startTime = performance.now() - this.currentTime;
this._animate();
return this;
};
Animation.prototype.pause = function() {
if (this.playState !== 'running') return;
this.playState = 'paused';
this.currentTime = performance.now() - this.startTime;
if (this._rafId) {
cancelAnimationFrame(this._rafId);
this._rafId = null;
}
};
Animation.prototype.cancel = function() {
this.playState = 'idle';
this.currentTime = 0;
this.startTime = null;
if (this._rafId) {
cancelAnimationFrame(this._rafId);
this._rafId = null;
}
// Reset to first frame
this._applyFrame(0);
};
Animation.prototype.finish = function() {
this.currentTime = this.options.duration * this.options.iterations;
this._applyFrame(1);
this.playState = 'finished';
if (this._finishedResolve) {
this._finishedResolve(this);
}
};
Animation.prototype._animate = function() {
var self = this;
this._rafId = requestAnimationFrame(function() {
if (self.playState !== 'running') return;
var elapsed = (performance.now() - self.startTime) * self.playbackRate;
var duration = self.options.duration;
var iterations = self.options.iterations;
var totalDuration = duration * iterations;
if (elapsed >= totalDuration) {
self.finish();
return;
}
// Calculate progress (0 to 1)
var progress = (elapsed % duration) / duration;
progress = self._applyEasing(progress, self.options.easing);
self._applyFrame(progress);
self.currentTime = elapsed;
self._animate();
});
};
Animation.prototype._applyFrame = function(progress) {
var keyframes = this.keyframes;
if (keyframes.length === 0) return;
// Simple two-keyframe interpolation
var startFrame = keyframes[0];
var endFrame = keyframes[keyframes.length - 1];
for (var prop in endFrame) {
if (endFrame.hasOwnProperty(prop)) {
var startValue = this._parseValue(startFrame[prop] || '0');
var endValue = this._parseValue(endFrame[prop]);
var currentValue = startValue + (endValue - startValue) * progress;
if (prop === 'opacity' || prop === 'scale') {
this.element.style[prop] = currentValue;
} else {
this.element.style[prop] = currentValue + 'px';
}
}
}
};
Animation.prototype._parseValue = function(value) {
if (typeof value === 'number') return value;
return parseFloat(value) || 0;
};
Animation.prototype._applyEasing = function(progress, easing) {
// Simple easing functions
switch(easing) {
case 'ease-in':
return progress * progress;
case 'ease-out':
return progress * (2 - progress);
case 'ease-in-out':
return progress < 0.5
? 2 * progress * progress
: -1 + (4 - 2 * progress) * progress;
case 'linear':
default:
return progress;
}
};
Object.defineProperty(Animation.prototype, 'finished', {
get: function() {
if (!this._finishedPromise) {
var self = this;
this._finishedPromise = new Promise(function(resolve) {
self._finishedResolve = resolve;
});
}
return this._finishedPromise;
}
});
// Polyfill Element.prototype.animate
Element.prototype.animate = function(keyframes, options) {
var animation = new Animation(this, keyframes, options);
animation.play();
return animation;
};
})();
// Usage example
var element = document.getElementById('box');
var animation = element.animate([
{ transform: 'translateX(0px)', opacity: 1 },
{ transform: 'translateX(300px)', opacity: 0.5 }
], {
duration: 1000,
easing: 'ease-in-out',
iterations: Infinity,
direction: 'alternate'
});
// Control animation
animation.pause();
animation.play();
animation.cancel();
// Wait for completion
animation.finished.then(function() {
console.log('Animation finished');
});
Warning: Full Web Animations API polyfill is complex. Use the
official
web-animations-js polyfill for production. This example shows basic concepts only.
3. PerformanceObserver and Performance Timeline API
| API | Method | Browser Support | Use Case |
|---|---|---|---|
| PerformanceObserver | new PerformanceObserver(callback) |
Modern browsers | Monitor performance entries |
| observe() | observer.observe({ entryTypes: ['measure'] }) |
Start observing | Specify entry types to watch |
| Entry Types | navigation, resource, mark, measure, paint | Different metrics | Track specific events |
| performance.mark() | performance.mark('start') |
Create timestamp | Mark points in time |
| performance.measure() | performance.measure('task', 'start', 'end') |
Calculate duration | Time between marks |
| getEntries() | performance.getEntries() |
Get all entries | Retrieve performance data |
Example: PerformanceObserver polyfill
// PerformanceObserver polyfill (basic implementation)
(function() {
if ('PerformanceObserver' in window) {
return; // Native support
}
var PerformanceObserver = function(callback) {
this.callback = callback;
this.entryTypes = [];
this.buffered = false;
this._entries = [];
};
PerformanceObserver.prototype.observe = function(options) {
this.entryTypes = options.entryTypes || [];
this.buffered = options.buffered || false;
// Get existing entries if buffered
if (this.buffered) {
var existingEntries = this._getExistingEntries();
if (existingEntries.length > 0) {
this._notify(existingEntries);
}
}
// Start monitoring
this._startMonitoring();
};
PerformanceObserver.prototype.disconnect = function() {
this.entryTypes = [];
};
PerformanceObserver.prototype._getExistingEntries = function() {
var entries = [];
this.entryTypes.forEach(function(type) {
var typeEntries = performance.getEntriesByType(type);
entries = entries.concat(typeEntries);
});
return entries;
};
PerformanceObserver.prototype._startMonitoring = function() {
var self = this;
// Poll for new entries
this._pollInterval = setInterval(function() {
var newEntries = self._getNewEntries();
if (newEntries.length > 0) {
self._notify(newEntries);
}
}, 100);
};
PerformanceObserver.prototype._getNewEntries = function() {
var allEntries = this._getExistingEntries();
var newEntries = [];
allEntries.forEach(function(entry) {
if (this._entries.indexOf(entry) === -1) {
newEntries.push(entry);
this._entries.push(entry);
}
}.bind(this));
return newEntries;
};
PerformanceObserver.prototype._notify = function(entries) {
var list = {
getEntries: function() { return entries; },
getEntriesByType: function(type) {
return entries.filter(function(e) { return e.entryType === type; });
},
getEntriesByName: function(name, type) {
return entries.filter(function(e) {
return e.name === name && (!type || e.entryType === type);
});
}
};
this.callback(list, this);
};
// Polyfill mark and measure if needed
if (!performance.mark) {
var marks = {};
performance.mark = function(name) {
marks[name] = performance.now();
};
performance.measure = function(name, startMark, endMark) {
var startTime = marks[startMark] || 0;
var endTime = marks[endMark] || performance.now();
var duration = endTime - startTime;
// Store measure entry
var entry = {
name: name,
entryType: 'measure',
startTime: startTime,
duration: duration
};
if (!performance._measures) {
performance._measures = [];
}
performance._measures.push(entry);
return entry;
};
performance.getEntriesByType = function(type) {
if (type === 'measure') {
return performance._measures || [];
}
return [];
};
}
window.PerformanceObserver = PerformanceObserver;
})();
// Usage example
var observer = new PerformanceObserver(function(list) {
list.getEntries().forEach(function(entry) {
console.log('Performance entry:', entry.name, entry.duration + 'ms');
});
});
observer.observe({ entryTypes: ['measure', 'mark'] });
// Measure performance
performance.mark('task-start');
// Do some work
setTimeout(function() {
performance.mark('task-end');
performance.measure('task-duration', 'task-start', 'task-end');
}, 1000);
Note: PerformanceObserver is useful for real user monitoring
(RUM). Track metrics like First Contentful Paint (FCP), Largest Contentful Paint (LCP), and custom
timings.
4. IntersectionObserver and ResizeObserver Performance
| Observer | Use Case | Performance Impact | Optimization |
|---|---|---|---|
| IntersectionObserver | Visibility detection | Low (native optimization) | Use rootMargin for early trigger |
| ResizeObserver | Element size changes | Low (batched by browser) | Debounce callbacks if needed |
| MutationObserver | DOM changes | Medium (can be expensive) | Limit subtree scope |
| Scroll events | Scroll position | High (frequent firing) | Use IntersectionObserver instead |
| Window resize | Viewport size | High (frequent firing) | Use ResizeObserver + debounce |
Example: Performance-optimized IntersectionObserver usage
// Optimized lazy loading with IntersectionObserver
var LazyLoader = {
observer: null,
init: function(options) {
options = options || {};
// Create observer with optimized settings
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
root: options.root || null,
rootMargin: options.rootMargin || '50px', // Load before visible
threshold: options.threshold || 0.01 // Trigger at 1% visibility
}
);
// Find all lazy-loadable elements
var elements = document.querySelectorAll('[data-lazy-src]');
elements.forEach(function(element) {
this.observer.observe(element);
}.bind(this));
},
handleIntersection: function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
this.loadElement(entry.target);
observer.unobserve(entry.target); // Stop observing after load
}
}.bind(this));
},
loadElement: function(element) {
var src = element.getAttribute('data-lazy-src');
if (!src) return;
if (element.tagName === 'IMG') {
element.src = src;
} else {
element.style.backgroundImage = 'url(' + src + ')';
}
element.removeAttribute('data-lazy-src');
element.classList.add('loaded');
},
disconnect: function() {
if (this.observer) {
this.observer.disconnect();
}
}
};
// Initialize lazy loading
LazyLoader.init({
rootMargin: '100px', // Start loading 100px before entering viewport
threshold: 0.01
});
// HTML usage:
// <img data-lazy-src="image.jpg" alt="Lazy loaded image">
Example: Optimized ResizeObserver with debouncing
// Performance-optimized resize handling
var ResponsiveHandler = {
observer: null,
handlers: new Map(),
debounceTimers: new Map(),
init: function() {
if (!('ResizeObserver' in window)) {
console.warn('ResizeObserver not supported');
return;
}
this.observer = new ResizeObserver(this.handleResize.bind(this));
},
observe: function(element, callback, options) {
options = options || {};
var debounce = options.debounce || 0;
// Store handler
this.handlers.set(element, {
callback: callback,
debounce: debounce
});
// Start observing
this.observer.observe(element);
},
handleResize: function(entries) {
entries.forEach(function(entry) {
var handler = this.handlers.get(entry.target);
if (!handler) return;
if (handler.debounce > 0) {
// Debounce callback
this.debouncedCallback(entry, handler);
} else {
// Immediate callback
handler.callback(entry);
}
}.bind(this));
},
debouncedCallback: function(entry, handler) {
var element = entry.target;
// Clear existing timer
if (this.debounceTimers.has(element)) {
clearTimeout(this.debounceTimers.get(element));
}
// Set new timer
var timer = setTimeout(function() {
handler.callback(entry);
this.debounceTimers.delete(element);
}.bind(this), handler.debounce);
this.debounceTimers.set(element, timer);
},
unobserve: function(element) {
if (this.observer) {
this.observer.unobserve(element);
}
this.handlers.delete(element);
if (this.debounceTimers.has(element)) {
clearTimeout(this.debounceTimers.get(element));
this.debounceTimers.delete(element);
}
},
disconnect: function() {
if (this.observer) {
this.observer.disconnect();
}
this.handlers.clear();
this.debounceTimers.forEach(function(timer) {
clearTimeout(timer);
});
this.debounceTimers.clear();
}
};
// Initialize
ResponsiveHandler.init();
// Observe element with debouncing
var container = document.getElementById('container');
ResponsiveHandler.observe(container, function(entry) {
console.log('Container resized:', entry.contentRect.width, 'x', entry.contentRect.height);
// Adjust layout based on size
if (entry.contentRect.width < 600) {
container.classList.add('mobile');
} else {
container.classList.remove('mobile');
}
}, { debounce: 150 }); // Debounce by 150ms
Warning: Avoid polling or scroll events for visibility detection. IntersectionObserver provides better performance with native browser optimization.
5. requestAnimationFrame and cancelAnimationFrame
| Pattern | Technique | Benefit | Use Case |
|---|---|---|---|
| Read/Write Separation | Batch DOM reads, then writes | Avoid layout thrashing | Multiple element updates |
| Frame Budgeting | Limit work per frame | Maintain 60fps | Heavy computations |
| Cancellation | cancelAnimationFrame when done | Stop unnecessary loops | Conditional animations |
| Throttling | Skip frames when needed | Reduce CPU usage | Non-critical animations |
| Transform/Opacity | Use GPU-accelerated properties | Better performance | All animations |
| will-change | Hint browser about changes | Prepare for animation | Complex animations |
Example: Read/Write batching to avoid layout thrashing
// FastDOM-style read/write batching
var FastDOM = (function() {
var reads = [];
var writes = [];
var scheduled = false;
function scheduleFlush() {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(flush);
}
function flush() {
scheduled = false;
// Run all reads first
var readsCopy = reads.slice();
reads = [];
readsCopy.forEach(function(read) {
read();
});
// Then run all writes
var writesCopy = writes.slice();
writes = [];
writesCopy.forEach(function(write) {
write();
});
// Schedule next flush if more pending
if (reads.length || writes.length) {
scheduleFlush();
}
}
return {
read: function(callback) {
reads.push(callback);
scheduleFlush();
},
write: function(callback) {
writes.push(callback);
scheduleFlush();
},
clear: function() {
reads = [];
writes = [];
}
};
})();
// Usage - prevents layout thrashing
var boxes = document.querySelectorAll('.box');
// BAD: Causes layout thrashing
boxes.forEach(function(box) {
var height = box.offsetHeight; // Read (triggers layout)
box.style.height = (height + 10) + 'px'; // Write (invalidates layout)
});
// GOOD: Batches reads and writes
boxes.forEach(function(box) {
FastDOM.read(function() {
var height = box.offsetHeight;
FastDOM.write(function() {
box.style.height = (height + 10) + 'px';
});
});
});
Example: Frame budget management
// Frame budget manager for smooth animations
var FrameBudget = {
frameTime: 16.67, // 60fps = 16.67ms per frame
tasks: [],
isRunning: false,
addTask: function(task, priority) {
this.tasks.push({
task: task,
priority: priority || 0
});
// Sort by priority (higher first)
this.tasks.sort(function(a, b) {
return b.priority - a.priority;
});
if (!this.isRunning) {
this.start();
}
},
start: function() {
this.isRunning = true;
this.processFrame();
},
stop: function() {
this.isRunning = false;
},
processFrame: function() {
if (!this.isRunning || this.tasks.length === 0) {
this.isRunning = false;
return;
}
var self = this;
requestAnimationFrame(function(timestamp) {
var frameStart = performance.now();
var frameEnd = frameStart + self.frameTime;
// Process tasks while budget remains
while (self.tasks.length > 0 && performance.now() < frameEnd) {
var item = self.tasks.shift();
try {
item.task();
} catch(e) {
console.error('Task error:', e);
}
}
// Continue to next frame
self.processFrame();
});
}
};
// Usage
function processLargeList(items) {
items.forEach(function(item, index) {
FrameBudget.addTask(function() {
// Process one item
var element = document.createElement('div');
element.textContent = 'Item ' + index;
document.body.appendChild(element);
}, 1); // Priority 1
});
}
// Process 1000 items without blocking
var items = Array.from({ length: 1000 }, (_, i) => i);
processLargeList(items);
Example: GPU-accelerated animations
// Optimized animation using transform and opacity
function animateOptimized(element, from, to, duration) {
var start = null;
var animationId = null;
// Prepare for animation
element.style.willChange = 'transform, opacity';
function step(timestamp) {
if (!start) start = timestamp;
var progress = Math.min((timestamp - start) / duration, 1);
// Use easing
var eased = easeInOutCubic(progress);
// Animate with GPU-accelerated properties
var x = from.x + (to.x - from.x) * eased;
var y = from.y + (to.y - from.y) * eased;
var opacity = from.opacity + (to.opacity - from.opacity) * eased;
// Use transform instead of left/top
element.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
element.style.opacity = opacity;
if (progress < 1) {
animationId = requestAnimationFrame(step);
} else {
// Cleanup
element.style.willChange = 'auto';
}
}
function easeInOutCubic(t) {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
}
animationId = requestAnimationFrame(step);
// Return cancel function
return function cancel() {
if (animationId) {
cancelAnimationFrame(animationId);
element.style.willChange = 'auto';
}
};
}
// Usage
var box = document.getElementById('box');
var cancelAnimation = animateOptimized(
box,
{ x: 0, y: 0, opacity: 1 },
{ x: 300, y: 200, opacity: 0.5 },
1000
);
// Cancel if needed
// cancelAnimation();
Key Takeaways - Performance & Animation
- requestIdleCallback: Schedule low-priority work during idle periods
- Web Animations API: Use native API or web-animations-js polyfill
- PerformanceObserver: Monitor real user metrics (RUM) for optimization
- IntersectionObserver: Preferred over scroll events for visibility
- Frame budgeting: Batch reads/writes, maintain 60fps with RAF
- GPU acceleration: Use transform/opacity, avoid layout properties