Andrew's Digital Garden

Breaking up long tasks in Javascript

https://web.dev/optimize-long-tasks/

The main thread is where most tasks are run in the browser. It can only process one task at a time. A long tasks is any task >50ms. If the user is attempting to interact with the page while a long task runs—or if an important rendering update needs to happen—the browser will be delayed in handling that work. This results in interaction or rendering latency.

Thus, you should break up tasks. If tasks are broken up, the browser has more opportunities to respond to higher-priority work—and that includes user interactions.

Importantly Javascript runs functions within a single function as a single task. Given the following code snippet, each five inner functions are part of the same long task.

function saveSettings () { validateForm(); showSpinner(); saveToDatabase(); updateUI(); sendAnalytics(); }

JavaScript works this way because it uses the run-to-completion model of task execution. This means that each task will run until it finishes, regardless of how long it blocks the main thread.

There are some ways to break up tasks:

  • setTimeout can be used to break up tasks into smaller ones. This works well if you have a series of functions that need to run sequentially, but your code may not always be organized this way.
  • You can also break up work using requestIdleCallback(), but tasks are scheduled at the lowest priority, meaning they may never run.
  • Yield to the main thread. Yielding adds to the end of the task queue.

When you yield to the main thread, you're giving it an opportunity to handle more important tasks than the ones that are currently queued up. Ideally, you should yield to the main thread whenever you have some crucial user-facing work that needs to execute sooner than if you didn't yield. Yielding to the main thread creates opportunities for critical work to run sooner.

When tasks are broken up, other tasks can be prioritized better by the browser's internal prioritization scheme. One way to yield to the main thread involves using a combination of a Promise that resolves with a call to setTimeout(): Importantly the setTimeout is here, promise callbacks run as microtasks.

function yieldToMain () { return new Promise(resolve => { setTimeout(resolve, 0); }); }

You should yield only when necessary. Also consider batching related tasks together that make sense to be run one shortly after one another.

isInputPending() is a function you can run at any time to determine if the user is attempting to interact with a page element: a call to isInputPending() will return true. It returns false otherwise.

A new scheduler API is being created to offer finer-grained scheduling of tasks.

Main takeaways:

  • Yield to the main thread for critical, user-facing tasks.
  • Use isInputPending() to yield to the main thread when the user is trying to interact with the page.
  • Prioritize tasks with postTask().
  • Finally, do as little work as possible in your functions.

[[js]] [[performance]]

Breaking up long tasks in Javascript