Reputation: 30006
My Javascript app needs to do some heavy calculations on the UI thread (let's ignore web workers for the scope of this question).
Luckily, the work to be done is embarrassingly parallel and can be cut into thousands of small async functions.
What I would like to do is: Queue up all these async functions on the event loop, but prioritize UI events.
If this works, the impact on UI responsiveness should be very low. Each of the functions takes only a few milliseconds.
The problem is that Javascript eagerly executes async code. Until you run into something like await IO
it simply executes all the code as if it was synchronous.
I would like to break this up: When the execution encounters an async call, it should put it on the event loop but before executing it, it should check if there's a UI event waiting to be processed.
Basically, I'm looking for a way to immediately yield
from my worker functions, without returning a result.
I've experimented and found a workaround. Instead of calling the async function directly, it can be wrapped in a timeout. This is pretty much exactly what I wanted: The async call is not executed but queued up on the event loop.
Here is a simple example, based on react. After clicking, the button will update immediately, instead of waiting for all those calculations to finish.
(The example probably looks like horrible code, I'm putting heavy calculations inside the render function. But that's just to keep it short. It really doesn't matter where the async functions are kicked off, they always block the single UI thread):
// timed it, on my system it takes a few milliseconds
async function expensiveCalc(n: number) {
for (let i = 0; i < 100000; i++) {
let a = i * i;
}
console.log(n);
}
const App: React.FC = () => {
const [id, setId] = useState("click me");
// many inexpensive calls that add up
for (let i = 0; i < 1000; i++) {
setTimeout(() => expensiveCalc(i), 1);
}
// this needs to be pushed out as fast as possible
return (
<IonApp>
<IonButton
onClick={() => setId(v4())}>
{id}
</IonButton>
</IonApp>
);
};
Now, this looks neat. The synchronous execution of the render function is never interrupted. All the calculation functions are queued up and done after render is finished, they don't even have to be async.
What I don't understand is: This works even if you click on the button many times!
My expectation would be that all calls on the event loop are treated equally. So the first time the render should be instant, the second time it needs to wait for all the timeouts to be executed first.
But what I see is:
So the event loop does not execute in-order. And somehow UI events are prioritized. That's awesome. But how does it work? And can I prioritize events myself?
Here's a sandbox where you can play around with the example.
Upvotes: 0
Views: 524
Reputation: 136608
Bergi's answer has a good explanation on what happens (there are task prioritizations in browser's event-loop).
Note that we may in a near future1 have an API to deal with this ourselves as web-devs.
But I must note that what you are willing to do initially seems to be the exact job requestIdleCallback
has been designed for: execute a callback only when nothing else happens in the event-loop.
btn.onclick = (e) => {
for (let i = 0; i<100000; i++) {
requestIdleCallback( () => {
log.textContent = 'executed #' + i;
});
}
console.log( 'new batch' );
}
<pre id="log"></pre>
<button id="btn">click me</button>
This way you are not relying on undefined behavior which are up to implementations, here the specs ask that behavior.
1. This API can already be tested in Chrome by switching on chrome://flags/#enable-experimental-web-platform-features
Upvotes: 3
Reputation: 664196
So the event loop does not execute in-order. And somehow UI events are prioritized. That's awesome. But how does it work?
The browser event loop is a complex beast. It does contain multiple "task queues" for different "task sources", and browsers are free to optimise the scheduling between them. However, each queue by itself does follow the order.
And can I prioritize events myself?
No - not without writing your own scheduler and running that inside the event loop.
Upvotes: 1