vanilla-performance-patterns
Version:
Production-ready performance patterns for vanilla JavaScript. Zero dependencies, maximum performance.
345 lines (342 loc) • 9.92 kB
JavaScript
/**
* vanilla-performance-patterns v0.1.0
* Production-ready performance patterns for vanilla JavaScript. Zero dependencies, maximum performance.
* @author [object Object]
* @license MIT
*/
;
/**
* @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