Giter VIP home page Giter VIP logo

main-thread-scheduling's Introduction



main-thread-scheduling

Fast and consistently responsive apps using a single function call

Gzipped Size Build Status


Install

npm install main-thread-scheduling

Overview

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.
  • Your INP (Interaction to Next Paint) is in green.
  • It's easy to plug it into your existing codebase.

A real world showcase of searching in a folder with 10k notes, 200k+ lines of text, that take 50MB on disk and getting results instantly.

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

  • Uses requestIdleCallback() and requestAfterFrame() 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 strategy and gives priority to tasks requested later.
  • Considerate about your existing code. Tasks with idle strategy are executed last so there isn't some unexpected work that slows down the main thread after the background task is finished.

Why

  • Simple. 90% of the time you only need the yieldOrContinue(strategy) function. The API has two more functions for more advanced cases.
  • Not a weekend project. Actively maintained for three years — see contributors page. I've been using it in my own products for over four years — Nota and iBar. Flux.ai are also using it in their product (software for designing hardware circuits using web technologies).
  • This is the future. Some browsers have already implemented support for scheduling tasks on the main thread. This library tries even harder to improve user perceived performance — see explanation for details.
  • High quality. Aiming for high-quality with my open-source principles.

Example

You can see the library in action in this CodeSandbox. Try removing the call to yieldToContinue() and then type in the input to see the difference.

API

yieldOrContinue(strategy: 'interactive' | 'smooth' | 'idle', signal?: AbortSignal)

The complexity of the entire library is hidden behind this method. You can have great app performance by calling a single method.

async function findInFiles(query: string) {  
    for (const file of files) {
        await yieldOrContinue('interactive')
        
        for (const line of file.lines) {
            fuzzySearchLine(line, query)
        }
    }
}

More complex scenarios

The library has two more functions available:

  • yieldControl(strategy: 'interactive' | 'smooth' | 'idle', signal?: AbortSignal)
  • isTimeToYield(strategy: 'interactive' | 'smooth' | 'idle', signal?: AbortSignal)

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:

async function doHeavyWork() {
    for (const value of values) {
        if (isTimeToYield('interactive')) {
            render()
            await yieldControl('interactive')
        }
        
        computeHeavyWorkOnValue(value)
    }
}

Scheduling strategies

There are three scheduling strategies available. You can think about them more easily by completing the sentence with one of the three words: "Scheduling the task keeps the page interactive/smooth/idle."

  • interactive – use this for things that need to display to the user as fast as possible. Every interactive task is run for 83ms – this gives you a nice cycle of doing heavy work and letting the browser render pending changes.
  • smooth — use this for things you want to display to the user quickly but you still want for animations to run smoothly for example. smooth runs for 13ms and then gives around 3ms to render the frame.
  • idle – use this for background tasks. Every idle task is run for 5ms.

Alternatives

Web Workers

Web Workers are a great fit if you have: 1) heavy algorithm (e.g. image processing), 2) heavy process (runs for a long time, big part 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 most cases, you would discover the doing it on the main thread is good enough.

scheduler.postTask()

scheduler.postTask() is available in some browsers today. postTask() and main-thread-scheduling do similar things. You can think of postTask() as a lower level API — it might be the right choice in specific scenarios. Library owners might be interested in exploring the nuanced differences between the two. For most cases, main-thread-scheduling provides a scheduleTask() method that mimics that API of postTask() while providing the extra benefits of the library.

main-thread-scheduling's People

Contributors

astoilkov avatar cyreb7 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

main-thread-scheduling's Issues

Any GC concerns when using async/await

If I use a .then() chain, I believe the browser is able to GC better compared to async await which I believe prevents automated GC

Have you come across this scenario before?

Easier web workers

This is a cool idea for the future of web-apps, and it's nice to have a userland implementation before it's added to the standard! Since you said in the README to make an issue if there's an alternate solution to UI freezing, I'm making this issue to mention my package isoworker, which does some magic to make web workers easier to use. Most worker wrappers that allow you to avoid creating workers by hand have one key flaw: you can't easily copy anything from the main thread into the worker and must send over only a function. My package mitigates pretty much all the complexity of web workers: you can make long-running closures on a separate thread with virtually no restrictions, it almost works like calling the closure asynchronously on the main thread.

This package still seems better for cases where there is a lot of data being processed or constant communication is necessary, but isoworker can yield zero UI freezing by running on a separate thread.

Possible to manually cancel a scheduled function

Ive been wanting to use main thread scheduling to time aggressive updates on a function.

So in this case, if i have a update come through, then 1ms later another update is sent, id want to cancel the preexisting scheduled event and reschedule only the last valid call.

I do something like this with idleCallback but was wondering if this library could expose something similar, perhaps a label?

So if "something" already exists, and a new "something" event is sent in, remove the previous one from the queue and push this onto the end instead

class RenderWhenIdle extends React.Component {
  constructor() {
    super();
    this.updateDebounced = this.updateDebounced.bind(this);
    this.idleCallback = null;
  }

  updateDebounced() {
    this.idleCallback = requestIdleCallback(() => {
        this.forceUpdate();
    }, { timeout: 200 });
  }

  shouldComponentUpdate(nextProps) {
    if (this.idleCallback) { cancelIdleCallback(this.idleCallback); }
    this.updateDebounced();
    return false;
  }

  componentWillUnmount() {
    cancelIdleCallback(this.idleCallback);
  }

  componentDidMount() {
    this.updateDebounced();
  }

  render() {
    return this.props.children;
  }
}

Stand-alone JS file?

Since this is a client side library, is it possible to publish a single .js file which exposes this functionality and allows for general purpose importing, without having to build a Node.js project for it?

Error when building a project with Vite

Hi and thanks for your work on this library !

Since version 11.0.0 (i.e. with versions 11.0.0 and 12.0.0) I have encountered problems when I compile a project with Vite (5.0.x) that uses "main-thread-scheduling".

The compilation fails with the following message and Vite stops :

 [ERROR] Could not resolve "./src/utils/queueTask"

    node_modules/main-thread-scheduling/index.js:7:37:
      7  export { default as queueTask } from './src/utils/queueTask';
                                              ~~~~~~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./src/utils/withResolvers"

    node_modules/main-thread-scheduling/index.js:8:41:
      8  export { default as withResolvers } from './src/utils/withResolvers';
                                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./src/utils/requestAfterFrame"

    node_modules/main-thread-scheduling/index.js:9:38:
      9  export { default as afterFrame } from './src/utils/requestAfterFrame';
                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./utils/hasValidContext"

    node_modules/main-thread-scheduling/src/isTimeToYield.js:2:28:
      2  import hasValidContext from './utils/hasValidContext';
                                     ~~~~~~~~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./utils/queueTask"

    node_modules/main-thread-scheduling/src/yieldControl.js:2:22:
      2  import queueTask from './utils/queueTask';
                               ~~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./utils/hasValidContext"

    node_modules/main-thread-scheduling/src/yieldControl.js:4:28:
      4  import hasValidContext from './utils/hasValidContext';
                                     ~~~~~~~~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./utils/promiseEscape"

    node_modules/main-thread-scheduling/src/yieldControl.js:5:58:
      5  ...PromiseEscape, requestPromiseEscape } from './utils/promiseEscape';
                                                       ~~~~~~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./tasks/createTask"

    node_modules/main-thread-scheduling/src/yieldControl.js:6:23:
      6  import createTask from './tasks/createTask';
                                ~~~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./tasks/removeTask"

    node_modules/main-thread-scheduling/src/yieldControl.js:7:23:
      7  import removeTask from './tasks/removeTask';
                                ~~~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./tasks/nextTask"

    node_modules/main-thread-scheduling/src/yieldControl.js:8:25:
      8  import { nextTask } from './tasks/nextTask';
                                  ~~~~~~~~~~~~~~~~~~

 [ERROR] Could not resolve "./utils/withResolvers"

    node_modules/main-thread-scheduling/src/schedulingState.js:1:26:
      1  import withResolvers from './utils/withResolvers';
                                   ~~~~~~~~~~~~~~~~~~~~~~~

Error: Build failed with 11 errors:
node_modules/main-thread-scheduling/index.js:7:37: ERROR: Could not resolve "./src/utils/queueTask"
node_modules/main-thread-scheduling/index.js:8:41: ERROR: Could not resolve "./src/utils/withResolvers"
node_modules/main-thread-scheduling/index.js:9:38: ERROR: Could not resolve "./src/utils/requestAfterFrame"
node_modules/main-thread-scheduling/src/isTimeToYield.js:2:28: ERROR: Could not resolve "./utils/hasValidContext"
node_modules/main-thread-scheduling/src/schedulingState.js:1:26: ERROR: Could not resolve "./utils/withResolvers"
...
    at failureErrorWithLog (/home/projects/vitejs-vite-adh7q9/node_modules/esbuild/lib/main.js:1641:15)
    at eval (/home/projects/vitejs-vite-adh7q9/node_modules/esbuild/lib/main.js:1049:25)
    at eval (/home/projects/vitejs-vite-adh7q9/node_modules/esbuild/lib/main.js:1517:9) {
  errors: [Getter/Setter],
  warnings: [Getter/Setter]
}

It happens in a large project I'm working on but you can see a minimal repro here in a projet with only Vite, Typescript and main-thread-scheduling (this is a stackblitz project, hope this is fine to demonstrate the issue - try to change the main-thread-scheduling version to 10.0.0 in package.json to see it was working fine with older versions).

Am I doing something wrong or is there a workaround ?
Thanks !

Can't find the effect.

I have entered the link, this CodeSandbox, can't find any difference between un-commented and commented code..Maybe my PC is powerful enought to handle the heavy task...Do u want to use some tools to test your improvements, such as tinybench?

Add more priorities

Currently, I have added only two priorities to the library — background and user-visible. I feel this won't be enough. However, I can't imagine all the use cases the library will be used in. This is why I am starting a discussion here, so together, we can find the right priorities.

You can read more about priorities here and here.

Comparisson with other frameworks

I'll like to see a comparison between this library and other approaches followed by some frameworks, like react or solid.js (the latter being a simplified version of the former).

I know that solid for example doesn't have different priorities for its scheduler, I'll like to know how much it benefits to do the distinction between 'user-visible' and 'background' in your approach.

Compile errors in v6.0.0

Hi! 👋

Firstly, thanks for your work on this project! 🙂

Today I used patch-package to patch [email protected] for the project I'm working on.

Here is the diff that solved my problem:

diff --git a/node_modules/main-thread-scheduling/src/isTimeToYield.ts b/node_modules/main-thread-scheduling/src/isTimeToYield.ts
index f54e6a9..d827ae9 100644
--- a/node_modules/main-thread-scheduling/src/isTimeToYield.ts
+++ b/node_modules/main-thread-scheduling/src/isTimeToYield.ts
@@ -19,8 +19,10 @@ export default function isTimeToYield(priority: 'background' | 'user-visible'):
     }
 
     lastCallTime = now
+    // @ts-ignore: scheduling is new experimental API
+    const inputPending = navigator.scheduling?.isInputPending?.()
     lastResult =
-        now >= calculateDeadline(priority) || navigator.scheduling?.isInputPending?.() === true
+        now >= calculateDeadline(priority) || inputPending === true
 
     if (lastResult) {
         state.frameTimeElapsed = true
diff --git a/node_modules/main-thread-scheduling/src/tracking.ts b/node_modules/main-thread-scheduling/src/tracking.ts
index 033c536..14cf024 100644
--- a/node_modules/main-thread-scheduling/src/tracking.ts
+++ b/node_modules/main-thread-scheduling/src/tracking.ts
@@ -41,6 +41,7 @@ export function startTracking(): void {
                 isTracking = false
 
                 if (typeof cancelIdleCallback !== 'undefined') {
+                    // @ts-ignore: will be defined
                     cancelIdleCallback(idleCallbackId)
                 }
             } else {
diff --git a/node_modules/main-thread-scheduling/src/yieldControl.ts b/node_modules/main-thread-scheduling/src/yieldControl.ts
index 7236816..74a98b2 100644
--- a/node_modules/main-thread-scheduling/src/yieldControl.ts
+++ b/node_modules/main-thread-scheduling/src/yieldControl.ts
@@ -49,6 +49,7 @@ async function schedule(priority: 'user-visible' | 'background'): Promise<void>
         await new Promise<void>((resolve) => requestNextTask(resolve))
 
         // istanbul ignore if
+        // @ts-ignore: scheduling is new experimental API
         if (navigator.scheduling?.isInputPending?.() === true) {
             await schedule(priority)
         } else if (state.frameWorkStartTime === undefined) {

This issue body was partially generated by patch-package.

callback based API

The performance overhead of the current Promise based API is not non-trivial. Any chance to get a callback API as well?

Don’t crash SSR

I'm currently using a conditional to opt out on the server. But it would be less risky if on node it just performs a pass through.

I've also had some problems with jest clear timers getting stuck in a infinite loop if MTS is used on one of those unit tested components

It could be useful if node_env is set to test, do something like resolve a promise with a setTimeout. This may also help with other things like some of my tests "fail" because X function I'm spying on wasn't called fast enough.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.