diff --git a/src/WorkCycleTracker.ts b/src/WorkCycleTracker.ts index 864aec1..1b62ae7 100644 --- a/src/WorkCycleTracker.ts +++ b/src/WorkCycleTracker.ts @@ -1,16 +1,17 @@ import ricTracker from './ricTracker' import frameTracker from './frameTracker' import SchedulingStrategy from './SchedulingStrategy' +import waitHiddenTask from './utils/waitHiddenTask' export default class WorkCycleTracker { #workCycleStart: number = -1 - startTracking() { + startTracking(): void { ricTracker.start() frameTracker.start() } - requestStopTracking() { + requestStopTracking(): void { ricTracker.stop() frameTracker.requestStop() } @@ -20,15 +21,14 @@ export default class WorkCycleTracker { return !isInputPending && this.#calculateDeadline(strategy) - Date.now() > 0 } - async nextWorkCycle(strategy: SchedulingStrategy) { - if (strategy === 'interactive') { - await frameTracker.waitAfterFrame() - } else if (strategy === 'smooth') { - await frameTracker.waitAfterFrame() + async nextWorkCycle(strategy: SchedulingStrategy): Promise { + if (strategy === 'interactive' || strategy === 'smooth') { + await Promise.race([frameTracker.waitAfterFrame(), waitHiddenTask()]) } else if (strategy === 'idle') { if (ricTracker.available) { await ricTracker.waitIdleCallback() } else { + // todo: use waitHiddenTask() with a timeout await frameTracker.waitAfterFrame() } } diff --git a/src/utils/waitHiddenTask.ts b/src/utils/waitHiddenTask.ts new file mode 100644 index 0000000..6df7ffb --- /dev/null +++ b/src/utils/waitHiddenTask.ts @@ -0,0 +1,32 @@ +import waitNextTask from './waitNextTask' +import withResolvers from './withResolvers' + +const state = { + scheduled: false, + hiddenTask: withResolvers(), +} + +export default async function waitHiddenTask(): Promise { + if (document.visibilityState === 'hidden') { + await waitNextTask() + + // in theory, here the page could have been hidden again, + // but we ignore this case on purpose + } else { + if (!state.scheduled) { + state.scheduled = true + state.hiddenTask = withResolvers() + onVisibilityChange(() => { + // events, are already in a new task, so we don't need to call `waitNextTask()` + state.scheduled = false + state.hiddenTask.resolve() + }) + } + + return state.hiddenTask.promise + } +} + +function onVisibilityChange(callback: () => void): void { + document.addEventListener('visibilitychange', callback, { once: true }) +} diff --git a/src/utils/waitNextTask.ts b/src/utils/waitNextTask.ts new file mode 100644 index 0000000..275d75d --- /dev/null +++ b/src/utils/waitNextTask.ts @@ -0,0 +1,28 @@ +import withResolvers from './withResolvers' + +const state = { + scheduled: false, + nextTask: withResolvers(), +} + +// same as queueMicrotask() but for tasks +// https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide#tasks_vs._microtasks +// https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth#tasks_vs._microtasks +export default function waitNextTask(): Promise { + if (!state.scheduled) { + state.scheduled = true + state.nextTask = withResolvers() + nextTask(() => { + state.scheduled = false + state.nextTask.resolve() + }) + } + + return state.nextTask.promise +} + +function nextTask(callback: () => void): void { + const channel = new MessageChannel() + channel.port2.postMessage(undefined) + channel.port1.onmessage = callback +}