main-thread-scheduling
Advanced tools
Comparing version 5.0.1 to 6.0.0-0
@@ -0,23 +1,4 @@ | ||
import yieldControl from './src/yieldControl'; | ||
import isTimeToYield from './src/isTimeToYield'; | ||
/** | ||
* It either calls `yieldControl()` because there isn't any more time left or it does nothing | ||
* if there is more time left. | ||
* | ||
* @param priority {('user-visible' | 'background')} The priority of the task being run. | ||
* `user-visible` priority will always be resolved first. `background` priority will always be | ||
* resolved second. | ||
* @returns {Promise<void>} The promise that will be resolved when the queue | ||
*/ | ||
export declare function yieldOrContinue(priority: 'background' | 'user-visible'): Promise<void>; | ||
/** | ||
* Waits for the browser to become idle again in order to resume work. Calling `yieldControl()` | ||
* multiple times will create a LIFO(last in, first out) queue – the last call to | ||
* `yieldControl()` will get resolved first. | ||
* | ||
* @param priority {('user-visible' | 'background')} The priority of the task being run. | ||
* `user-visible` priority will always be resolved first. `background` priority will always be | ||
* resolved second. | ||
* @returns {Promise<void>} The promise that will be resolved when the queue | ||
*/ | ||
export declare function yieldControl(priority: 'background' | 'user-visible'): Promise<void>; | ||
export { isTimeToYield }; | ||
import yieldOrContinue from './src/yieldOrContinue'; | ||
export { yieldOrContinue, yieldControl, isTimeToYield }; |
45
index.js
@@ -1,43 +0,4 @@ | ||
import { nextDeferred } from './src/deferred'; | ||
import yieldControl from './src/yieldControl'; | ||
import isTimeToYield from './src/isTimeToYield'; | ||
import yieldControlBase from './src/yieldControl'; | ||
import { cancelPromiseEscape, requestPromiseEscape } from './src/promiseEscape'; | ||
let promiseEscapeId; | ||
/** | ||
* It either calls `yieldControl()` because there isn't any more time left or it does nothing | ||
* if there is more time left. | ||
* | ||
* @param priority {('user-visible' | 'background')} The priority of the task being run. | ||
* `user-visible` priority will always be resolved first. `background` priority will always be | ||
* resolved second. | ||
* @returns {Promise<void>} The promise that will be resolved when the queue | ||
*/ | ||
export async function yieldOrContinue(priority) { | ||
cancelPromiseEscape(promiseEscapeId); | ||
if (isTimeToYield(priority)) { | ||
await yieldControlBase(priority); | ||
} | ||
cancelPromiseEscape(promiseEscapeId); | ||
promiseEscapeId = requestPromiseEscape(() => { | ||
nextDeferred(); | ||
}); | ||
} | ||
/** | ||
* Waits for the browser to become idle again in order to resume work. Calling `yieldControl()` | ||
* multiple times will create a LIFO(last in, first out) queue – the last call to | ||
* `yieldControl()` will get resolved first. | ||
* | ||
* @param priority {('user-visible' | 'background')} The priority of the task being run. | ||
* `user-visible` priority will always be resolved first. `background` priority will always be | ||
* resolved second. | ||
* @returns {Promise<void>} The promise that will be resolved when the queue | ||
*/ | ||
export async function yieldControl(priority) { | ||
cancelPromiseEscape(promiseEscapeId); | ||
await yieldControlBase(priority); | ||
cancelPromiseEscape(promiseEscapeId); | ||
promiseEscapeId = requestPromiseEscape(() => { | ||
nextDeferred(); | ||
}); | ||
} | ||
export { isTimeToYield }; | ||
import yieldOrContinue from './src/yieldOrContinue'; | ||
export { yieldOrContinue, yieldControl, isTimeToYield }; |
{ | ||
"name": "main-thread-scheduling", | ||
"version": "5.0.1", | ||
"version": "6.0.0-0", | ||
"description": "Consistently responsive apps while staying on the main thread", | ||
@@ -50,17 +50,18 @@ "license": "MIT", | ||
"devDependencies": { | ||
"@types/jest": "^26.0.22", | ||
"@typescript-eslint/eslint-plugin": "^4.30.0", | ||
"@typescript-eslint/parser": "^4.30.0", | ||
"@types/jest": "^28.1.1", | ||
"@typescript-eslint/eslint-plugin": "^5.28.0", | ||
"@typescript-eslint/parser": "^5.28.0", | ||
"confusing-browser-globals": "^1.0.10", | ||
"eslint": "^7.16.0", | ||
"eslint-config-strictest": "^0.3.1", | ||
"eslint": "^8.17.0", | ||
"eslint-config-strictest": "^0.4.0", | ||
"eslint-formatter-pretty": "^4.0.0", | ||
"eslint-plugin-promise": "^4.2.1", | ||
"eslint-plugin-unicorn": "^24.0.0", | ||
"jest": "^26.6.3", | ||
"np": "^7.2.0", | ||
"prettier": "^2.0.5", | ||
"ts-jest": "^26.5.4", | ||
"typescript": "^4.4.2" | ||
"eslint-plugin-promise": "^6.0.0", | ||
"eslint-plugin-unicorn": "^42.0.0", | ||
"jest": "^28.1.1", | ||
"jest-environment-jsdom": "^28.1.1", | ||
"np": "^7.6.1", | ||
"prettier": "^2.7.0", | ||
"ts-jest": "^28.0.5", | ||
"typescript": "^4.7.3" | ||
} | ||
} |
@@ -21,4 +21,4 @@ <br> | ||
</a> | ||
<a href="https://www.travis-ci.com/astoilkov/main-thread-scheduling"> | ||
<img src="https://www.travis-ci.com/astoilkov/main-thread-scheduling.svg?branch=master" alt="Build Status" /> | ||
<a href="https://github.com/astoilkov/main-thread-scheduling/actions/workflows/main.yml"> | ||
<img src="https://img.shields.io/github/workflow/status/astoilkov/main-thread-scheduling/CI" alt="Build Status" /> | ||
</a> | ||
@@ -31,3 +31,3 @@ <p> | ||
```shell | ||
```bash | ||
npm install main-thread-scheduling | ||
@@ -52,7 +52,6 @@ ``` | ||
An in-depth overview is available [here](./docs/in-depth-overview.md). These are the main things the library does to do it's magic: | ||
- Stops task execution when user interacts with the UI. Uses `navigator.scheduling.isInputPending()` and fallbacks to using either [`IdleDeadline`](https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline) or [`MessageChannel`](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel). | ||
- Uses `MessageChannel.postMessage()` and `requestIdleCallback()` for scheduling. | ||
- Stops task execution when user interacts with the UI (if `navigator.scheduling.isInputPending()` API is available). | ||
- Global queue. Multiple tasks are executed one by one so increasing the number of tasks doesn't degrade performance linearly. | ||
- Sorts tasks by importance. Sorts by [priority](#priorities) and gives priority to tasks requested later. | ||
- Urgent UI changes are given highest priority possible. Tasks with `user-visible` priority are optimized to deliver smooth UX. | ||
- Considerate about your existing code. Tasks with `background` priority are executed last so there isn't some unexpected work that slows down the main thread after the background task is finished. | ||
@@ -63,3 +62,3 @@ | ||
Why rely on some open-source library to ensure a good performance for my app? | ||
- **Not a weekend project.** I've already been using it for over a year in the core of two of my products — [Nota](https://nota.md) and [iBar](https://ibar.app). If you want to dive deeper, you can read the [in-depth](./docs/in-depth-overview.md) doc. | ||
- **Not a weekend project.** I've already been using it for over a year in the core of two of my products — [Nota](https://nota.md) and [iBar](https://ibar.app). | ||
- **This is the future.** Browsers are probably going to support scheduling tasks on the main thread in the future. Here is the [spec](https://github.com/WICG/scheduling-apis). This library will still be relevant in the future because it provides an easier API. | ||
@@ -75,4 +74,2 @@ - **Simple.** 90% of the time you only need `yieldOrContinue(priority)` function. The API has two more functions for more advanced cases. | ||
If you want to understand how this library works under the hook and some of the details – read the [in-depth](./docs/in-depth-overview.md) doc. | ||
#### `yieldOrContinue(priority: 'background' | 'user-visible')` | ||
@@ -118,5 +115,5 @@ | ||
Currently there are only two priorities available: `background` and `user-visible`: | ||
There are two priorities available: `user-visible` and `background`: | ||
- `user-visible` – use this for things that need to display to the user as fast as possible. Every `user-visible` task is run for 83ms – this gives you a nice cycle of doing heavy work and letting the browser render pending changes. | ||
- `background` – use this for background tasks. Every background task is run for 5ms. | ||
- `user-visible` – use this for things that need to display to the user as fast as possible. Every `user-visible` task is run for 83ms – this gives you a nice cycle of doing heavy work and letting the browser render pending changes. | ||
@@ -127,6 +124,6 @@ If you have a use case for a third priority, you can write in [this issue](https://github.com/astoilkov/main-thread-scheduling/issues/1). | ||
The problem this library solves isn't new. However, I haven't found a library that can solve this problem in a simple manner. [Open an issue](https://github.com/astoilkov/main-thread-scheduling/issues/new) if there is such a library so I can add it here. | ||
**Web Workers** are a great alternative if you have: 1) heavy code (e.g. image processing), 2) something that isn't a task but a process (runs through a big time of the app lifecycle). However, in reality, it's rare to see people using them. That's because they require significant investment of time due to the complexity that can't be avoided when working with CPU threads regardless of the programming language. This library can be used as a gateway before transitioning to Web Workers. In reality, a lot of the times, you would discover the doing it on the main thread is good enough. | ||
Web Workers are a possible alternative. However, in reality, it's rare to see people using them. That's because they require significant investment of time due to the complexity that can't be avoided when working with CPU threads regardless of the programming language. | ||
**[`scheduler.yield()`](https://github.com/WICG/scheduling-apis/blob/main/explainers/yield-and-continuation.md)** will probably land in browsers at some point. However, is `scheduler.yield()` enough? The spec isn't very clear on how it will work exactly so I'm not sure. My guess is that it would be possible go without this library but you will need extra code to do so. That's because you will need to reimplement the `isTimeToYield()` method for which I don't see an alternative in the [spec](https://github.com/WICG/scheduling-apis). | ||
React has an implementation for scheduling tasks – [react/scheduler](https://github.com/facebook/react/tree/3c7d52c3d6d316d09d5c2479c6851acecccc6325/packages/scheduler). They plan to make it more generic but there doesn't seem to be a public roadmap for that. | ||
**[React scheduler](https://github.com/facebook/react/blob/main/packages/scheduler/README.md)** is a similar implementation. They plan to make it more generic (for use outside of React) but there doesn't seem to be a public roadmap for that. |
@@ -1,2 +0,3 @@ | ||
import { startTrackingPhases, stopTrackingPhases } from './phaseTracking'; | ||
import { startTrackingIdleFrames, stopTrackingIdleFrames } from './idleFrameTracking'; | ||
import { startTrackingAnimationFrames, stopTrackingAnimationFrames } from './animationFrameTracking'; | ||
const deferred = []; | ||
@@ -15,3 +16,4 @@ /** | ||
if (deferred.length === 1) { | ||
startTrackingPhases(); | ||
startTrackingIdleFrames(); | ||
startTrackingAnimationFrames(); | ||
} | ||
@@ -34,8 +36,12 @@ return item; | ||
const index = deferred.indexOf(deferredItem); | ||
// istanbul ignore if | ||
if (index === -1) { | ||
throw new Error('Unreachabe code. This is probably a bug – please log an issue.'); | ||
// silentError() | ||
} | ||
deferred.splice(index, 1); | ||
else { | ||
deferred.splice(index, 1); | ||
} | ||
if (deferred.length === 0) { | ||
stopTrackingPhases(); | ||
stopTrackingIdleFrames(); | ||
stopTrackingAnimationFrames(); | ||
} | ||
@@ -42,0 +48,0 @@ } |
@@ -0,6 +1,7 @@ | ||
import { getLastIdleDeadline } from './idleFrameTracking'; | ||
import { getPerFrameScheduleStartTime } from './animationFrameTracking'; | ||
// #performance | ||
// calling `isTimeToYield()` thousand of times is slow. `lastCalls` helps to run logic inside of | ||
// calling `isTimeToYield()` thousand of times is slow. `lastCall` helps to run logic inside of | ||
// `isTimeToYield()` at most 1 per millisecond. | ||
import { getIdlePhase } from './phaseTracking'; | ||
let lastCall = 0; | ||
let lastCallTime = 0; | ||
let lastResult = false; | ||
@@ -12,26 +13,35 @@ /** | ||
var _a, _b; | ||
// #performance, `performance.now()` is around 40% slower. also `Date.now()` is accurate enough | ||
// for our use case | ||
const now = Date.now(); | ||
if (now - lastCall === 0) { | ||
if (now - lastCallTime === 0) { | ||
return lastResult; | ||
} | ||
const idlePhase = getIdlePhase(); | ||
lastCall = now; | ||
lastCallTime = now; | ||
lastResult = | ||
idlePhase === undefined || | ||
now >= calculateDeadline(priority, idlePhase) || | ||
((_b = (_a = navigator.scheduling) === null || _a === void 0 ? void 0 : _a.isInputPending) === null || _b === void 0 ? void 0 : _b.call(_a)) === true; | ||
now >= calculateDeadline(priority) || ((_b = (_a = navigator.scheduling) === null || _a === void 0 ? void 0 : _a.isInputPending) === null || _b === void 0 ? void 0 : _b.call(_a)) === true; | ||
return lastResult; | ||
} | ||
function calculateDeadline(priority, idlePhase) { | ||
var _a; | ||
const maxTime = priority === 'background' | ||
? 5 | ||
: // Math.round(100 - (1000/60)) = Math.round(83,333) = 83 | ||
83; | ||
return ((_a = navigator.scheduling) === null || _a === void 0 ? void 0 : _a.isInputPending) === undefined | ||
? // if `isInputPending()` isn't supported, don't go spend more than the idle deadline is | ||
// suggesting. otherwise, the app couldn't ensure responsiveness | ||
idlePhase.start + Math.min(idlePhase.deadline.timeRemaining(), maxTime) | ||
: // if `isInputPending()` is supported, just give the time it needs based on the priority | ||
idlePhase.start + maxTime; | ||
function calculateDeadline(priority) { | ||
const perFrameScheduleStartTime = getPerFrameScheduleStartTime(); | ||
if (perFrameScheduleStartTime === undefined) { | ||
// silentError() | ||
return -1; | ||
} | ||
switch (priority) { | ||
case 'user-visible': { | ||
// Math.round(100 - (1000/60)) = Math.round(83,333) = 83 | ||
return perFrameScheduleStartTime + 83; | ||
} | ||
case 'background': { | ||
const lastIdleDeadline = getLastIdleDeadline(); | ||
const idleDeadline = lastIdleDeadline === undefined | ||
? Number.MAX_SAFE_INTEGER | ||
: Date.now() + lastIdleDeadline.timeRemaining(); | ||
return Math.min(perFrameScheduleStartTime + 5, idleDeadline); | ||
} | ||
// istanbul ignore next | ||
default: | ||
throw new Error('Unreachable code'); | ||
} | ||
} |
@@ -0,1 +1,11 @@ | ||
/** | ||
* Waits for the browser to become idle again in order to resume work. Calling `yieldControl()` | ||
* multiple times will create a LIFO(last in, first out) queue – the last call to | ||
* `yieldControl()` will get resolved first. | ||
* | ||
* @param priority {('user-visible' | 'background')} The priority of the task being run. | ||
* `user-visible` priority will always be resolved first. `background` priority will always be | ||
* resolved second. | ||
* @returns {Promise<void>} The promise that will be resolved when the queue | ||
*/ | ||
export default function yieldControl(priority: 'user-visible' | 'background'): Promise<void>; |
import nextTask from './nextTask'; | ||
import waitCallback from './waitCallback'; | ||
import isTimeToYield from './isTimeToYield'; | ||
import requestLaterMicrotask from './requestLaterMicrotask'; | ||
import { createDeferred, isDeferredLast, removeDeferred } from './deferred'; | ||
import { notifyScheduleComplete } from './animationFrameTracking'; | ||
import { cancelPromiseEscape, requestPromiseEscape } from './promiseEscape'; | ||
import { createDeferred, isDeferredLast, nextDeferred, removeDeferred } from './deferred'; | ||
let promiseEscapeId; | ||
/** | ||
* Waits for the browser to become idle again in order to resume work. Calling `yieldControl()` | ||
* multiple times will create a LIFO(last in, first out) queue – the last call to | ||
* `yieldControl()` will get resolved first. | ||
* | ||
* @param priority {('user-visible' | 'background')} The priority of the task being run. | ||
* `user-visible` priority will always be resolved first. `background` priority will always be | ||
* resolved second. | ||
* @returns {Promise<void>} The promise that will be resolved when the queue | ||
*/ | ||
export default async function yieldControl(priority) { | ||
cancelPromiseEscape(promiseEscapeId); | ||
const deferred = createDeferred(priority); | ||
@@ -16,27 +29,30 @@ await schedule(priority); | ||
removeDeferred(deferred); | ||
cancelPromiseEscape(promiseEscapeId); | ||
promiseEscapeId = requestPromiseEscape(() => { | ||
nextDeferred(); | ||
}); | ||
} | ||
async function schedule(priority) { | ||
if (typeof requestIdleCallback === 'undefined') { | ||
await waitCallback(requestAnimationFrame); | ||
if (priority === 'user-visible' || typeof requestIdleCallback === 'undefined') { | ||
await waitCallback(requestLaterMicrotask); | ||
await waitCallback(nextTask); | ||
while (true) { | ||
await waitCallback(nextTask); | ||
notifyScheduleComplete(); | ||
if (!isTimeToYield(priority)) { | ||
break; | ||
} | ||
} | ||
} | ||
else if (priority === 'user-visible') { | ||
await waitCallback(requestLaterMicrotask); | ||
await waitCallback(requestIdleCallback, { | ||
// #WET 2021-06-05T3:07:18+03:00 | ||
// #connection 2021-06-05T3:07:18+03:00 | ||
// - call at least once per frame | ||
// - assuming 60 fps, 1000/60 = 16.667 = 16.7 | ||
// - the browser uses around 6ms. the user is left with 10ms: | ||
// https://developer.mozilla.org/en-US/docs/Web/Performance/How_long_is_too_long#:~:text=The%2016.7%20milliseconds%20includes%20scripting%2C%20reflow%2C%20and%20repaint.%20Realize%20a%20document%20takes%20about%206ms%20to%20render%20a%20frame%2C%20leaving%20about%2010ms%20for%20the%20rest. | ||
// - because 9*2 is equal to 18, we are sure the idle callback won't be called more than | ||
// once per frame | ||
timeout: 9, | ||
}); | ||
} | ||
else { | ||
await waitCallback(requestLaterMicrotask); | ||
await waitCallback(requestIdleCallback); | ||
notifyScheduleComplete(); | ||
} | ||
} | ||
async function waitCallback(callback) { | ||
return await new Promise((resolve) => { | ||
callback(() => { | ||
resolve(); | ||
}); | ||
}); | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
25
24261
15
404
1
123
1