loading...
Cover image for 60fps JS while sorting, mapping and reducing millions of records (with idle-time coroutines)

60fps JS while sorting, mapping and reducing millions of records (with idle-time coroutines)

miketalbot profile image Mike Talbot Updated on ・3 min read

js-coroutines

GitHub

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;
    }
  }
});

Alt Text

As you can see in this performance capture, the sort and processing are evenly spread across frames, maintaining 60fps.

Get the library here:

GitHub

or

npm i js-coroutines

License

js-coroutines - MIT (c) 2020 Mike Talbot

How it works?

Follow up article here

Posted on May 24 by:

Discussion

markdown guide
 

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)

Demo

Sweet! I think I'll be in a place to test this out next week. 👍

 

So github is now public and I've improved the docs and included es6 version in the dist folder of the npm package.

 
 

Sorry, late night, forgot to make the repo public...

 

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...

 
 

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!

 

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