Idle Until Urgent
philipwalton.com
philipwalton.com
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.
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.
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.
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.
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)
* A cooperative scheduler interrupts execution only when the task voluntarily yields control 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.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.
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.) await new Promise((resolve) => setImmediate(resolve))
or similar constructs using setTimeout or requestAnimationFrameThough setImmediate is specific to IE/Edge.
setTimeout(resolve, 0)
will allow any queued events/macrotasks to occur, and then immediately resolve. requireAnimationFrame(resolve)
will wait until the browser is ready to render another frame, and then resolve.> 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
I wonder what analytics platform is contributing all this slow-down to his first interaction... ;)
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.
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.
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!
However, scripts loaded before the `load` event do delay the load event, and analytics script are typically loaded with the lowest priority, so they're usually last and thus the ones you notice in the bottom-left corner of your window.
But the only way they'd be "blocking" anything is if the site was waiting for the load event to initialize any critical functionality (which it shouldn't be).
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.
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!
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.
Those numbers go back to the studies Jakob Nielsen did at the Sun Microsystems usability lab and the results posted in his oft cited AlertBox articles from 1993 and 1997...
Actually, Nielsen indicates that had already been a consistent finding for ~30 years at that point, citing “Response time in man-computer conversational transactions” (Miller, 1968) [0] as the original source.
[0] https://scholar.google.com/scholar?hl=en&as_sdt=0%2C5&q=Resp...
If 100ms strikes you as too high, then by all means target a lower number like 50ms. But it's still not a disaster if your 95th percentile is 100ms. Also, if your A time is above 16ms or your L time is above 5 seconds, your limited development time might be better spent improving those rather than bringing R down even further.
https://stackoverflow.com/questions/536300/what-is-the-short...
(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.
Modern pages with ads and widgets popping in potentially anywhere main remain unusable and unreadable because the main content keeps jumping around.
A good example of hyperacuity is in reading Vernier scales where you can see differences much below the angular resolution of the eye.
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);
}; > ['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 ]The design process of this, I imagine.
the point of UB is that you can tell your compiler to transform it into a specific behaviour -e.g., an assert with -fsanitize=undefined for instance.
Somehow, undefined behavior in C is a much smaller problem than it looks like. But the principle still applies.
Put differently, a property access x = foo.bar followed by x() is not the same as foo.bar()
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.
The reason for its size is my site is my playground. It's where I get to experiment with all the things I want to experiment with.
I also work on quite a few open source projects, which I usually test on my site before releasing them publicly just to make sure they work in production without errors.
I suppose the author just wants to know a little about how their blog and writing is performing in the wild
Sort of off topic, but it would be interesting if the browser handled these common cases instead and gave the user a way to opt-in/out. I suppose it sort of does by broadcasting those events to the js listeners in the first place.
- The [repo](https://github.com/GoogleChromeLabs/idlize) implementing the pattern described in the article
- The [package](https://yarnpkg.com/en/package/idlize)
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.
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.