Back to overview
How Framer transforms AST to speed up hydration

How Framer transforms AST to speed up hydration

— by · 
Published on

This post has been first published as an X Article. This version contains small updates for readability.

We’re leaning on several of @voidzerodev Rolldown’s advanced features at @framer. One of them: “Native MagicString”, a Rust reimplementation of the JS MagicString library (by Rich Harris)1. Together with direct access to OXC-AST in Rolldown plugins2, we use it to make React hydration faster on Framer sites.

Specifically, I’ve used it to eliminate Suspense waterfalls during React hydration. We’ve found reducing how often React suspends during hydration (to wait for I/O) directly improves how responsive websites feel during hydration, because it means users can interact with everything on a page sooner (see also my post on selective hydration).

As an example, if you lazy-load a text area, and a user tries typing in it, nothing will happen until the component has loaded. It’s the same reason why pretty much all web frameworks use <link rel="preload"> or similar, so the JavaScript is ready right when needed.

By using a bundler plugin, we achieve runtime perf improvements without making builds meaningfully slower. This is important, because our customers expect instant publishing, not 1-minute build times.

Table of contents

Open Table of contents

From lazy import to preloaded module

Here’s how. We transform

lazy(() => import("https://..."))

into

lazy(() => import("https://..."), "hash")

where hash is a murmurHash of the URL, base36-encoded to stay compact. This enables basically zero-cost cache lookups for lazy-loaded modules.

During static-site-generation (SSG), we then track which lazy modules each page actually renders. For those, during SSG we inject

<link rel="modulepreload" href="..." data-framer-lazy="hash" />

into the HTML. This means the browser will load these modules early during page load.

On the client, those preload tags populate a Map of module promises keyed by hash. When a lazy() component renders, it checks the cache first - if the module is already preloaded, it reads synchronously. This is the best case scenario: Nothing ever needs to suspend during hydration.

You might wonder: Why use lazy imports at all if they hurt UX?

Because some components are shared across many pages. One page needs it, the other 99 don’t. Loading it everywhere is wasteful. With this, the lazy modules become eager on one page without making others slower. Win-win.

Footnotes & Comments

  1. See rolldown / MagicString and Rich Harris’ original magic-string.

  2. See oxc / OXC-AST for the parser / AST toolkit Rolldown plugins can tap into.

📖 All posts  · ⬆️ Go up
Previous Post
Great React UX: Replay input after hydration
Next Post
Cursor's Automated PR approval system at Framer