Sign in

Ivan Akulov

@iamakulov.com
854 followers 74 following 118 posts

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

PostsRepliesMedia
Ivan Akulov @iamakulov.com · 22/09/2026
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 = 💥
100
Ivan Akulov @iamakulov.com · 15/07/2026
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.
3615
Ivan Akulov @iamakulov.com · 06/07/2026
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()
120
Ivan Akulov @iamakulov.com · 14/04/2026
Doctor: Chrome DevTools throttling is real and can hurt you Chrome DevTools throttling: [proceeds to not be real]
110
Ivan Akulov @iamakulov.com · 27/03/2026
Something I worked on (with a few other wonderful folks) for the past few weeks :) www.framer.com/llm
010
Ivan Akulov @iamakulov.com · 18/03/2026
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
020
Ivan Akulov @iamakulov.com · 17/03/2026
next.js (pages router) loads data for pages you navigate to, but never releases it, whoops: github.com/vercel/next...
120
Ivan Akulov @iamakulov.com · 17/03/2026
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
1242
Ivan Akulov @iamakulov.com · 23/02/2026
Story behind the improvement:
020
Ivan Akulov @iamakulov.com · 23/02/2026
Tomorrow, when you publish a Framer site, every page’s HTML will get 10-20% smaller:
110
Ivan Akulov @iamakulov.com · 14/02/2026
Last week in Framer performance: the first view of unoptimized pages is now much faster, especially on large sites:
100
Ivan Akulov @iamakulov.com · 26/11/2025
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
100
Ivan Akulov @iamakulov.com · 26/11/2025
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:
120
Ivan Akulov @iamakulov.com · 16/11/2025
just why
110
Ivan Akulov @iamakulov.com · 14/11/2025
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!)
010
Ivan Akulov @iamakulov.com · 14/11/2025
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)
3315
Ivan Akulov @iamakulov.com · 10/11/2025
In the app I spotted this issue in (a typical complex React app), these setTimeouts were ~1500 calls down the `recursivelyTraversePassiveMountEffects` stack
020
Ivan Akulov @iamakulov.com · 10/11/2025
setTimeout calls also become 2× slower if you have previously set ~750 timers (doesn’t matter whether they already fired):
140
Ivan Akulov @iamakulov.com · 10/11/2025
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...
2140
Ivan Akulov @iamakulov.com · 09/11/2025
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.
3131
Ivan Akulov @iamakulov.com · 30/10/2025
Currently in Chrome Canary behind a flag (enable at chrome://flags):
020
Ivan Akulov @iamakulov.com · 30/10/2025
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
2269
Ivan Akulov @iamakulov.com · 24/10/2025
So, another big part of this project was moving this logic back into the schema, to make sure it covers all code paths. This wasn’t always easy! Some things, like stripping unnecessary <b>s that come from Google Docs, were simple:
100
Ivan Akulov @iamakulov.com · 24/10/2025
Over time, each of these code paths grew a bunch of custom logic: • ⌘V would do extra sanitization for Google Docs (to strip e.g. <b>s that it puts around everything ↓) • HTML import/export would convert code blocks from <pre><code> into <template> (and back) • etc
100
Ivan Akulov @iamakulov.com · 24/10/2025
3️⃣ In Framer, there are multiple ways to get text into a text editor: • You can type something, adding images or tables with a button • You can ⌘V something from Notion, Google Docs, etc. • You can import HTML with plugins
100
Ivan Akulov @iamakulov.com · 24/10/2025
To fix this, we used “progressive enhancement”. We’d still serialize code blocks into <template></template> that Framer uses. But inside that <template>, we’d put a <pre><code> tag that all other editors understand ↓
100
Ivan Akulov @iamakulov.com · 24/10/2025
2️⃣ Code blocks. How hard can that be? A month ago, if you tried to copy a code block from Framer to Notion, it would just not paste. It would be missing! Reason? We serialized code blocks into our internal format, which other editors did not understand:
100
Ivan Akulov @iamakulov.com · 24/10/2025
Solving this was tricky. The solution was clear (use separate schemas for CMS and Canvas). But, over the years, our code grew to rely on having a single global schema! So shipping this required some careful API redesign + a lot of iteration on making it type-safe. E.g., one intermediate design:
100
Ivan Akulov @iamakulov.com · 24/10/2025
1️⃣ And here comes the first challenge. Framer has several separate text editors (CMS, canvas, etc). For ✨historical reasons✨, all these editors used *the same schema*. So if you added image support to CMS, you’d inadvertently add it to *every editor*. This led to cute bugs:
100
Ivan Akulov @iamakulov.com · 24/10/2025
0️⃣ Framer, like many other apps, uses ProseMirror for its rich text editor. Where React lets you manipulate the Virtual DOM and applies changes to the real DOM, ProseMirror lets you manipulate its document structure and applies changes to the real <div contenteditable>:
100
Ivan Akulov @iamakulov.com · 24/10/2025
This lil boy (www.framer.com/updates/cms...) actually took multiple weeks of full-time work! That’s because we didn’t only fix copy-paste papercuts, we also took an opportunity to improve Framer’s text editing architecture. Story time!
220
Ivan Akulov @iamakulov.com · 21/10/2025
also more: oxc.rs/blog/2025-1...
010
Ivan Akulov @iamakulov.com · 21/10/2025
til oxlint does some insane tricks (custom js ast interface that understands rust memory layout!) to make js plugins fast github.com/oxc-project...
140
Ivan Akulov @iamakulov.com · 20/10/2025
note that "eagerness": "moderate" is a bit bleh because it only starts preloading after you scroll [1]. once [2] gets shipped, you should switch to "eager". [1] issues.chromium.org/issues/4432... [2] issues.chromium.org/issues/4367...
110
Ivan Akulov @iamakulov.com · 20/10/2025
something very cool to see: Chrome recently shipped automatic <link rel="prefetch"> for all links in viewport you could do it previously with libs like Quicklink, but it’s super cool to see this implemented natively (it’s off by default, enabled with one html tag)
Screenshot of a table titled “Proposed plan”, showing some updates to speculative rules, from the doc linked in the next post
283
Ivan Akulov @iamakulov.com · 08/10/2025
i mean, you can’t say it doesn’t help
010
Ivan Akulov @iamakulov.com · 08/10/2025
cooking some more...
010
Ivan Akulov @iamakulov.com · 08/10/2025
feels so good to be writing something again huh
screenshot of an intro to a “Why you should ditch <Link>s and use <a>s”  article. it says:

The standard advice for React/Vue/Next.js sites is to use the framework’s router to navigate between pages. A single-page router comes with a lot of goodies: prefetching, keeping state between routes, integration with framework primitives (like server components), etc. So why am I, in the year of 2025, think it’s worth switching back to `<a>`s from the 90s?
060
Ivan Akulov @iamakulov.com · 01/10/2025
The hot-off-the-press URLPattern (developer.mozilla.org/en-US/docs/...) solves this perfectly btw
080
Ivan Akulov @iamakulov.com · 01/10/2025
The gotcha is `new URL()` %-encodes everything that’s not Latin. Tricky for parsing! And tricky to spot in testing if your test data is English. (Solution: hire Brazilian engineers)
170
Ivan Akulov @iamakulov.com · 01/10/2025
Got bitten by `new URL()` today. Guess why this doesn’t work?
160
Ivan Akulov @iamakulov.com · 30/09/2025
soon in @Framer.com: much better ⌘C ⌘V with google docs, notion, craft, and every other editor out there (my lil non-performance side project)
150
Ivan Akulov @iamakulov.com · 12/06/2025
The challenge with Lighthouse is when you test a site “on a slow network”, it (by default) doesn’t *actually* load the site on a slow connection. Instead, it uses a fast connection and then tries to compute how the site would load on a slower one. That’s quick – but imprecise.
120
Ivan Akulov @iamakulov.com · 12/06/2025
⚡ Lighthouse (incorrectly) punishes preloading If you’re like me, you probably struggled with Lighthouse scores at least once. “I already optimized everything; why is my score still yellow?” Often, the cause is there’s more to optimize. But sometimes, it’s Lighthouse itself.
1146
Ivan Akulov @iamakulov.com · 24/03/2025
📝 Wrote about that one time I was doing performance work and ran into an eval()-based JS minifier from 2004. (Also, it’s compatible with ES2024, guess what) 3perf.com/blog/packer/
052
Ivan Akulov @iamakulov.com · 17/03/2025
HOLE LEVEL 5️⃣...N There were other product complexities we had to address. • Framer supports not only components but also HOCs – so Code Boundaries had to support that too • Many errors are pretty cryptic – so Code Boundaries had to help you find which exact component broke
130
Ivan Akulov @iamakulov.com · 17/03/2025
HOLE LEVEL 4️⃣: You think that was all? Let’s zoom out to the product level. In Framer, you can not only write your code, but also reuse code written by others. And this code can be nested inside no-code UIs:
130
Ivan Akulov @iamakulov.com · 17/03/2025
This allows us to keep the server-side behavior (left) but disable the client-side behavior (right). Yay! Problem solved.
130
Ivan Akulov @iamakulov.com · 17/03/2025
Or, in React’s own words: react.dev/reference/r...
120
Ivan Akulov @iamakulov.com · 17/03/2025
💡 Perhaps suspend in the Suspense fallback? This is something @kurtextrem.de came up with. It turns out that (bear with me): • if something inside Suspense suspends, • and Suspense renders a fallback, • but the fallback also suspends, • then React will ignore this Suspense!
140