With the advent of ESM modules, any tool that dynamically loads modules needs
to do so asynchronously: due to its runtime plugins and configuration systems,
Babel is slowly migrating from a synchronous to an asynchronous API. However,
sometimes you really need your libraries to be synchronous: learn with us how
we solved this problem, thanks to Workers and Atomics!
(c) The Belgian Javascript Conference (BeJS Conf) 2023
12 May 2023
Brussels & Online
https://www.bejs.io/conf
https://www.youtube.com/playlist?list=PL53Z0yyYnpWhtlmFjyX1cIlisuFvHv1JK
2. @NicoloRibaudo 2
NICOLÒ RIBAUDO
● Working at Igalia on web standards
● Maintaining Babel, the JavaScript compiler
nribaudo@igalia.com
@NicoloRibaudo
@nicolo-ribaudo
@nic@tech.lgbt
@nicolo-ribaudo:matrix.org
5. @NicoloRibaudo
Why would you want to do it???
● 2021–2023 — We migrated Babel from internally using
CommonJS to using ESM.
5
🥳
6. @NicoloRibaudo
Why would you want to do it???
● Babel supports loading external plugins (actually,
everything is a plugin)
6
// babel.config.json
{
"plugins": [
"@babel/plugin-proposal-decorators"
]
}
// Somewhere inside Babel (previously)
function loadAllPlugins() {
const plugin = require(pluginName);
}
// Somewhere inside Babel (now)
async function loadAllPlugins() {
const plugin = await import(pluginName);
}
7. @NicoloRibaudo
Why would you want to do it???
● async/await virally makes everything asynchronous
7
// Someone using Babel (previously)
import * as babel from "@babel/core";
const code = babel.transform(input, {
// ... config
});
// Someone using Babel (now)
import * as babel from "@babel/core";
const code = await babel.transform(input, {
// ... config
});
8. @NicoloRibaudo
Why would you want to do it???
● Sometimes you can force😇 asynchronous APIs on
your users — sometimes you can't.
8
@babel/eslint-parser is an ESLint parser to support experimental syntax;
@babel/register hooks into Node.js' require() to compile files on-the-fly.
11. @NicoloRibaudo
Worker and Atomics
11
JavaScript is a multi-threaded programming language:
- new Worker() to spawn multiple threads
- new SharedArrayBuffer() to share memory across
threads
- Atomics.* for thread-safe operations on shared
memory
Atomics.add, Atomics.xor, Atomics.store, Atomics.load,
Atomics.wait, Atomics.notify, Atomics.isLockFree, ...
12. @NicoloRibaudo
Worker and Atomics
Main thread
function doSomethingSync() {
● Setup a synchronization channel
● Wait until the worker is done
● Read the received result
}
12
Worker thread
addListener("message", () => {
● Asynchronously computer the
result
● Notify the main thread that the
result is ready
});
Send the result back to
the main thread
Share some data and the
synchronization channel with the
worker
13. @NicoloRibaudo
Worker and Atomics
Main thread
function doSomethingSync() {
const signal = new Int32Array(new SharedArrayBuffer(4));
const { port1, port2 } = new MessageChannel();
// sleep
Atomics.wait(signal, 0, 0);
const { result } = receiveMessageOnPort(port2);
return result;
}
13
Worker thread
addListener("message", () => {
let result =
await doSomethingAsync();
port1.postMessage({ result });
Atomics.notify(signal, 0);
});
signal, port1
{ payload: /* ... */ }
{ result: /* ... */ }
14. @NicoloRibaudo
Worker and Atomics
Main thread
1. Create the worker
const { Worker, SHARE_ENV } = require("worker_threads");
const worker = new Worker("./path/to/worker.js", {
env: SHARE_ENV,
});
14
15. @NicoloRibaudo
Worker and Atomics
Main thread, doSomethingSync
2. Delegate the task to the worker
const { MessageChannel } = require("worker_threads");
const signal = new Int32Array(new SharedArrayBuffer(4));
const { port1, port2 } = new MessageChannel();
worker.postMessage({ signal, port: port1, payload }, [port1]);
Atomics.wait(signal, 0, 0);
15
16. @NicoloRibaudo
Worker and Atomics
Worker thread, "message" listener
3. Perform the task and send the result to the main thread
const result = await doSomethingAsync();
port.postMessage({ result });
port.close();
Atomics.notify(signal, 0);
16
17. @NicoloRibaudo
Worker and Atomics
Main thread, doSomethingSync
4. After waking up, read the result and return it
const { receiveMessageOnPort } = require("worker_threads");
... const { port1, port2 } = new MessageChannel(); ...
... Atomics.wait(signal, 0, 0); ...
const { result } = receiveMessageOnPort(port2);
return result;
17
20. @NicoloRibaudo
Usage outside of Node.js
20
● In web browsers, the main thread cannot sleep. You can only
synchronously call your asynchronous functions from other web workers.
● receiveMessageOnPort is only available in Node.js. When using
browsers or browser-compatible engines, you need to manually serialize
your data on a SharedArrayBuffer, and synchronously read and
deserialize them on the other side when it wakes up.
22. @NicoloRibaudo
Who's doing this?
22
● Babel, in @babel/register and @babel/eslint-plugin
● Prettier, in @prettier/sync
● Node.js 20+ itself, to support synchronous import.meta.resolve while
moving ESM loader hooks to a separate thread (nodejs/node#44710)