diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts index 06d92107a..8f7db01f9 100644 --- a/tracker/tracker/src/main/app/observer/observer.ts +++ b/tracker/tracker/src/main/app/observer/observer.ts @@ -1,4 +1,4 @@ -import { createMutationObserver } from '../../utils.js' +import { createMutationObserver, throttleWithTrailing } from '../../utils.js' import { RemoveNodeAttribute, SetNodeAttributeURLBased, @@ -413,6 +413,11 @@ export default abstract class Observer { this.app.attributeSender.sendSetAttribute(id, name, value) } + private throttledSetNodeData = throttleWithTrailing( + (id, parentElement, data) => this.sendNodeData(id, parentElement, data), + 30 + ); + private sendNodeData(id: number, parentElement: Element, data: string): void { if (hasTag(parentElement, 'style')) { this.app.send(SetCSSDataURLBased(id, data, this.app.getBaseHref())) @@ -570,7 +575,7 @@ export default abstract class Observer { } else if (isTextNode(node)) { // for text node id != 0, hence parentID !== undefined and parent is Element this.app.send(CreateTextNode(id, parentID as number, index)) - this.sendNodeData(id, parent as Element, node.data) + this.throttledSetNodeData(id, parent as Element, node.data) } return true } @@ -591,7 +596,7 @@ export default abstract class Observer { throw 'commitNode: node is not a text' } // for text node id != 0, hence parent is Element - this.sendNodeData(id, parent as Element, node.data) + this.throttledSetNodeData(id, parent as Element, node.data) } return true } @@ -640,5 +645,6 @@ export default abstract class Observer { disconnect(): void { this.observer.disconnect() this.clear() + this.throttledSetNodeData.clear() } } diff --git a/tracker/tracker/src/main/utils.ts b/tracker/tracker/src/main/utils.ts index 4b510ef86..83cd41b73 100644 --- a/tracker/tracker/src/main/utils.ts +++ b/tracker/tracker/src/main/utils.ts @@ -320,3 +320,48 @@ export function simpleMerge(defaultObj: T, givenObj: Partial): T { return result } + +export function throttleWithTrailing( + fn: (key: K, ...args: Args) => void, + interval: number +): ((key: K, ...args: Args) => void) & { clear: () => void } { + const lastCalls = new Map(); + const timeouts = new Map>(); + const lastArgs = new Map(); + + const throttled = function (key: K, ...args: Args) { + const now = Date.now(); + const lastCall = lastCalls.get(key) ?? 0; + const remaining = interval - (now - lastCall); + + lastArgs.set(key, args); + + if (remaining <= 0) { + if (timeouts.has(key)) { + clearTimeout(timeouts.get(key)!); + timeouts.delete(key); + } + lastCalls.set(key, now); + fn(key, ...args); + } else if (!timeouts.has(key)) { + const timeoutId = setTimeout(() => { + lastCalls.set(key, Date.now()); + timeouts.delete(key); + const finalArgs = lastArgs.get(key)!; + fn(key, ...finalArgs); + }, remaining); + timeouts.set(key, timeoutId); + } + }; + + throttled.clear = () => { + for (const timeout of timeouts.values()) { + clearTimeout(timeout); + } + timeouts.clear(); + lastArgs.clear(); + lastCalls.clear(); + }; + + return throttled; +} \ No newline at end of file