main-thread-scheduling
Advanced tools
Comparing version 1.0.0 to 2.0.0
56
index.js
@@ -1,18 +0,5 @@ | ||
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.isTimeToYield = exports.yieldToMainThread = exports.yieldOrContinue = void 0; | ||
const deferred_1 = require("./src/deferred"); | ||
const isTimeToYield_1 = require("./src/isTimeToYield"); | ||
exports.isTimeToYield = isTimeToYield_1.default; | ||
const yieldToMainThread_1 = require("./src/yieldToMainThread"); | ||
const promiseEscape_1 = require("./src/promiseEscape"); | ||
import { nextDeferred } from './src/deferred'; | ||
import isTimeToYield from './src/isTimeToYield'; | ||
import yieldToMainThreadBase from './src/yieldToMainThread'; | ||
import { cancelPromiseEscape, requestPromiseEscape } from './src/promiseEscape'; | ||
let promiseEscapeId; | ||
@@ -28,15 +15,12 @@ /** | ||
*/ | ||
function yieldOrContinue(priority) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
promiseEscape_1.cancelPromiseEscape(promiseEscapeId); | ||
if (isTimeToYield_1.default(priority)) { | ||
yield yieldToMainThread_1.default(priority); | ||
} | ||
promiseEscape_1.cancelPromiseEscape(promiseEscapeId); | ||
promiseEscapeId = promiseEscape_1.requestPromiseEscape(() => { | ||
deferred_1.nextDeferred(); | ||
}); | ||
export async function yieldOrContinue(priority) { | ||
cancelPromiseEscape(promiseEscapeId); | ||
if (isTimeToYield(priority)) { | ||
await yieldToMainThreadBase(priority); | ||
} | ||
cancelPromiseEscape(promiseEscapeId); | ||
promiseEscapeId = requestPromiseEscape(() => { | ||
nextDeferred(); | ||
}); | ||
} | ||
exports.yieldOrContinue = yieldOrContinue; | ||
/** | ||
@@ -52,12 +36,10 @@ * Waits for the browser to become idle again in order to resume work. Calling `yieldToMainThread()` | ||
*/ | ||
function yieldToMainThread(priority) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
promiseEscape_1.cancelPromiseEscape(promiseEscapeId); | ||
yield yieldToMainThread_1.default(priority); | ||
promiseEscape_1.cancelPromiseEscape(promiseEscapeId); | ||
promiseEscapeId = promiseEscape_1.requestPromiseEscape(() => { | ||
deferred_1.nextDeferred(); | ||
}); | ||
export async function yieldToMainThread(priority) { | ||
cancelPromiseEscape(promiseEscapeId); | ||
await yieldToMainThreadBase(priority); | ||
cancelPromiseEscape(promiseEscapeId); | ||
promiseEscapeId = requestPromiseEscape(() => { | ||
nextDeferred(); | ||
}); | ||
} | ||
exports.yieldToMainThread = yieldToMainThread; | ||
export { isTimeToYield }; |
{ | ||
"name": "main-thread-scheduling", | ||
"version": "1.0.0", | ||
"description": "Always responsive apps while staying on the main thread", | ||
"version": "2.0.0", | ||
"description": "Consistently responsive apps while staying on the main thread", | ||
"license": "MIT", | ||
@@ -49,4 +49,4 @@ "repository": "astoilkov/main-thread-scheduling", | ||
"@types/jest": "^26.0.22", | ||
"@typescript-eslint/eslint-plugin": "^4.11.0", | ||
"@typescript-eslint/parser": "^4.11.0", | ||
"@typescript-eslint/eslint-plugin": "^4.30.0", | ||
"@typescript-eslint/parser": "^4.30.0", | ||
"confusing-browser-globals": "^1.0.10", | ||
@@ -62,7 +62,4 @@ "eslint": "^7.16.0", | ||
"ts-jest": "^26.5.4", | ||
"typescript": "^4.1.3" | ||
}, | ||
"dependencies": { | ||
"@types/requestidlecallback": "^0.3.1" | ||
"typescript": "^4.4.2" | ||
} | ||
} |
@@ -14,2 +14,15 @@ <br> | ||
<p align="center"> | ||
<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> | ||
<a href="https://codeclimate.com/github/astoilkov/main-thread-scheduling/test_coverage"> | ||
<img src="https://img.shields.io/codeclimate/coverage/astoilkov/main-thread-scheduling" alt="Test Coverage" /> | ||
</a> | ||
<a href="https://bundlephobia.com/result?p=use-local-storage-state"> | ||
<img src="https://badgen.net/bundlephobia/min/main-thread-scheduling" alt="Test Coverage" /> | ||
</a> | ||
<!-- [![Minified Size](https://img.shields.io/npm/dm/main-thread-scheduling)](https://www.npmjs.com/package/use-local-storage-state) --> | ||
<p> | ||
<br> | ||
@@ -23,15 +36,30 @@ | ||
## Why | ||
## Overview | ||
It's hard to make an app responsive. With time apps get more complex and keeping your app responsive becomes even harder. Web Workers can help, but if you have tried it, you know it's a lot of work. | ||
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 | ||
This library keeps everything on the main thread. This allows for a very small and simple API that can be integrated easily in existing code base. | ||
This is accomplished through multiple strategies: | ||
- 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). | ||
- 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. | ||
Here a few more advantages: | ||
- Simple. 90% of the time you only need `yieldOrContinue(priority)` function. The API has two more functions for more advanced cases. | ||
- Utilizes the new `navigator.scheduling.isInputPending()` method (when available). Fallbacks to a good enough alternative otherwise. | ||
## 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). | ||
- Consistently responsive. You can have hundred of tasks pending but the library will always execute just a single one. | ||
- Aiming for high-quality with [my open-source principles](https://astoilkov.com/my-open-source-principles) | ||
- 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). | ||
@@ -86,4 +114,6 @@ ## API | ||
- 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. |
@@ -1,6 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.nextDeferred = exports.removeDeferred = exports.isDeferredLast = exports.createDeferred = void 0; | ||
const whenReady_1 = require("./whenReady"); | ||
const idlePhase_1 = require("./idlePhase"); | ||
import { startTrackingPhases, stopTrackingPhases } from './phaseTracking'; | ||
const deferred = []; | ||
@@ -11,4 +7,4 @@ /** | ||
*/ | ||
function createDeferred(priority) { | ||
const wr = whenReady_1.default(); | ||
export function createDeferred(priority) { | ||
const wr = whenReady(); | ||
const item = { priority, ready: wr.promise, resolve: wr.resolve }; | ||
@@ -20,7 +16,6 @@ const insertIndex = priority === 'user-visible' | ||
if (deferred.length === 1) { | ||
idlePhase_1.startTrackingIdlePhase(); | ||
startTrackingPhases(); | ||
} | ||
return item; | ||
} | ||
exports.createDeferred = createDeferred; | ||
/** | ||
@@ -30,6 +25,5 @@ * Checks if the task is last in the queue and it's time to run it. | ||
*/ | ||
function isDeferredLast(deferredItem) { | ||
export function isDeferredLast(deferredItem) { | ||
return deferredItem === deferred[deferred.length - 1]; | ||
} | ||
exports.isDeferredLast = isDeferredLast; | ||
/** | ||
@@ -40,3 +34,3 @@ * Remove the task from the queue. This happens when we execute this task and it's time for the next | ||
*/ | ||
function removeDeferred(deferredItem) { | ||
export function removeDeferred(deferredItem) { | ||
const index = deferred.indexOf(deferredItem); | ||
@@ -48,6 +42,5 @@ if (index === -1) { | ||
if (deferred.length === 0) { | ||
idlePhase_1.stopTrackingIdlePhase(); | ||
stopTrackingPhases(); | ||
} | ||
} | ||
exports.removeDeferred = removeDeferred; | ||
/** | ||
@@ -57,3 +50,3 @@ * Resolve the last task in the queue. This triggers executing the task by resolving the promise | ||
*/ | ||
function nextDeferred() { | ||
export function nextDeferred() { | ||
const lastDeferredItem = deferred[deferred.length - 1]; | ||
@@ -64,2 +57,9 @@ if (lastDeferredItem !== undefined) { | ||
} | ||
exports.nextDeferred = nextDeferred; | ||
function whenReady() { | ||
let promiseResolve; | ||
const promise = new Promise((resolve) => (promiseResolve = resolve)); | ||
return { | ||
promise, | ||
resolve: promiseResolve, | ||
}; | ||
} |
@@ -1,4 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.stopTrackingIdlePhase = exports.startTrackingIdlePhase = exports.getIdlePhase = void 0; | ||
let idlePhase; | ||
@@ -16,7 +13,6 @@ // Why the "stop-requested" status? | ||
let status = 'stopped'; | ||
function getIdlePhase() { | ||
export function getIdlePhase() { | ||
return idlePhase; | ||
} | ||
exports.getIdlePhase = getIdlePhase; | ||
function startTrackingIdlePhase() { | ||
export function startTrackingIdlePhase() { | ||
if (status === 'running') { | ||
@@ -46,4 +42,3 @@ throw new Error('Unreachabe code. This is probably a bug – please log an issue.'); | ||
} | ||
exports.startTrackingIdlePhase = startTrackingIdlePhase; | ||
function stopTrackingIdlePhase() { | ||
export function stopTrackingIdlePhase() { | ||
if (status === 'stopped' || status === 'stop-requested') { | ||
@@ -54,2 +49,1 @@ throw new Error('Unreachabe code. This is probably a bug – please log an issue.'); | ||
} | ||
exports.stopTrackingIdlePhase = stopTrackingIdlePhase; |
@@ -0,1 +1,4 @@ | ||
/** | ||
* Determines if it's time to call `yieldToMainThread()`. | ||
*/ | ||
export default function isTimeToYield(priority: 'background' | 'user-visible'): boolean; |
@@ -1,18 +0,30 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
// #performance | ||
// calling `isTimeToYield()` thousand of times is slow. `lastCalls` helps to run logic inside of | ||
// `isTimeToYield()` at most 1 per millisecond. | ||
import { getIdlePhase } from './phaseTracking'; | ||
let lastCall = 0; | ||
let lastResult = false; | ||
/** | ||
* Determines if it's time to call `yieldToMainThread()`. | ||
*/ | ||
const idlePhase_1 = require("./idlePhase"); | ||
function isTimeToYield(priority) { | ||
export default function isTimeToYield(priority) { | ||
var _a, _b; | ||
const idlePhase = idlePhase_1.getIdlePhase(); | ||
return (idlePhase === undefined || | ||
((_b = (_a = navigator.scheduling) === null || _a === void 0 ? void 0 : _a.isInputPending) === null || _b === void 0 ? void 0 : _b.call(_a)) === true || | ||
performance.now() > calculateDeadline(priority, idlePhase)); | ||
const now = Date.now(); | ||
if (now - lastCall === 0) { | ||
return lastResult; | ||
} | ||
const idlePhase = getIdlePhase(); | ||
lastCall = 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; | ||
return lastResult; | ||
} | ||
exports.default = isTimeToYield; | ||
function calculateDeadline(priority, idlePhase) { | ||
var _a; | ||
const maxTime = priority === 'background' ? 5 : 50; | ||
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 | ||
@@ -19,0 +31,0 @@ ? // if `isInputPending()` isn't supported, don't go spend more than the idle dealine is |
@@ -1,4 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.cancelPromiseEscape = exports.requestPromiseEscape = void 0; | ||
import requestLaterMicrotask from './requestLaterMicrotask'; | ||
let globalId = 0; | ||
@@ -11,12 +9,10 @@ const running = new Set(); | ||
*/ | ||
function requestPromiseEscape(callback) { | ||
export function requestPromiseEscape(callback) { | ||
const id = globalId; | ||
running.add(id); | ||
queueMicrotask(() => { | ||
queueMicrotask(() => { | ||
if (running.has(id)) { | ||
callback(); | ||
running.delete(id); | ||
} | ||
}); | ||
requestLaterMicrotask(() => { | ||
if (running.has(id)) { | ||
callback(); | ||
running.delete(id); | ||
} | ||
}); | ||
@@ -26,3 +22,2 @@ globalId += 1; | ||
} | ||
exports.requestPromiseEscape = requestPromiseEscape; | ||
/** | ||
@@ -32,3 +27,3 @@ * Cancels the request to escape promise. | ||
*/ | ||
function cancelPromiseEscape(id) { | ||
export function cancelPromiseEscape(id) { | ||
if (id !== undefined) { | ||
@@ -38,2 +33,1 @@ running.delete(id); | ||
} | ||
exports.cancelPromiseEscape = cancelPromiseEscape; |
@@ -1,13 +0,2 @@ | ||
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const whenReady_1 = require("./whenReady"); | ||
import whenReady from './whenReady'; | ||
let wr; | ||
@@ -22,22 +11,19 @@ /** | ||
*/ | ||
function requestLastIdleCallback() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (wr === undefined) { | ||
wr = whenReady_1.default(); | ||
// - we call `queueMicrotask()` twice in order to try to be the last one calling | ||
// `requestIdleCallback()` | ||
// - we don't use `setTimeout()` because calling `requestIdleCallback()` from there | ||
// registers it for the next iteration of the main event loop not for the current one | ||
export default async function requestLastIdleCallback() { | ||
if (wr === undefined) { | ||
wr = whenReady(); | ||
// - we call `queueMicrotask()` twice in order to try to be the last one calling | ||
// `requestIdleCallback()` | ||
// - we don't use `setTimeout()` because calling `requestIdleCallback()` from there | ||
// registers it for the next iteration of the main event loop not for the current one | ||
queueMicrotask(() => { | ||
queueMicrotask(() => { | ||
queueMicrotask(() => { | ||
requestIdleCallback((deadline) => { | ||
wr.resolve(deadline); | ||
wr = undefined; | ||
}); | ||
requestIdleCallback((deadline) => { | ||
wr.resolve(deadline); | ||
wr = undefined; | ||
}); | ||
}); | ||
} | ||
return wr.promise; | ||
}); | ||
}); | ||
} | ||
return wr.promise; | ||
} | ||
exports.default = requestLastIdleCallback; |
@@ -1,4 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
function whenReady() { | ||
export default function whenReady() { | ||
let promiseResolve; | ||
@@ -11,2 +9,1 @@ const promise = new Promise((resolve) => (promiseResolve = resolve)); | ||
} | ||
exports.default = whenReady; |
@@ -1,28 +0,40 @@ | ||
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const isTimeToYield_1 = require("./isTimeToYield"); | ||
const requestLastIdleCallback_1 = require("./requestLastIdleCallback"); | ||
const deferred_1 = require("./deferred"); | ||
function yieldToMainThread(priority) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const deferred = deferred_1.createDeferred(priority); | ||
yield requestLastIdleCallback_1.default(); | ||
if (!deferred_1.isDeferredLast(deferred)) { | ||
yield deferred.ready; | ||
if (isTimeToYield_1.default(priority)) { | ||
yield requestLastIdleCallback_1.default(); | ||
} | ||
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); | ||
if (!isDeferredLast(deferred)) { | ||
await deferred.ready; | ||
if (isTimeToYield(priority)) { | ||
await schedule(priority); | ||
} | ||
deferred_1.removeDeferred(deferred); | ||
}); | ||
} | ||
removeDeferred(deferred); | ||
} | ||
exports.default = yieldToMainThread; | ||
async function schedule(priority) { | ||
if (priority === 'user-visible') { | ||
await promiseSequantial([ | ||
() => 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 { | ||
await promiseSequantial([ | ||
() => waitCallback(requestLaterMicrotask), | ||
() => waitCallback(requestIdleCallback), | ||
]); | ||
} | ||
} | ||
async function promiseSequantial(getPromises) { | ||
for (const getPromise of getPromises) { | ||
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
29322
0
26
530
116
- Removed@types/requestidlecallback@^0.3.1
- Removed@types/requestidlecallback@0.3.7(transitive)