Ivan Akulov

Ivan Akulov

@iamakulov.com

Web perf engineer @ Framer. Prev. web perf consultant (Google, Appsmith, Toggl, etc). Getting React interactions 2-4x faster. GDE. He/him 🏳️‍🌈

854 followers74 following120 posts

View full thread

eg: - fetchpriority=low lowers the loading priority from medium to low (in Chromium). - loading=lazy makes the image lazy (duh). today, there’s no React primitive that you can use to tell React “just keep these images as-is, don’t change anything about them”. this feels like a weird API gap.

the context is we at @framer.com have our own React framework for Framer sites. we care about perf a lot, so we inject our own smart <link rel="preload"> tags and fetchpriority hints. React’s automatic preloads mess with that, and it’s really annoying to strip them out of HTML stream :(

the q is: i know react 19 automatically preloads images. i also know loading="lazy" and fetchpriority="low" opt out of that. but: would you folks consider creating a more explicit out-in or opt-out for this feature? the main problem is that these attributes *have other effects*.

common but surprisingly rarely mentioned react perf pitfall: 1) text changes its size when fonts load (duh, you see this every day). often it gets larger 2) lcp (= google’s loading speed metric) scans for the largest element on the page. but it scans only newly added elements. it doesn’t scan--

it doesn’t scan elements that were already there but got larger 3) when a react hydration mismatch happens, react removes all elements on the page and replaces them with (identically looking) new ones boom. text got larger + hydration mismatch = 💥

Possibly one of the most impressive Safari bugs I’ve run into lately: Going back (with a swipe) in *any* React app that uses Navigation API freezes the page for three seconds. What’s fun is the main thread is not frozen, only the rendered page is.

Kudos to a Framer customer for reporting a randomly freezing site. This one took two days to debug. Yes, we care about perf that much :)

TIL GTM has a History Change trigger, but this trigger doesn’t fire when you navigate via the new Navigation API :( There goes my dream of making all Framer sites use navigation.navigate() instead of history.pushState()

Ivan Akulov reposted

My Replay time-travel-based React+Redux perf reports experiments are coming together! Just nailed sourcemapped component + selector identities, and I've got accurate timing details on dispatches + renders from a Replay DevTools E2E recording! Early perf report example attached.

Mark Erikson@acemarke.dev

A week-ish later - it's working! Rebuilt my React+Redux dispatch+render perf POC on top of Replay's current infra. Currently extracting dispatch+render timings and selector calls . Just raw data atm, but now I can build meaningful visualizations / perf analysis with this! time travel's awesome!

the chart that I feel perhaps the proudest of is this framer’s lcp (aka page loading time), across all sites, going lower and lower day by day, free for all framer customers lots of work by our infra/canvas teams, @kurtextrem.de, and yours truly <3

ah yes it’s my favorite “iphone safari wraps every phone number with <a> → react has a hydration mismatch → it remounts the full dom → all images on the page flash, but only on iphone” type of day

View full thread

5) 2 weeks ago, we looked at these pages and realized we can make them faster. We still can’t optimize them – it’s too costly. But we can reuse something that other pages produce – for ~free. 6) What’s “something”? It’s the bundle (plus a few other things, like <link rel="modulepreload">s etc.).

7) So now, when a site is published, it remains unoptimized only for seconds. Once bundling for top pages completes, every other page on the site starts using that bundle (+ modulepreloads, etc)! It’s still not as fast as optimized pages – not until the 1st visit – but it’s much faster than before.

View full thread

4) This made optimization much faster! But this also meant a very small % of visitors would hit pages that are not bundled and not SSR-ed.

1) Framer sites are fully capable React apps 2) We used to SSR every page of every site on publish. As Framer grew, in 2025, this stopped scaling (a bundling + SSR + data injection pass, which we call “optimization”, would take minutes on large sites)

Yeah, Redux works great because it doesn’t import anything! Here’s a case when this breaks: 1) Add a random dependency in Redux that’s used in some random function:

Mark Erikson@acemarke.dev

I'll say that I do pre-bundle the Redux libs, and so far it's seemed to work fine as far as I can tell.

View full thread

Whereas if Redux *wasn’t* bundled, the bundler won’t even enter the file that imports fast-deep-equal. (This is thanks to `sideEffects: false` which Redux does.)

This doesn’t happen with every package btw. This happens with fast-deep-equal because it’s 1) written in CommonJS (so it’s not tree-shakable) and 2) doesn’t have `sideEffects: false` (so the bundler cannot ignore the package if its imports aren’t used). But most packages have one of those :(

Yeah, Redux works great because it doesn’t import anything! Here’s a case when this breaks: 1) Add a random dependency in Redux that’s used in some random function:

Mark Erikson@acemarke.dev

I'll say that I do pre-bundle the Redux libs, and so far it's seemed to work fine as far as I can tell.

2) Make an index.js that imports and uses a completely different function: import { compose } from "redux"; console.log(compose); 3) Bundle 4) Boom – fast-deep-equal is bundled, even though it’s not used

- esm > cjs modules (bundlers still don’t shake cjs well/at all) - sideEffects: false ftw - don’t bundle a library into a single file for npm. kills shakeability

Welp, turns out it’s not real. Just opening DevTools makes all timers 5-100× slower, due to the overhead of capturing stack traces. Even if you do nothing else (don’t record performance, etc)! Guess who accidentally spent a weekend optimizing this 😅 (h/t @paul.irish for explaining why)

Ivan Akulov@iamakulov.com

Why are native setTimeout and clearTimeout calls so expensive? Like, here it takes 1.5 ms. What’s the technical reason it’s so slow? Just 100-200 of those in a row (trivial if you’re mounting a bunch of React components that set timers) will easily block the page.

Benchmark: codepen.io/iamakulov/pe.... Try with DevTools open vs closed – especially with call stack depth 1000. (“1000 levels deep” might seem like a lot, but it’s pretty realistic with React’s recursivelyTraversePassiveMountEffects. That’s how I ran into it – in TanStack Query!)

Okay, so this is pretty wild: apparently, in Chromium, the deeper you are in the call stack, the slower your `setTimeout()` calls become? gist.github.com/iamakulov/85...

Ivan Akulov@iamakulov.com

Why are native setTimeout and clearTimeout calls so expensive? Like, here it takes 1.5 ms. What’s the technical reason it’s so slow? Just 100-200 of those in a row (trivial if you’re mounting a bunch of React components that set timers) will easily block the page.

Why are native setTimeout and clearTimeout calls so expensive? Like, here it takes 1.5 ms. What’s the technical reason it’s so slow? Just 100-200 of those in a row (trivial if you’re mounting a bunch of React components that set timers) will easily block the page.

Neat lil learning from today’s @perfnow.nl (via Umar Hansa): You can now throttle individual requests in Chrome! Neat for experimenting with stuff like - lazy-loading: does the UI look okay if this chunk takes MUCH longer to arrive - resilience: what happens if this specific CDN is extremely slow