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 🏳️🌈
In the app I spotted this issue in (a typical complex React app), these setTimeouts were ~1500 calls down the `recursivelyTraversePassiveMountEffects` stack
November 10, 2025 at 5:12 PM
In the app I spotted this issue in (a typical complex React app), these setTimeouts were ~1500 calls down the `recursivelyTraversePassiveMountEffects` stack
setTimeout calls also become 2× slower if you have previously set ~750 timers (doesn’t matter whether they already fired):
November 10, 2025 at 4:58 PM
setTimeout calls also become 2× slower if you have previously set ~750 timers (doesn’t matter whether they already fired):
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...
gist.github.com/iamakulov/85...
November 10, 2025 at 4:58 PM
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...
gist.github.com/iamakulov/85...
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.
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.
November 9, 2025 at 10:19 PM
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.
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.
Currently in Chrome Canary behind a flag (enable at chrome://flags):
October 30, 2025 at 5:29 PM
Currently in Chrome Canary behind a flag (enable at chrome://flags):
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
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
October 30, 2025 at 5:29 PM
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
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
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:
This wasn’t always easy! Some things, like stripping unnecessary <b>s that come from Google Docs, were simple:
October 24, 2025 at 10:18 AM
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:
This wasn’t always easy! Some things, like stripping unnecessary <b>s that come from Google Docs, were simple:
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
• ⌘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
October 24, 2025 at 10:18 AM
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
• ⌘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
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
• 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
October 24, 2025 at 10:18 AM
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
• 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
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 ↓
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 ↓
October 24, 2025 at 10:18 AM
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 ↓
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 ↓
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:
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:
October 24, 2025 at 10:18 AM
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:
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:
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:
So shipping this required some careful API redesign + a lot of iteration on making it type-safe. E.g., one intermediate design:
October 24, 2025 at 10:18 AM
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:
So shipping this required some careful API redesign + a lot of iteration on making it type-safe. E.g., one intermediate design:
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:
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:
October 24, 2025 at 10:18 AM
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:
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:
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>:
October 24, 2025 at 10:18 AM
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>:
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!
Story time!
October 24, 2025 at 10:18 AM
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!
Story time!
til oxlint does some insane tricks (custom js ast interface that understands rust memory layout!) to make js plugins fast github.com/oxc-project...
October 21, 2025 at 9:58 AM
til oxlint does some insane tricks (custom js ast interface that understands rust memory layout!) to make js plugins fast github.com/oxc-project...
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...
[1] issues.chromium.org/issues/4432...
[2] issues.chromium.org/issues/4367...
October 20, 2025 at 10:00 AM
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...
[1] issues.chromium.org/issues/4432...
[2] issues.chromium.org/issues/4367...
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)
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)
October 20, 2025 at 10:00 AM
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)
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)
i mean, you can’t say it doesn’t help
October 8, 2025 at 12:23 AM
i mean, you can’t say it doesn’t help
feels so good to be writing something again huh
October 8, 2025 at 12:20 AM
feels so good to be writing something again huh
The hot-off-the-press URLPattern (developer.mozilla.org/en-US/docs/...) solves this perfectly btw
October 1, 2025 at 9:59 AM
The hot-off-the-press URLPattern (developer.mozilla.org/en-US/docs/...) solves this perfectly btw
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)
(Solution: hire Brazilian engineers)
October 1, 2025 at 9:59 AM
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)
(Solution: hire Brazilian engineers)
Got bitten by `new URL()` today. Guess why this doesn’t work?
October 1, 2025 at 9:59 AM
Got bitten by `new URL()` today. Guess why this doesn’t work?