js-coroutines
I had a Eureka moment earlier after reading something very interesting on dev.to - gave me an idea - and wow did it work!
I asked myself this question:
When is the right time to sort a massive array on the main thread of a Javascript app? Well any time you like if you don't mind the user seeing all of your animations and effects jank to hell. Even transferring to a worker thread is going to hit the main thread for serialization and stutter everything.
So when is the right time? Well it's in all those gaps where you animation isn't doing anything and the system is idle. If only you could write something to use up that time and then relinquish control to the system so it can animate and do the rest of the work, then resume in the next gap. Well now you can...
Now supports asynchronous JSON see the follow up article!
Wait there's more!
Another super useful way of using coroutines is to animate and control complex states - js-coroutines provides this too with the powerful update
method that runs every frame in high priority. See below.
It comes ready with the most useful functions for arrays:
- forEach
- map
- filter
- reduce
- findIndex
- find
- some
- every
- sort
- append (array to array)
- concat (two arrays into a new array)
The helper yielding
wraps a normal function as a generator and checks remaining time every few iterations. You can see it in use above. It's just a helper though - if your map
function needs to do more work it can just be a generator itself, yield when it likes and also pass on to deeper functions that can yield:
const results =
yield *
map(inputArray, function* (element, index) {
//Every 200 indices give up work
//on this frame by yielding 'true'
//yield without true, checks the amount
//of remaining time
if (index % 200 === 199) yield true;
//Yield out a filter operation
let matched = yield* filter(
element,
yielding((c) => c > 1000)
);
//Now yield out the calculation of a sum
return yield* reduce(
matched,
yielding((c, a) => c + a),
0
);
});
yielding(fn, [optional yieldFrequency]) -> function *
Update coroutines
A great way to do stateful animation is using a coroutine running every frame. In this case when you yield
you get called back on the next frame making stateful animations a piece of cake:
import { update } from "js-coroutines";
//Animate using a coroutine for state
update(function* () {
while (true) {
//Move left to right
for (let x = -200; x < 200; x++) {
logoRef.current.style.marginLeft = `${x * multiplier}px`;
yield;
//Now we are on the next frame
}
//Move top to bottom
for (let y = 0; y < 200; y++) {
logoRef.current.style.marginTop = `${y * multiplier}px`;
yield;
}
//Move diagonally back
for (let x = 200; x > -200; x--) {
logoRef.current.style.marginLeft = `${x * multiplier}px`;
logoRef.current.style.marginTop = ((x + 200) * multiplier) / 2 + "px";
yield;
}
}
});
As you can see in this performance capture, the sort and processing are evenly spread across frames, maintaining 60fps.
Get the library here:
or
npm i js-coroutines
License
js-coroutines - MIT (c) 2020 Mike Talbot