Ivan Akulov @iamakulov.com · 22/09/2026it 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/2026Possibly 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/2026TIL 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/2026Doctor: Chrome DevTools throttling is real and can hurt you Chrome DevTools throttling: [proceeds to not be real] 110
Ivan Akulov @iamakulov.com · 27/03/2026Something 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/2026the 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/2026next.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/2026ah 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/2026Tomorrow, when you publish a Framer site, every page’s HTML will get 10-20% smaller: 110
Ivan Akulov @iamakulov.com · 14/02/2026Last 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/20252) 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/2025Yeah, 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 · 14/11/2025Benchmark: 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/2025Welp, 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/2025In 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/2025setTimeout 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/2025Okay, 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/2025Why 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/2025Currently in Chrome Canary behind a flag (enable at chrome://flags): 020
Ivan Akulov @iamakulov.com · 30/10/2025Neat 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/2025So, 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/2025Over 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/20253️⃣ 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/2025To 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/20252️⃣ 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/2025Solving 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/20251️⃣ 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/20250️⃣ 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/2025This 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/2025til 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/2025note 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/2025something 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) 283
Ivan Akulov @iamakulov.com · 01/10/2025The hot-off-the-press URLPattern (developer.mozilla.org/en-US/docs/...) solves this perfectly btw 080
Ivan Akulov @iamakulov.com · 01/10/2025The 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/2025Got bitten by `new URL()` today. Guess why this doesn’t work? 160
Ivan Akulov @iamakulov.com · 30/09/2025soon 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/2025The 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/2025HOLE 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/2025HOLE 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/2025This 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💡 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