v1.1.1 Async Rendering (setTimeout)

Leo Fabrikant
4 min readMay 23, 2019

--

In the previous iteration we’ve split the components:

But our attempt to make the SearchResults component queue up the expensive searchEngine work in the JavaScript event queue and run it on a separate execution stack failed. So how else can we try to queue up searchEngine.search?

There are two well known methods to set up asynchronous callbacks: Promises and setTimeout.

Promises

Lets see how that looks in the profiler:

(anonymous) is the Promise.then() callback

Foiled again!

Promise callbacks, while technically async in that they don’t run synchronously, are not called on a subsequent execution stack.

If you look at the profile closely, the callback is placed under the Run Microtasks section since Promise callbacks are considered a microtask. The browser usually checks for and runs microtasks right after it has finished executing the regular stack, kind of like a last second stow away at the end of the current execution.

Learn more: here’s a great presentation by Jake Archibald on all things JavaScript event loop that goes over microtasks and many of the topics I touch on in greater detail... Bookmark it and watch it later, highly recommend.

While this is good to know, it doesn’t solve our problem. We want a new execution stack so that the browser has a chance to paint before starting the new stack.

setTimeout

Survey says:

It’s hard to see because paints are so small, but there is a paint being highlighted where the blue line is.

Ahah! That did it! Notice that there’s two execution stacks: Event (keypress) and Timer Fired(searchResults.js:49). And there is a paint between the two stacks (where the blue line is). This is exactly what we theorized would happen! Lets see that beautiful UI in action!

Lag is especially noticeable when holding delete

Big improvement but it is still pretty disappointing… It has definitely gotten better, but there is still significant, noticeable UI lag. Let’s take a closer look at the profile.

Analyzing this particular profile and understanding what’s causing the lag is tricky. A couple of helpful notes on using the profile:

There are 2 especially useful Key Input Interactions that the profiler highlights, each giving you a different performance measurement:

Key Down: length of time from key press to kicking off the Event (keypress) handler.
Key Character: length of time from key press to painting an update in the browser.

Ideally the Key Down interaction is very short, because if there is nothing blocking the main JavaScript thread, the event should fire immediately after key press. So wherever you see longer Key Downs this is already a UI blocking issue.

In this case, the cause for the longer Key Downs is because they are triggered while the main thread is blocked by the expensive setTimeoutCallback. Take for example the last Key Down. It is unfortunately timed right at the start of the setTimeoutCallback which runs the expensive search method. Which means the Key Down can’t fire its event until the search calculation is done. For a sense of scale, in this profile those search methods are taking around 330 ms. That’s ⅓ of a second. This is a very long time to block the main thread in the world of good UI.

What’s especially interesting to me though is that last Key Character. Even after it’s associated Key Down finishes and fires an Event (keypress), the browser doesn’t paint the new searchTerm and runs another setTimeoutCallback instead. That is why that Key Character interaction takes twice as long.

This was, and still is, a big surprise to me. The whole point of moving the search to the setTimeoutCallback is to give the browser a chance to paint before launching the setTimeoutCallback. Yet there is no paint after that last Event (keypress) (where the blue line is.)

What we can conclude then is that we can’t rely on our theories about how the browser chooses queued tasks. It apparently does not always prioritize paints over timeout callbacks. And when your timeout callbacks can take 330ms on the main thread, this ends up blocking the thread for far too long and is bound to cause lag.

BONUS

Extra credit for anyone that can explain why the setTimeoutCallback is being prioritized over a paint.

I remember looking at these profiles for hours wracking my brain about what else could be done to improve performance. I was frustrated and I distinctly remember saying out loud in exasperation “God damn JavaScript and its single thread…”

and suddenly, an idea:

v1.2.0 Multi-threading

--

--