Made in Builder

Made in Builder.io

How to Build: Localization webinar on March 23rd @ 10am PST. Register Now

×

Developers

Product

Use Cases

Pricing

Developers

Resources

Company

Log in

Product

Features

Integrations

Talk to an Expert

Pricing

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

web development

Understanding Resumability from the Ground Up

March 10, 2023

Written By Miško Hevery

There has been a lot of talk about resumability, so let’s build a resumable web application from the ground up to explain the concept better.

Resumability promises a constant boot time for your application, whereas the current hydration approach has no upper limit on the time it can take. Resumability allows for instant-on applications, and many case studies demonstrate that startup speed impacts your bottom line.

Imagine booting up a virtual machine on your host machine. You watch it go through the boot sequence, login screen, and so on. Then you log in, open a document and start typing a letter. At some point, you save the virtual machine to a file and move the file to a different physical machine.

Image showing two computers but passing HTML instead of files.

On the new physical host machine, you open the “saved” virtual machine and it opens to the exact place in the document where you were typing. What you did not have to do was to watch it boot, log in, open the document editor, and enter the document. You resumed where you left off.

Replace “file” with HTML, bootup process with “hydration,” and physical machines with server/client, and you now understand what resumabality is. Resumability is a faster way to get an interactive web application in front of the user by skipping the hydration cost.

Want to know more? Check out these deep dives:

Before we dive into the details, I would like to clarify terminology that is important for the discussion:

  • App-code: code that the developer writes to describe the application.
  • Framework: a third-party library the developer uses when creating the application.
  • Global event handler: a small piece of JS that must eagerly run to set up global event handling.

The key insight is that web applications don’t do anything by themselves. They only perform work due to some external event. (You can think of setInterval IntersectionObserver as a kind of event.)

If we somehow serialize the location of all events in an application during SSR, the client would not need to do anything on startup. The client would just wait for the next event to process. This would make the startup essentially instant, as there is no app-code JavaScript to execute on startup.

How can we serialize the event handlers? Let’s build a pared-down counter application.

function Counter() {
  const [count, setCount] = useState(0);

  return <button 
            onClick={() => setCount(count+1)}
         >
           {count}
         </button>;
}

As part of the SSR we will generate HTML, which will be like this:

<button>0</button>

The problem with the above HTML is that we need to gain information about the fact that the button has a click listener. The standard approach to recover the click listener is to re-run the application (hydration) on the client, but that is precisely what we wish to avoid.

So let’s try to serialize the fact that it has a click listener:

<button on:click="./button-handler.js">0</button>

Perfect, we serialized it, but how do we make it work?

The above code will not work because on:click has no particular meaning to the browser. So instead, we need a way to teach the browser what these attributes mean. Enter the global event handler.

The global event handler is a tiny piece of code (about 1kb) that “teaches” the browser how to process the on:click attributes. This piece of code must execute eagerly to enable the behavior.

The global event handler looks something like this (simplified psuedo-code):

allPossibleEventsInApplication.forEach(async (eventName) => {
  document.addEventListener(eventName, (event) => {
    const url = event.target.getAttribute('on:'` + eventName);
    if (url) {
       (await import(url)).default();
    }
  });
});

The code sets up a global listener for all relevant events and relies on the fact that events bubble up. When the event occurs, the global listener looks for the corresponding on:click attribute, and if found, it uses the attribute as a URL to the code which needs to be loaded.

The important thing to take away is that the global event handler is constant in size. It has nothing to do with your application code, as it is entirely generic.

OK, we now have this HTML, which contains information about the event listeners.

<button on:click="./button-handler.js">0</button>

But we have a problem. Where does button-handler.js come from? Remember, our application looks like this:

export function Counter() {
  const [count, setCount] = useState(0);

  return <button 
            onClick={() => setCount(count+1)}
         >
           {count}
         </button>;
}

What we want is to get a hold of this closure: () => setCount(count+1), and as of right now, we can’t do that because it is not a top-level export. The only way we can get a hold of the event handler function is to download the application and execute it to get a hold of the event handler. But that is precisely what makes it hydration and not resumable. Resumable requires that we somehow skip that part.

So we need to have a way to transform the above code to something like this:

export function Counter() {
  const [count, setCount] = useState(0);

  return <button 
            onClick="./button-handler.js"      // <==== HERE
         >
           {count}
         </button>;
}

And then, move the extracted function to a new file called button-handler.js:

export default () => setCount(count +1);

NOTE: Ignore the fact that setCount() and count are undefined. We will deal with that later.

What we did with the above transformation created a new entry point. Our original application only had a single entry point Counter. But that is a problem because if you only have a single entry point, the only thing that can happen is that the code starts executing from that single entry point, and the only thing such code can do is run the whole application. The very thing we are trying to avoid with resumability.

Creating new entry points is a crucial concept of resumability. Our application is simple and only has a single click listener, but real-world applications would have hundreds of listeners and, therefore, hundreds of entry points. Each listener is a potential place where the application can start executing and therefore needs a different entry point.

It would be unreasonable to expect that the developer would be forced to break up their code into so many entry points. It would not be a good DX. So what we need is a way to automate this.

We can do this in two parts:

  1. Create a tool that can automatically extract such code (let’s call it the Optimizer.)
  2. Create a marker in the code so the Optimizer knows where such transformations are needed. Let’s assume that a marker is $ character at the end of a function name or JSX property.

Tip: This may be an excellent time to read WTF is Code Extraction.

With the above, let’s rewrite our application:

export function Counter() {
  const [count, setCount] = useState(0);

  return <button 
            onClick$={() => setCount(count +1)}      // <==== HERE
         >
           {count}
         </button>;
}

When the optimizer runs, it breaks the application code into two files:

The original file:

export function Counter() {
  const [count, setCount] = useState(0);

  return <button 
            onClick$={'./hash-1234.js#s456'}      // <==== HERE
         >
           {count}
         </button>;
}

And a new file called hash-1234.js:

export const s456 = () => setCount(count +1)

Notice we now made it slightly more complicated. The URL now contains #s456, which allows us to place more than one entry point into a single file. This way, we can ensure that we can place related code into a single file and don’t end up with thousands of small files, which would be counterproductive — more on that later.

To have a resumable application, we must continue execution from an event handler. To do so, we need to serialize the event handle into HTML and extract the event handler code into a top-level export and thus create an entry point.

The 🤯 thing for me is that this is as if the addEventListener() was executed on the server but the listener function executes on the client.

But let’s tie up a few loose ends first…

We extracted our click listener to a top-level export like so:

export default () => setCount(count +1);

Unfortunately, this will not work! The issue is that this function refers to setCount() and count, which are undefined in this file. The closure state is not persisted. When you load them, they no longer know what count is (or setCount). They forgot. So let’s fix that. Let’s generate this instead:

export function Counter() {
  const [count, setCount] = useState(0);

  return <button 
            onClick$={
               toURL('./hash-1234.js#s456', [count, setCount]) // <==== HERE
            }
         >
           {count}
         </button>;
}

And a new file called hash-1234.js:

export const s456 = () => {
  const [count, setCount] = useLexicalScope(); // <==== IMPORTANT BIT
  return setCount(count +1);
}

Notice that the onClick$ handler now “saves” count and setCount, and the extracted function got rewritten to restore count and setCount using the useLexicalScope() function.

So the problem now becomes that the Counter executes on the server. On the server, we have count and setCount values. In contrast, the click handler executes on the client. How do we get count and setCount from the server to the client? Well, we do the same thing we did with events. We serialize them.

So let’s look at how the above changes our generated HTML:

<button on:click="./hash-1234#s456[0,1]">0</button>
<script>/* global event handler*/</script>
<script type="text/json">
[
  0,                     // state of `count` during serialization
  'hash-2345.js#s789[...]', // information which will allow us to 
                         // rebuild `setCount` on the client.
]
<script>

OK, a few things changed!

First, notice that the on:click URL now contains an extra array [0,1]. This array includes information that useLexicalScope() will use to recover count and setCount explained next. The array is just a collection of pointers to the serialized state of the system, saying which object should be recovered at position 0, position 1, and so on.

Second, we had to insert an extra

Builder.io is a headless CMS that lets you drag and drop with your components.

Learn more

Like our content?

Sign up for our newsletter


Continue Reading
Web Development12 MIN
Signals vs. Observables, what's all the fuss about?
WRITTEN BYMiško Hevery
March 20, 2023
software engineering5 MIN
Using Feature Flags for API Version Upgrades
WRITTEN BYShyam Seshadri
March 20, 2023
performance optimization5 MIN
Inspect Source Code Performance with Deoptigate
WRITTEN BYMiško Hevery
March 14, 2023

Product

Visual CMS

Theme Studio for Shopify

Sign up

Login

Featured Integrations

React

Angular

Next.js

Gatsby

Get In Touch

Chat With Us

Twitter

Linkedin

Careers

© 2020 Builder.io, Inc.

Security

Privacy Policy

Terms of Service

Visually build and optimize digital experiences on any tech stack. No coding required, and developer approved.

Sign up

Log in

DEVELOPERS

Builder for Developers

Developer Docs

Github

JSX Lite

Qwik

INTEGRATIONS

React

Angular

Next.js

Gatsby

PRODUCT

Product features

Pricing

RESOURCES

User Guides

Blog

Forum

Templates

COMPANY

About

Careers 🚀

Visually build and optimize digital experiences on any tech stack. No coding required, and developer approved.
Get Started
Log in

DEVELOPERS

Builder for Developers

Developer Docs

Open Source Projects

Performance Insights

PRODUCT

Features

Pricing

RESOURCES

User Guides

Blog

Community Forum

Templates

Partners

Submit an Idea

INTEGRATIONS

React

Next.js

Gatsby

Angular

Vue

Nuxt

Hydrogen

Salesforce

Shopify

All Integrations

Security

Privacy Policy

SaaS Terms