> It also seems to be a metric that is very easily gamed.
Fun fact: the current JS-specific metric (which is being fazed out) is First Input Delay, and it was explicitly designed to avoid this gaming:
> FID only measures the "delay" in event processing. It does not measure the event processing time itself nor the time it takes the browser to update the UI after running event handlers. While this time does affect the user experience, including it as part of FID would incentivize developers to respond to events asynchronously—which would improve the metric but likely make the experience worse.
> - https://web.dev/fid/
I wonder why they decided to reconsider this trade-off when designing INP.
This is correct (source: working in web perf for 5 years).
INP is the time between you click/press a key/etc and the moment the next paint happens. It’s only measured for on-page interactions, not for navigations.
AFAIK (correct me if I’m wrong!) first-party and third-party codecs in media players are kinda equal: you load a file, and the media player picks which codec to delegate that file to. You can’t do that in with a Chrome extension – the extensions can manipulate HTML and run custom JS, but that’s more or less it (plus some basic browser-level stuff like work with tabs and intercept network requests).
This doesn’t mean an extension isn’t possible at all – eg there’s https://github.com/zamfofex/jxl-crx/ that detects JXL images and decodes them with JS – but that’s slower.
Plus the bigger issue is adoption. You want all users to get your JPEG XL images, not just the 0.1% of users that installed the extension.
This fall, I was lucky to help Causal (a web spreadsheet for forecasting) with speeding up their frontend app. Here are some of my favorite findings:
- Turns out when React DevTools tell you “the component rerenderd because hook no. 2 changed”, it doesn’t mean that it’s hook no. 2 that actually changed! That’s because a) `useContext` hooks aren’t counted, b) only native hooks are counted – so if you use one `useSelector`, but it has 8 native hooks inside, you’ll see something like “hook no. 7 changed”. You need to go to the Components pane and look for hook numbers there.
- Web workers are cool but hard to make use of :/ In this case, we tried moving an operation into a web worker but weren’t able to because copying data between threads took longer than the actual op. And one doc I’ve read earlier still fascinates me – in 2018, Google explored moving frameworks into web workers but stumbled upon many limitations [1].
- React (unfortunately) still causes a lot of performance issues. It’s too easy to rerender something unnecessarily; and in a large app, many cheap components easily add up into an expensive render. I’m looking forward to React Forget [2], hopefully, solving this (at least partially?)
(Co)author here - happy to answer any questions :)
- This was a v fun (and challenging, due to Causal's scale!) project to do
- Most optimizations we’ve implemented were (very effective) one-off fixes. But we were also glad to find some rules of thumb that help to keep the app faster by default [1]
- My most interesting discovery was how ineffective Web Workers are. Causal, sometimes, has to do some expensive client-side data parsing. We tried to move data parsing to a web worker (= a separate thread) – but ended up reverting the optimization because it made the app slower. In JS, you can’t just pass a pointer to a data structure between threads; the browser has to clone the data structure [2] (with limited exceptions [3]). In our case, cloning the data between threads ended up being several times slower than parsing it in the main thread.
Hey Hacker News! I’m a long-time user of Notion (an advanced note-taking app). Notion’s web app has been quite slow for me, so I decided to reverse-engineer it and see why. Here’s a case study with findings and recommendations!
TL;DR:
* Notion has a large startup time because they serve a lot of JavaScript. This JS has to be parsed, compiled, and executed – and this takes time. This is especially slow on Android phones [1].
* The obvious solution to this is code splitting. But in case of Notion, there’re a few other low-hanging fruits. For example, if you switch to CommonJS, you can (surprisingly) speed up the startup by deferring unnecessary modules [2]. Or, there’s a bunch of unnecessary libraries in the bundle (including 3 duplicates of CoreJS).
* Apart from JS execution, there’re also a few other issues. Analytics is loaded early – and it defers loading and execution of other, more important resources [3]. And page resources don’t have `Cache-Control` headers being set, which could lead to different caching behavior in different browsers.
Overall, based on my measurements and some guesses, I think Notion can cut around 30% off their initialization time – simply by tuning some configs and deferring some loading.
So, a couple months ago, I discovered a way to make a text that uses Google Fonts render faster. In slow connections, that might give a boost of 1-3 seconds. Recently, I released that as a tool.
Here’re more details:
— When you use custom fonts (like Google Fonts), most modern browsers won’t render the text with that font immediately. Instead, the text will stay invisible until the font is downloaded – or until 3 seconds pass. You might’ve experienced this yourself if you have an Android phone. This is done to prevent a FOIT (flash of invisible text), but this hurts user experience.
— A typical solution for this is to add a CSS rule like font-display: swap which tells the browser to avoid waiting for the custom font. However, Google Fonts don’t support this rule, and the only remaining solution becomes to a) self-host fonts (more hassle; impossible in some environments) or b) use JS libraries like Typekit Webfont Loader (hurts the initial font load time)
— Recently, I found a solution for this. Turns out, you can fetch a Google Fonts stylesheet asynchronously, patch it to include font-display, and insert it into the page. So I composed a tool that does it! The tool generates an inline script that does the work + a few <link rel="preload/preconnect"> tags to avoid hurting the performance. The script also does some extra work to avoid unnecessary FOUTs. The total size of the generated code snippet is 550 bytes minified + gzipped.
So, a few weeks ago, I discovered a way to make a text that uses Google Fonts render faster. In slow connections, that might give a boost of 1-3 seconds. Some backstory:
* When you use custom fonts (like Google Fonts), most modern browsers won’t render the text with that font immediately. Instead, the text will stay invisible until the font is downloaded – or until 3 seconds pass. You might’ve experienced this yourself (if you had an Android phone and used a slow connection once). This is done to prevent a FOIT, but this hurts user experience.
* A typical solution for this is to add a CSS rule like `font-display: swap` which tells the browser to avoid waiting for the custom font. However, Google Fonts don’t support this rule, and the only remaining solution becomes to
a) self-host fonts (annoying)
b) use JS libraries like Web Font Loader (hurts the initial font load time; pretty complicated for one task)
* Recently, I found a solution for this. Turns out, you can fetch a Google Fonts stylesheet asynchronously, patch it to include font-display, and insert it into the page. So I composed a project that does it! To avoid hurting the performance, the project generates an inline script and a few `<link rel="preload/preconnect">` tags. The script also does some extra work to avoid unnecessary FOUTs. The total size of the code snippet is 550 bytes minified and gzipped.
So, in summer, I gave an introductory talk into web performance. This is its textual version :)
The talk includes 94 slides with text about:
— why web performance matters
— how to optimize:
— JS (async/defer, code splitting)
— CSS (the critical CSS approach & tools for it)
— HTTP/connection stuff (Gzip/Brotli, preloading, CDN)
— Images (compressing images, webp, progressive vs baseline images)
— Fonts (the `font-display` trick)
— and what tools help to understand your app’s performance
Fun fact: the current JS-specific metric (which is being fazed out) is First Input Delay, and it was explicitly designed to avoid this gaming:
> FID only measures the "delay" in event processing. It does not measure the event processing time itself nor the time it takes the browser to update the UI after running event handlers. While this time does affect the user experience, including it as part of FID would incentivize developers to respond to events asynchronously—which would improve the metric but likely make the experience worse. > - https://web.dev/fid/
I wonder why they decided to reconsider this trade-off when designing INP.