Richie McColl

Scheduling JavaScript

Published by Richie McColl on 10/20/2019

As good as JavaScript may be, is it possible to have too much of a good thing?

Imagine an e-commerce website with a variety of different products. These are dynamically loaded and rendered as the page scrolls.

Consider the different requirements that come together to provide this user experience.

  • Scroll responsiveness
  • Smooth animations
  • Fetching the content
  • Rendering the content
  • Sending any analytics

What is the best way to meet these requirements? How do you keep the scrolling and rendering responsive for the user?

It's almost too easy to slip up and let some long running JavaScript block the main thread. The user could notice this through many things. Some examples:

  • unresponsive scrolling
  • janky animations
  • the page being slow to render.

It's fair to say that some of these requirements have a higher priority than others. What if there were an API that knew about the different priorities of tasks running on the main thread? This is where a scheduler comes in.

If we were to design an API for this, what different things should it handle?

  • A set of tasks, with priorities.
  • Task queues to manage groups of tasks.
  • An API for scheduling a task to a queue.
  • A way to execute tasks at an appropriate time. A run-loop.

If we go back to the previous problem, let's split some of these requirements into separate tasks.

function renderContent() {...}
function handleScrollAnimation() {...}
function handleAnalytics() { ... }

We would then need an API that would allow us to assign each of these tasks to a priority queue. There are different types of priority queues. Each one would have a handler that knows how and when to execute these tasks.

Ok, let's create a scheduler and some priority queues.

const scheduler = new Scheduler();
const animationFrameScheduler = new AnimationFrameScheduler();
const idleCallbackScheduler = new IdleCallbackScheduler();
// virtual task queues to manage tasks
scheduler.addQueue("animation", animationFrameScheduler);
scheduler.addQueue("idle", idleCallbackScheduler);

Once the queues are set up, tasks could be scheduled like so:

scheduler.scheduleTask("animation", handleScrollAnimation);
scheduler.scheduleTask("animation", renderContent);
scheduler.scheduleTask("idle", handleAnalytics);

Internally the controlled infinite "run-loop" might look something like:

class Scheduler {
...
async execute() {
while (this.nextTask) {
// execute while there are tasks
// in the queue(s)
}
}
}

To figure out the appropriate timing for when this execute() method will be called, a scheduler sub-class might handle that.

class AnimationFrameScheduler extends Scheduler {
...
schedule() {
return requestAnimationFrame(frameStart => {
super.execute();
})
}
}

The scheduler would then be able to run the JavaScript in a timely fashion and pass control back to the browser.

The browser would then:

  • check its input event queue
  • see whether it has to process anything
  • go back to running the JavaScript tasks as they get added.

There is an important trade-off here, that isn't immediately obvious:

When we talk about giving control back to the browser, if it happens too often then the page will be slow to load. If it happens less often then any user events won't be responded to fast enough.

To remove this trade-off, there is a browser API called called isInputPending. (This is currently in origin trial at the time of writing)

How does it work?

By hooking into Chrome's input queue, it can intercept events before any work is done on the main thread. This means that any calls to isInputPending should be fast.

How this might look as part of the scheduler:

class Scheduler {
...
async execute() {
while (this.nextTask) {
if (navigator.scheduling.isInputPending()) {
// stop any tasks to handle an input event
break;
}
// execute while there are tasks in the queue(s)
}
}
}

It is a useful design exercise to imagine what a scheduling API might look like. But there are still some browser primitives missing.

Teams at Google and Facebook are already on it and seem to be making the most headway in the FE scheduling space so far.

The React team have built a scheduler that will help them with concurrent rendering. Engineers at Google are also working on an API for main thread scheduling.

At this stage, a lot of this is still up in the air. Whether there will be a scheduler built into the browser or if it will exist as a library internal it's too early to say.

Here are some reading links if you want to read more about this problem space.

← Back to all posts