Skip to content

Commit

Permalink
refactor: adjust the fallback logic (#2)
Browse files Browse the repository at this point in the history
Removes the Idle Callback fallback and uses timeouts directly.

Adds the queueMicrotask as a fallback for user-blocking tasks, since it yields and queues the callback with the highest priority (in the micro task and not macro task queue).
  • Loading branch information
danieleloscozzese authored Jan 11, 2025
1 parent 4ed87c1 commit 3c524c2
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 88 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: "Test"
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- "main"

# Only allow one workflow run at a time per PR
concurrency:
Expand All @@ -26,4 +29,4 @@ jobs:

- name: "Test the package"
run: npm cit --quiet
timeout-minutes: 5
timeout-minutes: 5
7 changes: 4 additions & 3 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ on:
types: ["created"]

jobs:
build:
test:
name: "Test"
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
Expand All @@ -19,7 +20,8 @@ jobs:
timeout-minutes: 5

publish-npm:
needs: build
name: "Publish to npm"
needs: ["test"]
permissions:
contents: read
id-token: write
Expand All @@ -30,7 +32,6 @@ jobs:
with:
node-version: 22
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}}
4 changes: 3 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.github

.prettierrc.json
.github
.prettierignore
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
README.md
44 changes: 28 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
# post-task

A pre-configured progressively-enhancement utility function based on the Scheduler API.
A pre-configured progressively-enhancement utility function based on the
[Scheduler API](https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API).

If the scheduler API is available, use it.
If not, but `requestIdleCallback` is, then use that. Otherwise set a timeout.
If the Scheduler API is available, use it. Otherwise set a timeout as a
fallback.

The Scheduler API relies on browser heuristics, while the fallbacks wait and
call back (in the case of `requestIdleCallback`, as soon as possible in idle time
and definitely at the timeout, for `setTimeout` only at the timeout).

If not in a browser environment, call back immediately.
The Scheduler API relies on browser heuristics, while the fallback waits and
calls back only after a certain time has passed.

The tasks all return a `Promise<void>` since the `scheduler.postTask` returns
one, but without any return value.
one.

The interface re-exposes the values accepted for the
[`scheduler.postTask` API](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask)
and forwards them through when that API is available.

The fallbacks are configured as following:

| Priority | Timeout delay (ms) |
| ----------------- | ------------------ |
| `"user-blocking"` | 0 |
| `"user-visible"` | 0 |
| `"background"` | 150 |

The API is preconfigured with defaults to schedule tasks, based on the
[`scheduler.postTask` API](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask).
There is one exception: if a priority of `"user-blocking"` is passed, and the
Scheduler API is not available, the fallback will be
[`queueMicrotask`](https://developer.mozilla.org/en-US/docs/Web/API/Window/queueMicrotask)
if that function
[is available, which it usually will be, including in Node.js](https://developer.mozilla.org/en-US/docs/Web/API/Window/queueMicrotask#browser_compatibility).

This function is useful for breaking up chunks of work and freeing the main
thread, particularly important when focusing on the
This function is useful for breaking up chunks of work and allowing the event
loop to cycle, which is particularly important when focusing on the
[Interaction to Next Paint](https://web.dev/articles/inp)
web vital.
web vital and of course the smooth interaction which it tries to measure.

## Use

Expand All @@ -29,12 +42,11 @@ import postTask from "post-task";

// ...
postTask(() => {
trackEvent("something-happened");
trackEvent("something-happened");
}, "background");
```

## Formats

This package is equally available as ESM and CJS and has a single, default
export.
The code is identical between the formats except on the exporting itself.
13 changes: 11 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@

declare module "post-task" {
/**
* Queues an arbitrary task to be executed in the browser, with the given priority.
* Allows breaking up the work of potentially long-running tasks to avoid blocking the main thread.
* Queues an arbitrary task to be scheduled for execution with the given
* priority.
*
* Allows the discrete and prioritised queuing of tasks which if run serially
* would block the main thread, but which do not have to be run immediately.
*
* @param task The callback to be executed.
* @param priority The priority of the task, following the
* Scheduler API.
* @returns A promise that resolves when the task is executed,
* in case it needs to be tracked.
*/
export default function postTask(
task: () => void,
Expand Down
122 changes: 62 additions & 60 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,74 +4,76 @@
/**
* The priority of the task: these are the priorities of the Scheduler API.
* @typedef {('background' | 'user-visible' | 'user-blocking')} SchedulerPriority
*/

/** @typedef {Record<SchedulerPriority, number>} PriorityConfigurationFallback */

/**
* The timeouts used for requestIdleCallback, which define the maximum time the task can be delayed.
* The task will be executed as soon as possible, in idle time, but guaranteed within the timeout.
* @type {PriorityConfigurationFallback}
*/
const priorityIdleTimeouts = Object.create(null, {
background: { value: 1000, enumerable: true },
"user-visible": { value: 100, enumerable: true },
"user-blocking": { value: 50, enumerable: true },
});

/**
* The timeouts used for setTimeout, which define the delay before the task is executed.
* @type {PriorityConfigurationFallback}
*
* The timeouts used for `setTimeout`, which define the minimum delay in
* milliseconds before the callback will be executed.
* @type {Record<SchedulerPriority, number>}
*/
const priorityCallbackDelays = Object.create(null, {
background: { value: 150, enumerable: true },
"user-visible": { value: 0, enumerable: true },
"user-blocking": { value: 0, enumerable: true },
/**
* A 150ms duration defines a "long task" for the Web Vitals.
* Waiting at least that long will give a better chance for long queues of
* work to be broken up.
*
* The task will then be scheduled as part of the next event loop, but
* ideally without directly adding to congestion if the CPU is busy.
*/
background: { value: 150 },
/**
* User-visible tasks are scheduled immediately, and although this is the
* same timeout as for `"user-blocking"`, the fallback for that case will
* almost always schedule a microtask with the `queueMicrotask` function;
* while the 0ms timeout here will schedule a **macro**task which will run
* with a lower priority in the event loop.
*/
"user-visible": { value: 0 },
/**
* User-blocking callbacks are scheduled immediately, mirroring the
* user-visible case as a fallback for the unlikely case that
* `queueMicrotask` is not available.
*/
"user-blocking": { value: 0 },
});

/** @typedef {() => void} Task */

/**
* Queues an arbitrary task to be executed in the browser, with the given priority.
* Allows breaking up the work of potentially long-running tasks to avoid blocking the main thread.
* @param {Task} task The callback to be executed.
* @param {SchedulerPriority} priority The priority of the task, following the Scheduler API.
* @returns {Promise<void>} A promise that resolves when the task is executed, in case it needs to be tracked.
* Queues an arbitrary task to be scheduled for execution with the given
* priority.
*
* Allows the discrete and prioritised queuing of tasks which if run serially
* would block the main thread, but which do not have to be run immediately.
*
* @param {() => void} task The callback to be executed.
* @param {SchedulerPriority} priority The priority of the task, following the
* Scheduler API.
* @returns {Promise<void>} A promise that resolves when the task is executed,
* in case it needs to be tracked.
*/
const postTask = (task, priority) => {
if (typeof window !== "undefined") {
// Prefer to use the Scheduler API, if available.
if ("scheduler" in window) {
return scheduler.postTask(task, {
priority,
});
}
// Otherwise, if possible, queue the tracking in browser idle time.
else if ("requestIdleCallback" in window) {
return new Promise((resolve) => {
requestIdleCallback(
() => {
task();
resolve();
},
{ timeout: priorityIdleTimeouts[priority] },
);
});
}
// Otherwise set a timeout with the appropriate delay
else {
return new Promise((resolve) => {
setTimeout(() => {
task();
resolve();
}, priorityCallbackDelays[priority]);
// Prefer to use the Scheduler API, if available.
if ("scheduler" in globalThis) {
return globalThis.scheduler.postTask(task, {
priority,
});
}
// Otherwise, if available and for user-blocking tasks,
// use the native `queueMicrotask`.
else if (priority === "user-blocking" && "queueMicrotask" in globalThis) {
return new Promise((resolve) => {
globalThis.queueMicrotask(() => {
task();
resolve();
});
}
} else {
// On Node.js, just run the task immediately.
// This should be an edge case, but we will not suppress tasks.
task();
return Promise.resolve();
});
}
// Otherwise, and always for the lower priorities on Node.js where the
// Scheduler API is not available, set a timeout with the appropriate delay.
else {
return new Promise((resolve) => {
globalThis.setTimeout(() => {
task();
resolve();
}, priorityCallbackDelays[priority]);
});
}
};

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "post-task",
"version": "1.1.5",
"version": "1.2.0",
"description": "A polyfill for the Scheduler API with a pre-configured progressively-enhanced function helps to split long-running tasks into chunks.",
"type": "module",
"exports": {
Expand All @@ -9,7 +9,7 @@
"require": "./index.cjs"
},
"scripts": {
"test": "echo 'no tests yet'",
"test": "node --check index.mjs",
"prepublishOnly": "sed 's/export default/module.exports =/g' ./index.mjs > index.cjs"
},
"repository": {
Expand All @@ -22,7 +22,10 @@
"INP",
"yield"
],
"author": "Daniel Arthur Gallagher <daniel.gallagher@adevinta.com>",
"author": {
"name": "Daniel Arthur Gallagher",
"email": "daniel.gallagher@adevinta.com"
},
"license": "MIT",
"bugs": {
"url": "/~https://github.com/adevinta/post-task/issues"
Expand Down

0 comments on commit 3c524c2

Please sign in to comment.