
Idle Until Urgent - bootslebaron
https://philipwalton.com/articles/idle-until-urgent/
======
alangpierce
> Important! While browsers can run input callbacks ahead of queued tasks,
> they cannot run input callbacks ahead of queued microtasks. And since
> promises and async functions run as microtasks, converting your sync code to
> promise-based code will not prevent it from blocking user input!

Wow, my initial reaction while reading was "just use async functions and then
then code will naturally allow user input in the middle", good to know that
that doesn't work.

~~~
hinkley
This is slowly turning into a rock in my shoe. You would think a single
threaded language would implement some sort of cooperative multitasking
scheme. But promises aren’t cooperative, especially in Bluebird (the queue
management logic was converted to LIFO a long time ago, although that code
still contained comments or variable names that imply the opposite, when last
I looked).

I’m tempted to call this the legacy of Brendan “design a language in a week”
Eich, but that would let too many other people off the hook.

IMO, webasm can’t happen fast enough.

~~~
paulddraper
> But promises aren’t cooperative

Promises are cooperative.

You cannot interrupt execution (unlike, say, preemptible threads).

You can only perform other execution when the current execution terminates or
yields, e.g. with await.

\---

I think you are wanting browser UI events to preempt between
Promises/microtasks.

That's fair enough, but saying that Promise aren't cooperative is the wrong
description.

~~~
hinkley
The difference between multitasking and cooperative multitasking is that you
can yield the CPU in the middle of a long process. You can do that in
Javascript but it involves combining multiple asynchrony APIs in complex ways.
Ways you probably don’t want to invite your team to use frequently.

You cannot split a large calculation in the middle by chaining promises to
allow even other promises to make progress, let alone event loop processing.

~~~
paulddraper
> The difference between multitasking and cooperative multitasking is that you
> can yield the CPU in the middle of a long process.

As long as "can yield" means "able to yield, and able to not yield". More
clearly put, the difference between preemptable and cooperative multitasking
is

* A preempting scheduler interrupts execution without requiring cooperation from the task [https://en.wikipedia.org/wiki/Preemption_(computing)](https://en.wikipedia.org/wiki/Preemption_\(computing\))

* A cooperative scheduler interrupts execution only when the task voluntarily yields control [https://en.wikipedia.org/wiki/Cooperative_multitasking](https://en.wikipedia.org/wiki/Cooperative_multitasking)

> You cannot split a large calculation in the middle by chaining promises

You can split up large calculations easily:

    
    
        (async() => {
            console.log('1');
            await (async() => {});
            console.log('2');
        })();
    
        (async() => {
            console.log('3');
            await (async() => {});
            console.log('4');
        })();
    
    

prints interleaved 1, 3, 2, 4. Calculation split!

\---

HTML5 defines a scheduling system ("tasks") on top of ECMAScript's job
("microtask") system.

Thus, browsers have a tiered scheduling system. And DOM events happen at the
higher task level.

In a browser, if you want to cooperatively schedule at the task level,

    
    
        (async() => {
            console.log('1');
            await new Promise(resolve => setTimeout(resolve));
            console.log('2');
        })();
    
        (async() => {
            console.log('3');
            await new Promise(resolve => setTimeout(resolve));
            console.log('4');
        })();
    

You could argue that HTML5 should not have created tiered queues. Perhaps you
are correct, though I think it can come in handy.

~~~
hinkley
I'm not sure what you trying to demonstrate here. It's not the problem I'm
talking about. For starters, you have no calculation. You're just running a
couple awaits and setTimeouts. Of course those are going to run in 1,3,2,4
order.

The question is how would you make sure 4 happens before 2?

Here's a real world example, from Node: You need to make 3 service calls, A B
& C, to build a page. Service A is the fastest call, but takes a lot of
processing time. Service C is the slowest call, but requires a bit of data
from Service B. Since A and B are unrelated, odds are good they're being
invoked from completely separate parts of the code.

If you fire A and B, service C won't get called until service A's processing
is complete. You could await both A and B, then call C before you start the
processing, but you have to turn your code flow inside out to do that, so it
only works for trivial applications.

Adding promise chaining to A.process() won't get B's promise to resolve before
A's chain finishes resolving. setImmediate() might work in some places, and
you might be able to come up with a code pattern that works for your team, but
I don't believe it's guaranteed to work everywhere.

~~~
paulddraper
My initial point what that your terms are muddled.

Your hypothetical could benefit from a preemptable (aka _non-cooperative_ )
scheduler, which can forcibly interrupt A, to allow C to start.

A _cooperative_ scheduler (which is what JS has) is at the mercy of A to
properly yield.

\---

As for how to yield on the macro- or microtask queue of your choice, they are
the same difficulty to write.

    
    
       // HTML5, Node.js
       await new Promise(resolve => resolve());
       await new Promise(resolve => setTimeout(resolve, 0));
    
       // Node.js
       await new Promise(resolve => resolve());
       await new Promise(setImmediate);
    

You're correct that setTimeout and setImmediate are not guaranteed to work on
all ES runtimes, because they are HTML5 and Node.js specific additions. (As is
the entire concept of a separate macrotask queue, which you dislike so much.)

------
sequoia
[cw: tangential point]

> This is the JavaScript equivalent of death by a thousand cuts.

Seems to me there's one really big knife in particular, doling out most of the
cuts: [https://i.imgur.com/Hzrfq13.png](https://i.imgur.com/Hzrfq13.png)

I wonder what analytics platform is contributing all this slow-down to his
first interaction... ;)

~~~
philipwalton
Article author here. Yep, not hiding that fact (I could have easily used a
trace with minified code, but I didn't to point this out).

Two things though:

1\. I used to work on Google Analytics, and I've created a lot of open source
libraries around Google Analytics, which I use on my own site because I like
to test my own libraries (and feel any pain they may be causing). The way most
people use Google Analytics does not block for nearly this long.

2\. I've updated my Google Analytics libraries to take advantage of this
strategy [1], and I'm working with some of my old teams internally to see if
they can bake it in to GA's core analytics.js library, because I strongly
believe that analytics code should _never_ degrade the user experience.

[1]
[https://github.com/googleanalytics/autotrack/pull/235](https://github.com/googleanalytics/autotrack/pull/235)

~~~
sequoia
> Yep, not hiding that fact (I could have easily used a trace with minified
> code, but I didn't to point this out).

Kudos to you for your honesty here! I was a bit confused by your question "So
what’s taking so long to run?" when it seemed pretty clear what was taking so
long to run. If the goal were simply "speed up the pageload/FID", removing
browser analytics (in favor of server e.g.) would seem to be at least an
_option_ to immediately achieve that end.

Thanks for the article.

~~~
philipwalton
Right, when I said "what's taking so long to run?", in my mind I was thinking
there'd be one obviously slow thing that I could just remove or refactor, but
it turned out that it wasn't any _one_ single slow function/API causing the
problem.

And yes, clearly removing the analytics code would have also solved the
problem for me, and in many cases, removing code is the best solution.

In this particular case I couldn't remove any code because I was refactoring
an open source library that a lot of people use. I wanted to try to make it
better for input responsiveness in general, so people who use the library (and
maybe don't know much about performance) will benefit for free.

Also, I wanted to help educate people about how tasks run on the browser's
main thread, and how certain coding styles can lead to higher than expected
input latency.

Anyway, glad you enjoyed the post!

------
enobrev
I don't think this comment matters, but since so many other comments on this
article are speaking up against, maybe it does.

I don't care, at all whatsoever, if this person wants to use JavaScript for
their blog.

Otherwise, nice article about improving performance and prioritizing important
functionality during page-load.

~~~
craftyguy
> I don't care, at all whatsoever, if this person wants to use JavaScript for
> their blog.

In most cases complaining about this is 'offtopic', but in this case it is
very much on-topic since the blog _is_ about optimizing javascript usage.

I'm just happy that you can actually read what they have to say without JS
enabled!

------
AstralStorm
I'm not sure why you trust Google Web "Fundamentals" that 0-100ms is perceived
as instant.

The upper bound is perceptibly laggy. I have no idea where Google took their
numbers from.

FID of 100 ms is already bad as you have to add network latencies on top of
it.

To put things into perspective, it's more than the time it takes to fully boot
embedded Linux as coreboot from not too fast flash or start up Commodore 64
with a good extension cartridge.

The browsers are terribly slow.

~~~
treerock
r.e. the 100ms, some references are given on this stackoverflow question.

[https://stackoverflow.com/questions/536300/what-is-the-
short...](https://stackoverflow.com/questions/536300/what-is-the-shortest-
perceivable-application-response-delay#2547903)

~~~
Xichom2k
I don't think scroll gestures or incrementally-loaded content with layout
reflows were a thing back then, so that might need re-evaluating.

~~~
AstralStorm
I'm pretty sure layout reflows were considered, as HTML rendering itself is
incremental in almost all sensible browsers.

(Thus the ideas of embedding CSS and JS in localized pieces where applicable.)

Scroll gestures were not a thing typically, you just did a full request.

~~~
Xichom2k
Sure, it's incremental, but static site layouts, especially the old float-
based ones will have had their headers and sidebars loaded from the start and
the main content would not jump around.

Modern pages with ads and widgets popping in potentially anywhere main remain
unusable and unreadable because the main content keeps jumping around.

------
jgtrosh
Reading this article and applying a similar technique to a different webpage
could be a good exercise for advanced students in front end development. The
core idea is put forward, and no implementation detail is left out; great
article.

------
theandrewbailey
I guess I'm an old man that hasn't smoked the hype, but I don't understand why
one would write this:

    
    
        const main = () => {
            setTimeout(() => drawer.init(), 0);
            setTimeout(() => contentLoader.init(), 0);
            setTimeout(() => breakpoints.init(), 0);
            setTimeout(() => alerts.init(), 0);
            requestIdleCallback(() => analytics.init());
        };
    

over this:

    
    
        function main(){
            setTimeout(drawer.init, 0);
            setTimeout(contentLoader.init, 0);
            setTimeout(breakpoints.init, 0);
            setTimeout(alerts.init, 0);
            requestIdleCallback(analytics.init);
        };

~~~
rbonvall
JavaScript is a minefield of unexpected behavior when you try things like
this. Besides the issue with this-binding already mentioned in the thread,
there are other examples of weird stuff that happens when you don't introduce
an apparently redundant lambda:

    
    
        > ['10', '10', '10', '10', '10'].map(parseInt)
        [ 10, NaN, 2, 3, 4 ]
    
        > ['10', '10', '10', '10', '10'].map(x => parseInt(x))
        [ 10, 10, 10, 10, 10 ]

~~~
kristjansson
> I’ve heard something about a surprise principle in API design. Principle of
> most surprise I think it was? Let’s go with that.

The design process of this, I imagine.

~~~
Serow225
I lol'd. Downvote away haters, sometimes you gotta laugh.

------
thegeomaster
What I don't understand is why a very simple blog like this needs a ~56KB
bundle of JavaScript at all.

~~~
mrspeaker
Looking at the blog post, I'd say it was for drawer.init(),
contentLoader.init(), breakpoints.init(), alerts.init(), and analytics.init().
The site works fine without javascript, so it doesn't _need_ it - perhaps the
author thinks the 50k is worth the drawer, content loading, breakpoint?, and
alert features and also wants a bit of user analytics tracking.

~~~
AstralStorm
Even so, you could backload the drawer etc. placing them at the end of the
document and attaching to potentially already rendered page.

Reorder so that your main content loads first.

Author did part of it by deferring the analytics init. Not sure why they used
setTimeout though for the initialization instead of requestIdleTimeout like
everything else. The page is supposed to work without JS after all.

"I mentioned above that requestIdleCallback() doesn’t come with any guarantees
that the callback will ever run."

Not true - there's a timeout argument. It guarantees that the callback will by
ran by then.

------
benjaminjackman
Here is:

\- The
[repo]([https://github.com/GoogleChromeLabs/idlize](https://github.com/GoogleChromeLabs/idlize))
implementing the pattern described in the article

\- The
[package]([https://yarnpkg.com/en/package/idlize](https://yarnpkg.com/en/package/idlize))

------
code-is-code
A few month ago, I used a similar optimisation that does the same idle-
awaiting but with server-requests instead of cpu-usage. So instead of
submitting all ajax to the server directly, background tasks can be delayed
until the important tasks have finished. This is useful especially when the 6
requests per origin limit gets hit often.

[https://github.com/pubkey/custom-idle-
queue](https://github.com/pubkey/custom-idle-queue)

------
iofiiiiiiiii
Not being a JavaScript guy, my eyes sort of glazed over after the flame
graphs. Do I understand the gist of it right that a 200+ millisecond delay is
normal if you just have 56KB of light blog page style JavaScript code whose
loading you do not somehow optimize? Or is there something pathological in
play here?

~~~
alanning
It depends on what the JavaScript code is doing.

The article discusses an example where code is loading Intl.DateTimeFormat
which takes some time but is not immediately used.

So if the 56KB code doesn’t do a lot of loading of expensive components then
it may not need further optimization, although it may still have the problem
of blocking user input.

Main moral of the story is you can’t assume performance based on code size,
you have to measure.

------
eridius
If the measurement here is how long it takes to respond to the first user
input, why does a 233ms main function matter? As a user, how am I expected to
scan the page, locate a link, and click on it within that 233ms?

~~~
jschwartzi
Imagine that his site was linked somewhere else. In that scenario it takes
233ms from click to display. That's why this matters, because users aren't
opening a browser to that one page. They're clicking around on different
pages, and if each one takes 233ms that's a very slow process.

------
bovermyer
While this is impressive work, I can't help but wonder if this is solving the
wrong problem.

If the goal is to load fast, wouldn't it be better to just stop using
JavaScript altogether? It's a blog, not an app.

~~~
alangpierce
I don't think the author is suggesting that people should put this much effort
into optimizing their blog. It's a toy example that's simple enough to explain
concepts that can then be applied to bigger and more complex webapps where
"don't use JavaScript" isn't an option, like with the Redux example mentioned
further down in the article.

------
elfakyn
The Amazon app has an absolutely horrendous FID of around 10 seconds or so.
They could use a little bit of optimization.

------
a012
Meanwhile, there are many websites don't care if their page loads in a few
seconds. For example:
[https://i.imgur.com/coBKPa1.jpg](https://i.imgur.com/coBKPa1.jpg)

