Skip to content

Instantly share code, notes, and snippets.

@jesstelford
Last active November 14, 2023 12:26
Show Gist options
  • Save jesstelford/bbb30b983bddaa6e5fef2eb867d37678 to your computer and use it in GitHub Desktop.
Save jesstelford/bbb30b983bddaa6e5fef2eb867d37678 to your computer and use it in GitHub Desktop.
Starving the Event Loop with Microtasks

Starving the Event Loop with microtasks

"What's the Event Loop?"

Sparked from this twitter conversation when talking about doing fast async rendering of declarative UIs in Preact

These examples show how it's possible to starve the main event loop with microtasks (because the microtask queue is emptied at the end of every item in the event loop queue). Note that these are contrived examples, but can be reflective of situations where Promises are incorrectly expected to yield to the event loop "because they're async".

  • setTimeout-only.js is there to form a baseline
  • chained-promises.js shows the event loop being starved when many microtasks are queued at once
  • promises-returning-promises.js shows chained .then() calls starving the event loop
  • nested-chained-promises.js shows nested .then() calls starving the event loop
  • promise-all.js shows Promise.all() also suffers from this

For more on microtasks and the event loop, check out Jake Archibald's article "Tasks, microtasks, queues and schedules"

let start;
setTimeout((_=>console.log(performance.now() - start)), 0)
let p = Promise.resolve(1)
for(let i = 100000; i; i--) {
p = p.then(_ => 1);
}
start = performance.now();
// Invokes overhead of creating new Promise objects until the start of each microtask.
let start;
setTimeout((_=>console.log(performance.now() - start)), 0)
let count = 100000
function createNewPromise() {
return Promise.resolve(1).then(_ => {
if (count) {
createNewPromise()
count--;
}
})
}
Promise.resolve(1).then(createNewPromise);
start = performance.now();
// Running parallel promises all occur in the single turn of the micro task queue
let start;
setTimeout((_=>console.log(performance.now() - start)), 0)
let promises = []
for(let i = 100000; i; i--) {
promises.push(Promise.resolve(1));
}
Promise.all(promises)
start = performance.now();
// Invokes overhead of creating new Promise objects
let start;
setTimeout((_=>console.log(performance.now() - start)), 0)
let p = Promise.resolve(1);
for(let i = 100000; i; i--) {
p = p.then(_ => Promise.resolve(1));
}
start = performance.now();
let start;
setTimeout((_=>console.log(performance.now() - start)), 0)
start = performance.now();
@richardscarrott
Copy link

Should nested-chained-promises.js not return the nested promise on line 8?

@jesstelford
Copy link
Author

It could, but doesn't have to. The goal of that example is only to show that a promise within a promise still gets resolved on the same turn of the mocrotask queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment