Performance and Animation Polyfills

1. requestIdleCallback Polyfill Implementation

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 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 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. Intersection and Resize Observer 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 Optimization Patterns

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