Ivan Akulov's Avatar

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 πŸ³οΈβ€πŸŒˆ

804
Followers
71
Following
99
Posts
02.06.2023
Joined
Posts Following

Latest posts by Ivan Akulov @iamakulov.com

Post image

Story behind the improvement:

23.02.2026 19:41 πŸ‘ 2 πŸ” 0 πŸ’¬ 0 πŸ“Œ 0
Post image

Tomorrow, when you publish a Framer site, every page’s HTML will get 10-20% smaller:

23.02.2026 19:41 πŸ‘ 1 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0

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.

14.02.2026 20:48 πŸ‘ 1 πŸ” 0 πŸ’¬ 0 πŸ“Œ 0

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.).

14.02.2026 20:48 πŸ‘ 0 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0

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.

14.02.2026 20:48 πŸ‘ 1 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0
Preview
Framer Updates β€” Dynamic Optimization Introducing Dynamic Optimization, an all-new way of optimizing your published websites. All sites now optimize in seconds, even large ones. And adding pages has no impact on optimization time. Pages are now optimized on first visit, and we cache the result until the next publish. Your most visited pages are pre-optimized on publish, ensuring consistent performance and full visibility into any issue. This makes Framer’s very best feature, instant publishing, even better. We’re rolling this out gradually throughout October 2025. Publishing, even faster. Only in Framer.

3) In October, we rewrote our optimization stack – and made it so that on publish, only the most visited pages are optimized. Less popular ones would get lazily handled after the first visit. (H/t our Piotr Krawiec, who single-handedly did the whole rewrite: www.framer.com/updates/dyn...)

14.02.2026 20:48 πŸ‘ 1 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0

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)

14.02.2026 20:48 πŸ‘ 0 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0
Video thumbnail

Last week in Framer performance: the first view of unoptimized pages is now much faster, especially on large sites:

14.02.2026 20:48 πŸ‘ 0 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0

Responded! bsky.app/profile/did:...

And learned that this doesn’t happen with every dep, only with some of them :D

(Is it a bad taste to quote-reply on bluesky btw? I haven’t posted much in a while lol, might’ve missed the memo)

26.11.2025 23:44 πŸ‘ 4 πŸ” 0 πŸ’¬ 0 πŸ“Œ 0

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 :(

26.11.2025 23:42 πŸ‘ 2 πŸ” 0 πŸ’¬ 0 πŸ“Œ 0

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.)

26.11.2025 23:42 πŸ‘ 1 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0
Post image

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

26.11.2025 23:42 πŸ‘ 0 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0

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:

26.11.2025 23:42 πŸ‘ 2 πŸ” 0 πŸ’¬ 1 πŸ“Œ 1

- 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

26.11.2025 05:36 πŸ‘ 4 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0

spent the day shaking the tree, shook 1.5 MB

26.11.2025 05:36 πŸ‘ 4 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0
Preview
General philosophy and vision Β· Issue #13 Β· tc39/proposal-bigint-math Original post Spinning this out of #8 (comment) and #9 (comment). There are two dueling philosophies we could take for this proposal. β€œBigInts and Numbers should always be interchangeable by defaul...

Turns out this is why! github.com/tc39/proposa...

16.11.2025 20:36 πŸ‘ 0 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0
Post image

just why

16.11.2025 17:55 πŸ‘ 1 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0

Using PerformanceObserver or manual timers/logs, I guess :D

Logging into the console while it’s closed still works

(But yup, somewhat annoying πŸ™ˆ)

14.11.2025 14:09 πŸ‘ 3 πŸ” 0 πŸ’¬ 0 πŸ“Œ 0

So turns out none of this was real

(which was pretty hard to tell because the only way to tell was to close DevTools lol)

14.11.2025 03:43 πŸ‘ 3 πŸ” 0 πŸ’¬ 0 πŸ“Œ 0
Post image

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!)

14.11.2025 03:42 πŸ‘ 1 πŸ” 0 πŸ’¬ 0 πŸ“Œ 0

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)

14.11.2025 03:42 πŸ‘ 31 πŸ” 5 πŸ’¬ 3 πŸ“Œ 1

this needs to nerdsnipe someone from Chromium to investigate this :P

10.11.2025 17:12 πŸ‘ 6 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0
Post image

In the app I spotted this issue in (a typical complex React app), these setTimeouts were ~1500 calls down the `recursivelyTraversePassiveMountEffects` stack

10.11.2025 17:12 πŸ‘ 2 πŸ” 0 πŸ’¬ 0 πŸ“Œ 0

Okay, I noticed that I cannot repro these slow setTimeouts outside of React / TanStack Query.

So I tried to profile this, and apparently setTimeout calls are much (10Γ—) slower if you do them deep within a call stack (eg React’s recursivelyTraversePassiveMountEffects)? bsky.app/profile/did:...

10.11.2025 17:06 πŸ‘ 4 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0
Post image

setTimeout calls also become 2Γ— slower if you have previously set ~750 timers (doesn’t matter whether they already fired):

10.11.2025 16:58 πŸ‘ 4 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0

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...

10.11.2025 16:58 πŸ‘ 14 πŸ” 0 πŸ’¬ 2 πŸ“Œ 1

hahaha, full circle :D

10.11.2025 15:57 πŸ‘ 1 πŸ” 0 πŸ’¬ 0 πŸ“Œ 0

I might publish the impl I posted in that issue as an npm package!

10.11.2025 15:56 πŸ‘ 3 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0

(Question prompted by spending an hour to implement userland setTimeout batching :D)

09.11.2025 22:19 πŸ‘ 2 πŸ” 0 πŸ’¬ 1 πŸ“Œ 0
Post image

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.

09.11.2025 22:19 πŸ‘ 13 πŸ” 1 πŸ’¬ 3 πŸ“Œ 2