1. throttle (line 1146-1174)

Before: Used recursive setTimeout calls via unpause, creating nested closures and multiple timer contexts.

After:
- Tracks a single timeoutId to manage timer lifecycle
- Eliminates nested closure creation
- Cleaner timer chain when calls are pending

2. batch (line 1204-1227)

Before: Used throttle internally, inheriting its timer overhead.

After:
- Direct timer management without the throttle wrapper
- Single timeoutId tracked explicitly
- Maintains the same semantics (first item processed immediately, rest batched)

3. batcher (line 1235-1259)

Before:
- Created new timer every time queue went from empty to non-empty
- Had unnecessary async in forEach and await r when r was already type U

After:
- Tracks timeoutId to prevent duplicate timer creation
- Only creates timer when queue is truly empty
- Removed unnecessary async/await in result handling
- More efficient error handling (rejects all items at once if length mismatch)

Performance Benefits:

These changes should significantly reduce timer overhead by:
- Fewer timer objects: Only one active timer per throttled/batched function instead of chains
- Less garbage collection: Fewer closure allocations and intermediate objects
- Better memory usage: Explicit timer ID tracking instead of implicit state in closures
This commit is contained in:
Jon Staab
2025-11-14 15:29:03 -08:00
parent 2675f57ebc
commit de5695339d
+44 -28
View File
@@ -1148,26 +1148,27 @@ export const throttle = <F extends (...args: any[]) => any>(ms: number, f: F) =>
return f
}
let paused = false
let nextArgs: Parameters<F> | undefined
let timeoutId: ReturnType<typeof setTimeout> | undefined
let lastArgs: Parameters<F> | undefined
const unpause = () => {
if (nextArgs) {
f(...nextArgs)
nextArgs = undefined
setTimeout(unpause, ms)
const later = () => {
if (lastArgs !== undefined) {
const args = lastArgs
lastArgs = undefined
f(...args)
timeoutId = setTimeout(later, ms)
} else {
paused = false
timeoutId = undefined
}
}
return (...thisArgs: Parameters<F>) => {
if (!paused) {
f(...thisArgs)
paused = true
setTimeout(unpause, ms)
} else {
nextArgs = thisArgs
return (...args: Parameters<F>) => {
lastArgs = args
if (timeoutId === undefined) {
f(...args)
lastArgs = undefined
timeoutId = setTimeout(later, ms)
}
}
}
@@ -1202,11 +1203,26 @@ export const throttleWithValue = <T>(ms: number, f: () => T) => {
*/
export const batch = <T>(t: number, f: (xs: T[]) => void) => {
const xs: T[] = []
const cb = throttle(t, () => xs.length > 0 && f(xs.splice(0)))
let timeoutId: ReturnType<typeof setTimeout> | undefined
const later = () => {
if (xs.length > 0) {
f(xs.splice(0))
timeoutId = setTimeout(later, t)
} else {
timeoutId = undefined
}
}
return (x: T) => {
const shouldFlush = timeoutId === undefined
xs.push(x)
cb()
if (shouldFlush) {
f(xs.splice(0))
timeoutId = setTimeout(later, t)
}
}
}
@@ -1218,27 +1234,27 @@ export const batch = <T>(t: number, f: (xs: T[]) => void) => {
*/
export const batcher = <T, U>(t: number, execute: (request: T[]) => U[] | Promise<U[]>) => {
const queue: {request: T; resolve: (x: U) => void; reject: (reason?: string) => void}[] = []
let timeoutId: ReturnType<typeof setTimeout> | undefined
const _execute = async () => {
timeoutId = undefined
const items = queue.splice(0)
const results = await execute(items.map(item => item.request))
results.forEach(async (r, i) => {
if (results.length === items.length) {
items[i].resolve(await r)
} else {
items[i].reject("Execute must return a result for each request")
}
})
if (results.length === items.length) {
results.forEach((r, i) => items[i].resolve(r))
} else {
items.forEach(item => item.reject("Execute must return a result for each request"))
}
}
return (request: T): Promise<U> =>
new Promise((resolve, reject) => {
if (queue.length === 0) {
setTimeout(_execute, t)
}
queue.push({request, resolve, reject})
if (timeoutId === undefined) {
timeoutId = setTimeout(_execute, t)
}
})
}