DEV Community

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

Posted on • Updated on

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

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

Top comments (19)

Collapse
 
miketalbot profile image
Mike Talbot ⭐

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

Collapse
 
jonrandy profile image
Jon Randy 🎖️

GitHub link is 404?

Collapse
 
miketalbot profile image
Mike Talbot ⭐

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

Collapse
 
gollyjer profile image
Jeremy Gollehon

Do you expect this would work with React Native?

Collapse
 
miketalbot profile image
Mike Talbot ⭐

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?

Collapse
 
gollyjer profile image
Jeremy Gollehon

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.

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐

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.

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐ • Edited

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

Thread Thread
 
gollyjer profile image
Jeremy Gollehon

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

Collapse
 
miketalbot profile image
Mike Talbot ⭐

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

Collapse
 
rahulbhanushali profile image
Rahul Bhanushali

404 on the github link.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Now made it public - link is github.com/miketalbot/js-coroutines

Collapse
 
joshuaaron_ profile image
Josh Reynolds

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?

Collapse
 
miketalbot profile image
Mike Talbot ⭐

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!

Collapse
 
joshuaaron_ profile image
Josh Reynolds

Awesome, can't wait to read it!

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐

Hey Josh

I've posted the updated library info and the description of the process here.

M

Thread Thread
 
joshuaaron_ profile image
Josh Reynolds

Hey Mike, that is a brilliant write up, super in-depth and well explained!

Thank you again for this.

Collapse
 
thejayhaykid profile image
Jake Hayes

That is impressive! Well done man

Collapse
 
gabrielctroia profile image
Gabriel C. Troia

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 🤔