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
Top comments (19)
GitHub link is 404?
Sorry, late night, forgot to make the repo public...
So github is now public and I've improved the docs and included es6 version in the dist folder of the npm package.
This library has been updated to support JSON and a whole bunch of helper Async functions to make coding easier. There is more detail available in this article: dev.to/miketalbot/60fps-javascript...
Do you expect this would work with React Native?
I wouldn't have thought so the way it is at the moment as it relies on
requestIdleCallback
which is a browser thing. Though clearly it could be adapted to work in that environment either by estimating the time per frame (using whatever the RN tick is) or if RN has actually got something to tell us.I have only dabbled in React Native - would this be useful do you think? Very happy to do a version for it if it would help?
We have a ton of data we must manipulate client side to put in charts. Parallel animations have been a challenge. Something like
js-coroutines
would potentially provide a much better user experience.Ok cool - I looked after your comment. It appears that I can do something with the timers. I'm just not sure how much time it will need when reverting to native after the JS. But definitely looks possible. I'm excited to try if you'll find it useful. Will give it a look.
Ok, it works :)
js-coroutines 2.1.40 - very recent version of React Native (but I think I've polyfilled requestIdleCallback for older versions)
Sweet! I think I'll be in a place to test this out next week. 👍
404 on the github link.
Now made it public - link is github.com/miketalbot/js-coroutines
This is really sweet! Might be asking for a few of us here, but do you think you could possibly delve into how this works, and how you came to these answers?
Good point, I definitely will write on that. I'm going to add JSON parsing as a coroutine and then when that's done (hopefully tomorrow) I'll write about the core principles. Should have done it at the start!
Awesome, can't wait to read it!
Hey Josh
I've posted the updated library info and the description of the process here.
M
Hey Mike, that is a brilliant write up, super in-depth and well explained!
Thank you again for this.
That is impressive! Well done man
This is really really cool. I'm sure I'll find use-cases for it in the next future. Probably sooner than later for chessroulette.org 🤔