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

Mike Talbot ⭐ - May 24 '20 - - Dev Community

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
    );
  });
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

License

js-coroutines - MIT (c) 2020 Mike Talbot

How it works?

Follow up article here

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player