UNPKG

vanilla-performance-patterns

Version:

Production-ready performance patterns for vanilla JavaScript. Zero dependencies, maximum performance.

345 lines (342 loc) 9.92 kB
/** * vanilla-performance-patterns v0.1.0 * Production-ready performance patterns for vanilla JavaScript. Zero dependencies, maximum performance. * @author [object Object] * @license MIT */ 'use strict'; /** * @fileoverview Advanced debounce and throttle implementations * @author - Mario Brosco <mario.brosco@42rows.com> @company 42ROWS Srl - P.IVA: 18017981004 * @module vanilla-performance-patterns/timing * * Pattern inspired by Lodash with additional features * MaxWait option ensures execution even with continuous input * Leading/trailing edge control for precise timing */ /** * Advanced debounce with maxWait option * * @example * ```typescript * const search = debounce( * async (query: string) => { * const results = await api.search(query); * updateUI(results); * }, * 300, * { * maxWait: 1000, // Force execution after 1s even with continuous input * leading: false, * trailing: true * } * ); * * // Type continuously - will execute after 300ms pause OR 1000ms max * input.addEventListener('input', (e) => search(e.target.value)); * ``` */ function debounce(func, wait, options = {}) { let timeoutId; let maxTimeoutId; let lastCallTime; let lastInvokeTime = 0; let lastArgs; let lastThis; let result; const { leading = false, trailing = true, maxWait } = options; const hasMaxWait = maxWait !== undefined; const maxWaitTime = hasMaxWait ? Math.max(maxWait, wait) : 0; function invokeFunc(time) { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function leadingEdge(time) { // Reset max wait timer lastInvokeTime = time; // Start max wait timer if (hasMaxWait) { maxTimeoutId = window.setTimeout(maxExpired, maxWaitTime); } // Invoke on leading edge if specified if (leading) { result = invokeFunc(time); } } function remainingWait(time) { const timeSinceLastCall = time - (lastCallTime ?? 0); const timeSinceLastInvoke = time - lastInvokeTime; const timeWaiting = wait - timeSinceLastCall; return hasMaxWait ? Math.min(timeWaiting, maxWaitTime - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time) { const timeSinceLastCall = time - (lastCallTime ?? 0); const timeSinceLastInvoke = time - lastInvokeTime; return lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (hasMaxWait && timeSinceLastInvoke >= maxWaitTime); } function timerExpired() { const time = Date.now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart timer timeoutId = window.setTimeout(timerExpired, remainingWait(time)); } function maxExpired() { if (timeoutId !== undefined) { clearTimeout(timeoutId); } maxTimeoutId = timeoutId = undefined; if (trailing && lastArgs) { result = invokeFunc(Date.now()); } else { lastArgs = lastThis = undefined; } } function trailingEdge(time) { timeoutId = undefined; if (trailing && lastArgs) { result = invokeFunc(time); } else { lastArgs = lastThis = undefined; } } function cancel() { if (timeoutId !== undefined) { clearTimeout(timeoutId); } if (maxTimeoutId !== undefined) { clearTimeout(maxTimeoutId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timeoutId = maxTimeoutId = undefined; } function flush() { if (timeoutId === undefined) { return result; } cancel(); if (lastArgs) { result = invokeFunc(Date.now()); } return result; } function pending() { return timeoutId !== undefined; } function debounced(...args) { const time = Date.now(); const isInvoking = shouldInvoke(time); lastArgs = args; lastThis = this; lastCallTime = time; if (isInvoking) { if (timeoutId === undefined) { leadingEdge(time); } else if (hasMaxWait) { // Handle invocations in a tight loop if (timeoutId !== undefined) { clearTimeout(timeoutId); } timeoutId = window.setTimeout(timerExpired, wait); result = invokeFunc(time); } } else if (timeoutId === undefined) { timeoutId = window.setTimeout(timerExpired, wait); } } debounced.cancel = cancel; debounced.flush = flush; debounced.pending = pending; return debounced; } /** * Advanced throttle implementation * * @example * ```typescript * const handleScroll = throttle( * () => { * const scrollY = window.scrollY; * updateParallax(scrollY); * }, * 16, // 60fps * { leading: true, trailing: false } * ); * * window.addEventListener('scroll', handleScroll, { passive: true }); * ``` */ function throttle(func, wait, options = {}) { const { leading = true, trailing = true } = options; return debounce(func, wait, { leading, trailing, maxWait: wait }); } /** * Request animation frame throttle for smooth animations * * @example * ```typescript * const animate = rafThrottle(() => { * element.style.transform = `translateX(${x}px)`; * }); * * slider.addEventListener('input', animate); * ``` */ function rafThrottle(func) { let rafId; let lastArgs; let lastThis; function invokeFunc() { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; rafId = undefined; func.apply(thisArg, args); } function throttled(...args) { lastArgs = args; lastThis = this; if (rafId === undefined) { rafId = requestAnimationFrame(invokeFunc); } } throttled.cancel = () => { if (rafId !== undefined) { cancelAnimationFrame(rafId); rafId = undefined; } lastArgs = lastThis = undefined; }; throttled.flush = () => { if (rafId !== undefined) { cancelAnimationFrame(rafId); invokeFunc(); } return undefined; }; throttled.pending = () => rafId !== undefined; return throttled; } /** * Idle callback throttle for non-critical updates * * @example * ```typescript * const saveAnalytics = idleThrottle(() => { * sendAnalytics(collectedData); * }); * * // Will execute during browser idle time * document.addEventListener('click', saveAnalytics); * ``` */ function idleThrottle(func, options) { let idleId; let lastArgs; let lastThis; function invokeFunc() { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; idleId = undefined; func.apply(thisArg, args); } function throttled(...args) { lastArgs = args; lastThis = this; if (idleId === undefined && 'requestIdleCallback' in window) { idleId = requestIdleCallback(invokeFunc, options); } else if (idleId === undefined) { // Fallback for browsers without requestIdleCallback idleId = window.setTimeout(invokeFunc, 1); } } throttled.cancel = () => { if (idleId !== undefined) { if ('cancelIdleCallback' in window) { cancelIdleCallback(idleId); } else { clearTimeout(idleId); } idleId = undefined; } lastArgs = lastThis = undefined; }; throttled.flush = () => { if (idleId !== undefined) { throttled.cancel(); invokeFunc(); } return undefined; }; throttled.pending = () => idleId !== undefined; return throttled; } function memoize(func, options = {}) { const { keyResolver = (...args) => JSON.stringify(args), maxSize = Infinity, ttl, weak = false } = options; const cache = weak ? new WeakMap() : new Map(); const timestamps = new Map(); function memoized(...args) { const key = weak ? args[0] : keyResolver(...args); // Check cache if (cache.has(key)) { // Check TTL if (ttl && !weak) { const timestamp = timestamps.get(key); if (timestamp && Date.now() - timestamp > ttl) { cache.delete(key); timestamps.delete(key); } else { return cache.get(key); } } else { return cache.get(key); } } // Compute result const result = func.apply(this, args); // Handle cache size limit if (!weak && cache.size >= maxSize) { const firstKey = cache.keys().next().value; cache.delete(firstKey); timestamps.delete(firstKey); } // Store in cache cache.set(key, result); if (ttl && !weak) { timestamps.set(key, Date.now()); } return result; } memoized.cache = cache; return memoized; } exports.debounce = debounce; exports.idleThrottle = idleThrottle; exports.memoize = memoize; exports.rafThrottle = rafThrottle; exports.throttle = throttle; //# sourceMappingURL=index.js.map