Skip to content

Commit

Permalink
✨ continue running scheduled tasks when page is hidden
Browse files Browse the repository at this point in the history
  • Loading branch information
astoilkov committed Feb 12, 2024
1 parent ab7a52f commit bb4366a
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 7 deletions.
14 changes: 7 additions & 7 deletions src/WorkCycleTracker.ts
Original file line number Diff line number Diff line change
@@ -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()
}
Expand All @@ -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<void> {
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()
}
}
Expand Down
32 changes: 32 additions & 0 deletions src/utils/waitHiddenTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import waitNextTask from './waitNextTask'
import withResolvers from './withResolvers'

const state = {
scheduled: false,
hiddenTask: withResolvers(),
}

export default async function waitHiddenTask(): Promise<void> {
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 })
}
28 changes: 28 additions & 0 deletions src/utils/waitNextTask.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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
}

0 comments on commit bb4366a

Please sign in to comment.