main-thread-scheduling
Advanced tools
Comparing version 2.0.0 to 3.0.0
import isTimeToYield from './src/isTimeToYield'; | ||
/** | ||
* It either calls `yieldToMainThread()` because there isn't any more time left or it does nothing | ||
* It either calls `yieldControl()` because there isn't any more time left or it does nothing | ||
* if there is more time left. | ||
@@ -13,5 +13,5 @@ * | ||
/** | ||
* Waits for the browser to become idle again in order to resume work. Calling `yieldToMainThread()` | ||
* 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 | ||
* `yieldToMainThread()` will get resolved first. | ||
* `yieldControl()` will get resolved first. | ||
* | ||
@@ -23,3 +23,3 @@ * @param priority {('user-visible' | 'background')} The priority of the task being run. | ||
*/ | ||
export declare function yieldToMainThread(priority: 'background' | 'user-visible'): Promise<void>; | ||
export declare function yieldControl(priority: 'background' | 'user-visible'): Promise<void>; | ||
export { isTimeToYield }; |
14
index.js
import { nextDeferred } from './src/deferred'; | ||
import isTimeToYield from './src/isTimeToYield'; | ||
import yieldToMainThreadBase from './src/yieldToMainThread'; | ||
import yieldControlBase from './src/yieldControl'; | ||
import { cancelPromiseEscape, requestPromiseEscape } from './src/promiseEscape'; | ||
let promiseEscapeId; | ||
/** | ||
* It either calls `yieldToMainThread()` because there isn't any more time left or it does nothing | ||
* It either calls `yieldControl()` because there isn't any more time left or it does nothing | ||
* if there is more time left. | ||
@@ -18,3 +18,3 @@ * | ||
if (isTimeToYield(priority)) { | ||
await yieldToMainThreadBase(priority); | ||
await yieldControlBase(priority); | ||
} | ||
@@ -27,5 +27,5 @@ cancelPromiseEscape(promiseEscapeId); | ||
/** | ||
* Waits for the browser to become idle again in order to resume work. Calling `yieldToMainThread()` | ||
* 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 | ||
* `yieldToMainThread()` will get resolved first. | ||
* `yieldControl()` will get resolved first. | ||
* | ||
@@ -37,5 +37,5 @@ * @param priority {('user-visible' | 'background')} The priority of the task being run. | ||
*/ | ||
export async function yieldToMainThread(priority) { | ||
export async function yieldControl(priority) { | ||
cancelPromiseEscape(promiseEscapeId); | ||
await yieldToMainThreadBase(priority); | ||
await yieldControlBase(priority); | ||
cancelPromiseEscape(promiseEscapeId); | ||
@@ -42,0 +42,0 @@ promiseEscapeId = requestPromiseEscape(() => { |
{ | ||
"name": "main-thread-scheduling", | ||
"version": "2.0.0", | ||
"version": "3.0.0", | ||
"description": "Consistently responsive apps while staying on the main thread", | ||
"license": "MIT", | ||
"repository": "astoilkov/main-thread-scheduling", | ||
"funding": "https://github.com/sponsors/astoilkov", | ||
"author": { | ||
@@ -8,0 +9,0 @@ "name": "Antonio Stoilkov", |
@@ -37,8 +37,17 @@ <br> | ||
The library ensures that: | ||
- the UI doesn't freeze | ||
- the user's computer fan doesn't spin | ||
- it can be easily integrated in an existing code base | ||
The library lets you run computationally heavy tasks on the main thread while ensuring: | ||
- Your app's UI doesn't freeze. | ||
- Your users' computer fans don't spin. | ||
- It can be easily integrated into your existing codebase. | ||
This is accomplished through multiple strategies: | ||
## Use Cases | ||
- You want to turn a synchronous function into a non-blocking asynchronous function. **Avoids UI freezes.** | ||
- You want to render important elements first and less urgent ones second. **Improves perceived performance.** | ||
- You want to run a long background task that doesn't spin the fans after a while. **Avoids bad reputation.** | ||
- You want to run multiple backgrounds tasks that don't degrade your app performance with time. **Prevents death by a thousand cuts.** | ||
## How It Works | ||
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 [IdleDeadline](https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline). | ||
@@ -50,22 +59,19 @@ - Global queue. Multiple tasks are executed one by one so increasing the number of tasks doesn't degrade performance linearly. | ||
## Use Cases | ||
- You want to turn a synchronous function into a non-blocking asynchronous function. Avoids UI freezes. | ||
- You want to yield important results first and less urgent ones second. Improves perceived performance. | ||
- You want to run a background task that doesn't spin the fans. Avoids bad reputation. | ||
- You want to run multiple backgrounds tasks that don't pile up with time. Prevents death by a thousand cuts. | ||
## Why | ||
Why rely on some open-source library to ensure a good performance for my app? | ||
- Not a weekend project. I have been working on this code for months. If you want to dive deeper, you can read the [in-depth](./docs/in-depth.md) doc. | ||
- 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). | ||
- Simple. 90% of the time you only need `yieldOrContinue(priority)` function. The API has two more functions for more advanced cases. | ||
- **Not a weekend project.** I've already been using it for a year in 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. | ||
- **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. | ||
- **Simple.** 90% of the time you only need `yieldOrContinue(priority)` function. The API has two more functions for more advanced cases. | ||
- Aiming for high-quality with [my open-source principles](https://astoilkov.com/my-open-source-principles). | ||
## Example | ||
You can see the library in action in [this CodeSandbox](https://codesandbox.io/s/main-thread-scheduling-example-qqef6?file=/src/App.js:1188-1361). Try removing the call to `yieldToContinue()` and then type in the input to see the difference. | ||
## API | ||
Note: If you want to understand how this library works under the hook and some of the details – read the [in-depth](./docs/in-depth.md) doc. | ||
Note: 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')` | ||
#### `yieldOrContinue(priority: 'background' | 'user-visible')` | ||
@@ -88,4 +94,8 @@ The complexity of the entire library is hidden behind this method. You can have great app performance by calling a single method. | ||
The library has two more functions available: `yieldToMainThread(priority: 'background' | 'user-visible')` and `isTimeToYield(priority: 'background' | 'user-visible')`. These two functions are used together to handle more advanced use cases. | ||
The library has two more functions available: | ||
- `yieldControl(priority: 'background' | 'user-visible')` | ||
- `isTimeToYield(priority: 'background' | 'user-visible')` | ||
These two functions are used together to handle more advanced use cases. | ||
A simple use case where you will need those two functions is when you want to render your view before yielding back control to the browser to continue its work: | ||
@@ -97,3 +107,3 @@ ```ts | ||
render() | ||
await yieldToMainThread('user-visible') | ||
await yieldControl('user-visible') | ||
} | ||
@@ -110,3 +120,3 @@ | ||
- `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 50ms – this gives you a nice cycle of doing heavy work and letting the browser render pending changes. | ||
- `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. | ||
@@ -117,6 +127,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). | ||
- Web Workers | ||
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. | ||
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 I don't know their timeline. | ||
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. | ||
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. |
@@ -24,5 +24,5 @@ declare type Deferred = { | ||
* Resolve the last task in the queue. This triggers executing the task by resolving the promise | ||
* inside `yieldToMainThread()` function. | ||
* inside `yieldControl()` function. | ||
*/ | ||
export declare function nextDeferred(): void; | ||
export {}; |
@@ -43,3 +43,3 @@ import { startTrackingPhases, stopTrackingPhases } from './phaseTracking'; | ||
* Resolve the last task in the queue. This triggers executing the task by resolving the promise | ||
* inside `yieldToMainThread()` function. | ||
* inside `yieldControl()` function. | ||
*/ | ||
@@ -46,0 +46,0 @@ export function nextDeferred() { |
@@ -1,2 +0,2 @@ | ||
let idlePhase; | ||
let idlePhase | ||
// Why the "stop-requested" status? | ||
@@ -7,41 +7,40 @@ // - we request to stop the tracking of IdlePhase becuase later in a microtask we may call | ||
// 1. `stopTrackingIdlePhase()` is called | ||
// 2. Promise for `yieldToMainThread()` resolves | ||
// 3. `yieldToMainThread()` is called again and `startTrackingIdlePhase()` is called | ||
// 2. Promise for `yieldControl()` resolves | ||
// 3. `yieldControl()` is called again and `startTrackingIdlePhase()` is called | ||
// - we don't want to cancel with `cancelIdleCallback()` because this would make us call | ||
// `requestIdleCallback()` again — this isn't optimal because we can lose free time left in the | ||
// currently running idle callback | ||
let status = 'stopped'; | ||
let status = 'stopped' | ||
export function getIdlePhase() { | ||
return idlePhase; | ||
return idlePhase | ||
} | ||
export function startTrackingIdlePhase() { | ||
if (status === 'running') { | ||
throw new Error('Unreachabe code. This is probably a bug – please log an issue.'); | ||
throw new Error('Unreachabe code. This is probably a bug – please log an issue.') | ||
} | ||
if (status === 'stop-requested') { | ||
status = 'running'; | ||
return; | ||
status = 'running' | ||
return | ||
} | ||
status = 'running'; | ||
status = 'running' | ||
requestIdleCallback((idleDeadline) => { | ||
if (status === 'stop-requested') { | ||
status = 'stopped'; | ||
idlePhase = undefined; | ||
} | ||
else { | ||
status = 'stopped' | ||
idlePhase = undefined | ||
} else { | ||
idlePhase = { | ||
deadline: idleDeadline, | ||
start: performance.now(), | ||
}; | ||
} | ||
// setting status to "stopped" so calling startTrackingIdlePhase() doesn't throw | ||
status = 'stopped'; | ||
startTrackingIdlePhase(); | ||
status = 'stopped' | ||
startTrackingIdlePhase() | ||
} | ||
}); | ||
}) | ||
} | ||
export function stopTrackingIdlePhase() { | ||
if (status === 'stopped' || status === 'stop-requested') { | ||
throw new Error('Unreachabe code. This is probably a bug – please log an issue.'); | ||
throw new Error('Unreachabe code. This is probably a bug – please log an issue.') | ||
} | ||
status = 'stop-requested'; | ||
status = 'stop-requested' | ||
} |
/** | ||
* Determines if it's time to call `yieldToMainThread()`. | ||
* Determines if it's time to call `yieldControl()`. | ||
*/ | ||
export default function isTimeToYield(priority: 'background' | 'user-visible'): boolean; |
@@ -8,3 +8,3 @@ // #performance | ||
/** | ||
* Determines if it's time to call `yieldToMainThread()`. | ||
* Determines if it's time to call `yieldControl()`. | ||
*/ | ||
@@ -11,0 +11,0 @@ export default function isTimeToYield(priority) { |
@@ -41,4 +41,4 @@ // #hack | ||
// 1. `stopTrackingIdlePhase()` is called | ||
// 2. Promise for `yieldToMainThread()` resolves | ||
// 3. `yieldToMainThread()` is called again and `startTrackingIdlePhase()` is called | ||
// 2. Promise for `yieldControl()` resolves | ||
// 3. `yieldControl()` is called again and `startTrackingIdlePhase()` is called | ||
// - we don't want to cancel with `cancelIdleCallback()` because this would make us call | ||
@@ -84,6 +84,6 @@ // `requestIdleCallback()` again — this isn't optimal because we can lose free time left in the | ||
var _a, _b, _c; | ||
const idlePhaseStart = (_a = idlePhaseTracker.getPhase()) === null || _a === void 0 ? void 0 : _a.start; | ||
const animationFramePhaseStart = (_b = animationFrameTracker.getPhase()) === null || _b === void 0 ? void 0 : _b.start; | ||
const deadline = (_a = idlePhaseTracker.getPhase()) === null || _a === void 0 ? void 0 : _a.deadline; | ||
const idlePhaseStart = (_b = idlePhaseTracker.getPhase()) === null || _b === void 0 ? void 0 : _b.start; | ||
const animationFramePhaseStart = (_c = animationFrameTracker.getPhase()) === null || _c === void 0 ? void 0 : _c.start; | ||
const start = animationFramePhaseStart !== null && animationFramePhaseStart !== void 0 ? animationFramePhaseStart : idlePhaseStart; | ||
const deadline = (_c = idlePhaseTracker.getPhase()) === null || _c === void 0 ? void 0 : _c.deadline; | ||
return start === undefined || deadline === undefined ? undefined : { start, deadline }; | ||
@@ -90,0 +90,0 @@ } |
@@ -1,1 +0,1 @@ | ||
export default function yieldToMainThread(priority: 'user-visible' | 'background'): Promise<void>; | ||
export default function yieldControl(priority: 'user-visible' | 'background'): Promise<void> |
@@ -1,15 +0,15 @@ | ||
import waitCallback from './waitCallback'; | ||
import isTimeToYield from './isTimeToYield'; | ||
import requestLaterMicrotask from './requestLaterMicrotask'; | ||
import { createDeferred, isDeferredLast, removeDeferred } from './deferred'; | ||
export default async function yieldToMainThread(priority) { | ||
const deferred = createDeferred(priority); | ||
await schedule(priority); | ||
import waitCallback from './waitCallback' | ||
import isTimeToYield from './isTimeToYield' | ||
import requestLaterMicrotask from './requestLaterMicrotask' | ||
import { createDeferred, isDeferredLast, removeDeferred } from './deferred' | ||
export default async function yieldControl(priority) { | ||
const deferred = createDeferred(priority) | ||
await schedule(priority) | ||
if (!isDeferredLast(deferred)) { | ||
await deferred.ready; | ||
await deferred.ready | ||
if (isTimeToYield(priority)) { | ||
await schedule(priority); | ||
await schedule(priority) | ||
} | ||
} | ||
removeDeferred(deferred); | ||
removeDeferred(deferred) | ||
} | ||
@@ -20,16 +20,16 @@ async function schedule(priority) { | ||
() => waitCallback(requestLaterMicrotask), | ||
() => waitCallback(requestIdleCallback, { | ||
// #WET 2021-06-05T3:07:18+03:00 | ||
// #connection 2021-06-05T3:07:18+03:00 | ||
// call at least once per frame | ||
// asuming 60 fps, 1000/60 = 16.667 | ||
timeout: 16, | ||
}), | ||
]); | ||
} | ||
else { | ||
() => | ||
waitCallback(requestIdleCallback, { | ||
// #WET 2021-06-05T3:07:18+03:00 | ||
// #connection 2021-06-05T3:07:18+03:00 | ||
// call at least once per frame | ||
// asuming 60 fps, 1000/60 = 16.667 | ||
timeout: 16, | ||
}), | ||
]) | ||
} else { | ||
await promiseSequantial([ | ||
() => waitCallback(requestLaterMicrotask), | ||
() => waitCallback(requestIdleCallback), | ||
]); | ||
]) | ||
} | ||
@@ -39,4 +39,4 @@ } | ||
for (const getPromise of getPromises) { | ||
await getPromise(); | ||
await getPromise() | ||
} | ||
} |
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
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
31645
28
570
127