Exploring Web Rendering: Partial Hydration (a.k.a. “Islands”)

Take a vacation from too-much-JavaScript slowing down your pages
Photo of a tropical island

Photo by tiarescott on Flickr

5-part Series: Exploring Web Rendering

  1. Isomorphic JavaScript & Hydration
  2. Partial Hydration (a.k.a. “Islands”) ⟵ you’re reading this
  3. Progressive Hydration
  4. Streaming HTML
  5. Server Components Architecture

Welcome back to the second part of my series focussed on exploring interesting and useful web rendering technologies. As the series progresses, you’ll notice a core theme of performance optimization by moving from client-first to server-first rendering. In fact, the reason for the ordering of these articles is to progress towards as little browser JavaScript as possible while maintaining interactivity. Starting from a traditional client-rendered single page application, if you follow these techniques in order, you can end up with far less JavaScript sent to the browser than when you started. As a reminder, when optimizing for performance, less application code means less to download and execute in order to accomplish the user’s intent. Considering the accelerating divergence between high-end and mainstream phones over the past 10 years as shown in the graph below, hardware is providing meaningful performance improvements only on expensive devices. Thus, as time goes on, making applications with efficient resource usage is becoming essential to guarantee a good user experience for most users because common devices are not getting noticeably faster. Remember: JavaScript is primarily a single-threaded language by design, and threads execute on cores.

Chart showing the single-core performance discrepancy between expensive and mainstream devices
From Alex’s Russel’s talk The Global Baseline, 2022 @ performance.now() 2022

As discussed in the previous article in this series, hydration initializes an  application by adding interactivity to server-rendered HTML. Doing so can cause serious performance problems due to the entire application needing to render before becoming interactive; long tasks, UI stutter, and even pauses can be consequences of this heavy-handed initialization strategy. There are two effective ways to tackle this problem: (1) do less work when hydrating by skipping the portions of the DOM that will never be interactive (a.k.a. “partial hydration”) or (2) do hydration at some point in the future (a.k.a. “progressive hydration”); only the former will be the focus of this article, then the latter will be covered in part 3 of this series.

Before examining its details, partial hydration will be best understood by first exploring its origins to appreciate how we arrived at where we are today. The concept first arose with the Marko framework by eBay back in 2014 with a feature they now refer to as “Code Elimination” on their homepage. eBay’s motivation for creating a framework like Marko is clear once you realize the direct correlation between performance and revenue and the lack of sufficient JavaScript ecosystem options at the time to meet their performance needs. In fact, their timing is interesting because Marko is one of the first examples of a full-stack, server-first JavaScript framework, a clear indication of how important this design philosophy is to performance optimization because of the scale at which the framework has been operating since then. If you’re interested in more details about eBay’s motivations for creating Marko, Ryan Carniato – creator of SolidJS and previously a Marko core team member – wrote an article What has the Marko Team Been Doing all These Years? that delves deeper.

Where did the modern “islands” naming come from? The idea of “component islands” was named by Katie Sylor-Miller – frontend architect at Etsy – back in 2019 and expanded upon by Preact creator Jason Miller in his blog post entitled Islands Architecture. Within it, he explains how “islands” can be thought of like independent single page applications wherein top-down rendering is not required for the full page output to be built. Rather, each island is an isolated unit, so any potential performance problems in one “application” do not affect any others. A side effect of combining many single page apps (SPAs) onto a single page is that client side routing is lost because they all cannot control the browser’s URL, thus the application must be a multi-page app (MPA) with full-page reloads; stay tuned for part 5 of this series for how server components help solve that problem. He continues to explain that because the full HTML is returned to the browser, SEO and accessibility benefit because web crawlers and assistive technologies inherently understand the semantics of static HTML better than a dynamically changing DOM. Navigating with <a> tags allows assistive technologies to understand how to move between pages using the semantics of HTML instead of needing to reinvent the wheel with custom components and JavaScript in order for a11y technologies to understand navigation; no JavaScript is used as a result which is a performance win.

Now that the history is understood, onto the main topic: what is partial hydration? Let’s compare it to full hydration to make the behavior more clear. Remember, hydration usually occurs after loading a full page of HTML. Let’s use the diagram below as a visual aid wherein the largest white box represents the whole page and each white, red, green, and blue box represents a component; the non-white boxes are interactive components a.k.a. islands. Imagine a server preparing what JavaScript to send, then draw a box around each component that needs its JS sent to the browser. The full hydration case is quite easy: every component will have a box around it, so all code gets sent to the browser. According to the diagram, all 8 components’ code should be sent including:

  1. Nav Bar
  2. Shopping Cart
  3. Product Image
  4. Buy Now Button
  5. Product Description
  6. Product Reviews
  7. Related Products
  8. Footer
Diagram of how islands rendering works
Each box is a component. Island areas filled in color, static areas outlined in white.

Partial hydration should include only interactive components and their children. Specifically, draw a colored line around only the interactive components on the page; the diagram above shows exactly this, and if you squint a little, they look like islands of interactivity hence their name. Now only 3 components’ code should be sent including:

  1. Shopping Cart
  2. Buy Now Button
  3. Product Reviews

With partial hydration, only ⅜ or 37.5% of components have their JavaScript sent to the browser! The smaller your islands, the bigger the benefits because the ultimate goal is minimizing the amount of JavaScript sent to the browser as evidenced by myriad articles on the subject. Because server-side rendering (SSR) is used, the non-interactive component code is never sent to the browser because it would never serve a purpose: if that HTML remains static, why send code that will never change it? Instead, only code for the interactive islands is sent which has been shown to reduce the amount of code sent to the browser. As a result, less code is downloaded, parsed, and executed, so Time to Interactive is minimized by running less code that delays the user’s ability to interact with the page; in other words, users can click that Buy Now Button more quickly which increases the chances of a sale.

Partial hydration provides numerous benefits over traditional hydration. Less JavaScript required for an identical amount of interaction has no downsides aside from requiring MPA navigation; streaming rendering (part 4 of this series) and the full-page load portion of the View Transitions API (animations are fun!) can help hide the additional delay from an MPA navigation, but the loss of application state on page change should remain a concern. Using islands, faster page loads will result which will make users happier and push your pages higher in search rankings. Furthermore, less code is shared between server and browser, so the mental overhead required for dual-mode rendering in isomorphic rendered applications disappears for static portions of the app because those regions only run on the server; read part 1 of this series for more on how mental overhead can affect isomorphic application development. A further benefit is parallel loading of island code because each island is completely independent, so each island can hydrate as soon as it’s ready. Page load performance can be further improved by delaying hydration of some or all islands into the future; the specifics of this are detailed in the following article in this series (part 3) about progressive hydration.

Unfortunately partial hydration has some drawbacks as well. The first and most obvious is requiring full-page reloads during navigation; server components are effectively persistent islands which can mitigate this and will be covered in part 5 of this article series. Traditionally, partial hydration was very difficult to set up. You effectively needed to design a build process that dynamically created multiple render roots for multiple component trees and the ability to pass props to each one independently in addition to being able to share state between them. A good example of this is Markus Oberlehner’s terrific overview of this process in his blog post entitled Building Partially Hydrated, Progressively Enhanced Static Websites with Isomorphic Preact and Eleventy. The process he describes is very manual and was written before the advent of modern islands solutions included in frameworks like Astro. Additionally, state sharing between islands is made more complex than a traditional context-based design because there is no shared component root. Luckily, however, signals and signal-like libraries have been developed that can tackle this problem very efficiently and effectively; some libraries such as the Astro-recommended Nano Stores are less than 1 kB! Thus, whether this is truly a problem is a matter of perspective.

After all this promise of performance improvement, how can partial hydration be used today? The easiest and most flexible option is Astro because you can not only choose from a variety of frameworks for your islands like React, Preact, Vue, and SolidJS, but you can even mix-and-match different frameworks in islands on the same page! It even supports deployment adapters for serverless and edge rendering platforms too with only a few line changes necessary in your config file. If you’re a fan of Deno, the Fresh framework has great support for islands as well. The most-performant but least well-known option is Marko, especially the upcoming Marko 6 release whose compiler optimizations and support for resumability and fine-grained reactivity will be truly something special.

I hope you enjoyed this overview of the islands architecture. If you have any questions or feedback about this topic or any others, please share your thoughts with me on Twitter. I would certainly enjoy hearing from you! And be sure to check out Babbel’s engineering team on Twitter to learn more about what’s going on across the department. Stay hydrated!