> 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.
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.
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.
> 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
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.
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.)
Yeah this is often misunderstood. Instead of thinking that promises make your code "run later" it's better to think about it as reorganizing still-synchronous code.
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.
> 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.
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.
It's likely not "blocked" by analytics since pretty much all analytics libraries get loaded async.
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).
Shouldn't, but this is absolutely common in the wild. Another one I get often (which makes me need to disable my ad blocker) is UI actions that first log the action in X analytics library and then execute the function. When X library is blocked, the code path never reaches the function. Off the top of my head, flight/hotel reservation websites are particularly bad about this.
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.
> I'm not sure why you trust Google Web "Fundamentals" that 0-100ms is perceived as instant.
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...
> 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.
Getting offtopic here but arguably the best thing about the growing prevalence of VR is that it's forcing everyone to focus on maximum latency rather than frame throughput, and so that godawful stutter is finally being exorcised from our rendering stacks.
100ms is slightly arbitrary, but the real point of saying "100ms" is that it's lower than the multiple seconds typically required for page load, and higher than the 16ms required for smooth animation. An appropriate target for input response time (the "R" in RAIL) is somewhere in between those two.
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.
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.
> ...a team of neuroscientists from MIT has found that the human brain can process entire images that the eye sees for as little as 13 milliseconds...That speed is far faster than the 100 milliseconds suggested by previous studies...
This is easy to internalize when you remember that humans can tell the difference between 30fps and 60fps which is one frame every ~33ms vs every ~17ms
You can tell the difference between 144hz and 60hz, especially if user input is involved (eg: using mouse to look around). The resolution of human skill depends heavily on the exact scenario as the body is complex. Even with eyes you have high density in the center of vision, low density outside, and they have different light sensitivities which makes them hard hard to model. To further complicate matters is the brain that does further processing that can result in hyperacuity.
A good example of hyperacuity is in reading Vernier scales where you can see differences much below the angular resolution of the eye.
Humans can tell the difference between 1000fps and 2000fps too (with a test signal of a point light source flickering at >500Hz, and viewed with rapid eye movement to produce the phantom array effect. Note that temporal anti-aliasing is needed to avoid cheating with easily visible beat patterns.) This doesn't mean we can process an image in 0.5ms.
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.
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:
Ok this was a 'wat' moment for me until I realized that map passes up to 3 args to the function, including index, and parseint accepts a radix parameter. Still kinda weird/unintuitive, I could see myself not paying attention and reducing to the first example accidentally.
Huh, I'm the opposite. I'd gladly take a confusing combination of consistent spec'd behavior over time travel, arbitrary code execution, and nasal demons.
The 2nd style will not work if those functions internally use `this` and were not previously manually bound to their parent objects, like `drawer.init.bind(drawer)`.
Due to javascript's funky notion of object methods. If you just pass the function reference itself invoking it will execute it with a |this| set to undefined.
Put differently, a property access x = foo.bar followed by x() is not the same as foo.bar()
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.
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.
Thank you. That'd be like someone criticizing a coding example for a new language saying "What's the point of using OOP for implementing Tic-Tac-Toe? It's too simple to necessitate it."
I'm not criticizing the technique from the article. It's useful. I'm just expressing my confusion that the blog, simple as it looks, needs this JavaScript (and analytics are in a separate file from the one I'm referencing, it appears).
That 56K number isn't gzipped, gzipped it's only 18K (plus there's also some inline JS, some webpack boilerplate, and then analytics.js).
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.
^ exactly; blogs are mostly static. You can improve the responsiveness of e.g. a comments section with a bit of JS, but that's very low priority and doesn't need to be much.
I poked around a little and it seemed like the majority of it is related to analytics events (googleanalytics/autotrack)
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.
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.
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?
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 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?
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.
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.
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.