Beyond the Canvas

Sites Now Become Interactive 50% Faster

Framer Blog

Yellow Flower
Yellow Flower

This week, we’ve started rolling out an update that makes most Framer sites become interactive 50% faster. Here’s how we achieved this and what interactivity means.



How do Framer sites become interactive?

A website is made of HTML and made interactive with JavaScript. To ensure the initial page load is fast, we send HTML first, so users (and search engines) see content as fast as possible. This is usually called server-side rendering.

In parallel, we load the JavaScript that is needed for a process called “hydration” (yes, like watering plants 🫗🪴). The process itself is handled by our framework of choice, React, which powers every Framer site. Technically, hydration takes the received HTML and attaches event listeners, e.g., it makes elements listen for ‘clicks’ or ‘scroll’. This is when your website is able to process user interactions and when we call it ‘interactive’.

Some of the JavaScript-based elements need data that has to be fetched from the server to provide interactivity. For example, our homepage needs the user login state in the areas marked in red:




React: How Suspense & Hydration play together

For data fetching during hydration, we use a React feature called ‘Suspense’, which is a way of telling React ‘This component is waiting for something’ — in our case, waiting for data received from network requests.

Previously, we’ve used a single instance of Suspense for that purpose. Here’s very simplified how that looked like:


<App>
  <Suspense>
    <Page>
      <Header />
      <DataFetching content="blog" /> {/* ⬅️ triggers Suspense */}
      <DataFetching content="footer" /> {/* ⬅️ triggers Suspense */}
    </Page>
  </Suspense>
</App>


This means, whenever we fetch data for ‘blog’ and ‘footer’, the single Suspense tag (“Suspense boundary”) would trigger (also called ‘suspending’). While React initializes the data fetching of those two components in parallel, the devil is in the details:

During hydration, whenever one component suspends and then after successfully fetching data, unsuspends, hydration restarts from the Suspense boundary that caught the suspending component.

This means in the example above, for every single data fetch, React would start hydration from the single Suspense tag below <App>. More specifically, when fetching data for ‘blog’, React triggers the render process for <Header> and the children of both <DataFetching> components all over again. This also happens when fetching data for ‘footer’.

Basically, we’d start all over lots of times. Each fetching component would cause a parent to render again. In the example, if we assume each data fetcher has children, we'd get a total of 12 renders, as <Page> and <Header> would render 3x, the blog data fetcher & children 2x, and the footer data fetcher & children 1x (= 6 + 4 + 2).



Faster Hydration with granular Suspense

Rendering just a handful of components like in the example is fast, but real websites fetch lots of data and contain hundreds or thousands of components. Also, how fast rendering takes depends on the device (especially on the CPU). Some users might have a slower device than others, and we also want them to enjoy the same speedy experience.

As a first instinct, we reached for memoization (React.memo) — surely that would stop React from rendering those components again, right? No, because React schedules re-renders for the children of the Suspense boundary.

To fix it, we’ve added granular Suspense boundaries around components that fetch data:


<App>
  <Suspense>
    <Page>
      <Header />
      <Suspense><DataFetching content="blog" /></Suspense> {/* 🎉 has its own boundary */}
      <Suspense><DataFetching content="footer" /></Suspense> {/* 🎉 has its own boundary */}
    </Page>
  </Suspense>
</App>


Now each Suspense boundary catches suspending components in the tree-below them. This means, React resumes hydration right from where it paused. As an example, the Framer homepage went from 1 to 151 granular boundaries.

This makes it a fast, linear process O(n) (with n being the number of nodes in the component tree) instead, as every component renders just once during hydration, no matter how many data fetchers are in the tree.

Original post by Framer