Auditing Which Components Need Hydration

Deciding what becomes an island is usually done by intuition, one component at a time, which produces boundaries that are too large and triggers that are too eager. An audit replaces that with a list. This walkthrough produces an island manifest for one route before any code is written, using the boundary rules from understanding partial hydration.

Prerequisites

Implementation Steps

The Audit Output for One Route Eleven interactive elements on a listing route are sorted into three groups. Five need no JavaScript at all and become links or forms. Four become small islands sharing one activation trigger. Two are large enough to deserve their own boundaries with deferred triggers. The total shipped JavaScript falls from a single ninety-kilobyte bundle to twenty-two kilobytes across six modules. Eleven interactive elements, three destinations NO JAVASCRIPT sort control β†’ link pagination β†’ links filter form β†’ GET form FAQ toggle β†’ details 0 KB SMALL ISLANDS save button quantity stepper compare checkbox basket badge 6 KB, one shared trigger LARGE ISLANDS typeahead search image gallery own boundary each 16 KB, deferred before: one 90 KB bundle Β· after: 22 KB across six modules, most of it deferred

Step 1 β€” Enumerate, do not remember

// Console script: every element that could plausibly be interactive.
const INTERACTIVE = 'a,button,input,select,textarea,summary,[role],[onclick],[data-island]';
const rows = [...document.querySelectorAll(INTERACTIVE)].map((el) => ({
  tag: el.tagName.toLowerCase(),
  role: el.getAttribute('role') || '',
  label: (el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 40),
  inViewport: el.getBoundingClientRect().top < window.innerHeight,
}));
console.table(rows);

The inViewport column matters as much as the rest: it separates the elements that need an eager trigger from the ones that can wait for the viewport, which is the single largest lever on blocking time.

Step 2 β€” Ask what each element would do with no JavaScript

Work down the list and write the no-JavaScript answer next to each row. Sorting becomes a link with a query parameter. A tab set becomes anchors. A disclosure becomes details. Anything with a genuine no-JavaScript answer leaves the manifest entirely, which is a larger saving than any bundling technique.

Step 3 β€” Group by activation moment, not by component tree

Two controls that must both work the instant the page is usable belong in one island even if they sit in different components; two controls in the same component that activate at different moments belong in different islands. Grouping by activation is what makes the trigger assignment obvious in the next step, and it is the reason a component library and an island manifest are different lists.

Step 4 β€” Price each group and write it down

# islands.manifest β€” committed next to the route
save-button      trigger: visible   est: 1.8 KB   owns: POST /favourites
quantity-stepper trigger: visible   est: 1.2 KB   owns: local state only
typeahead        trigger: focus     est: 11  KB   owns: /api/search, debounce
gallery          trigger: visible   est: 5   KB   owns: touch, preload

A manifest with triggers and budgets is a design artefact the whole team can review. Without one, boundaries are decided in pull requests one component at a time, which is how a route ends up with eleven eager islands.

Verification

Confirming the Manifest Matches Reality Three checks after implementation. The modules requested on load must match the islands whose triggers should have fired. The transferred total must be within the manifest budget. And the elements that were supposed to need no JavaScript must still work with scripting disabled, which is the check that most often fails. CHECK FAILS WHEN modules requested on load a deferred island was imported by an eager one transferred total against budget a dependency was larger than the estimate no-JavaScript elements still work a link was replaced by a click handler again

The third check deserves a permanent test rather than a one-off. It is the one that regresses, because β€œmake the sort control update without a page load” is a reasonable-sounding ticket that quietly converts a zero-kilobyte link into an island. A test that loads the route with scripting disabled and asserts the sort links navigate takes a minute to write and holds the audit’s most valuable outcome in place.

Re-run the whole audit when a route changes materially β€” a redesign, a new panel, a feature that adds interactivity above the fold. The enumeration script makes that a ten-minute job, which is the difference between an audit that happens twice and one that happens whenever it matters.

Reading the Result

Two patterns show up in almost every first audit, and both are worth naming because they are easy to fix once seen.

The first is that a third of the β€œinteractive” elements did not need to be. Sorting, paging, filtering, tab switching and disclosure are all expressible in HTML, and each one implemented as an island is a boundary paying for a capability the platform already provides. Teams consistently underestimate this share, because the components arrived from a client-rendered codebase where everything was JavaScript by default.

The second is that the interactive elements cluster by activation moment far more tightly than by component structure. A page usually has one group that must be ready immediately, one that can wait for the viewport, and a couple of outliers that only matter on interaction. That structure β€” three or four boundaries rather than eleven β€” is what the manifest should encode, and it is the reason understanding partial hydration treats fragmentation as a failure mode rather than a goal. More boundaries are not better; correctly placed boundaries are.

Re-Audit Triggers Four changes that move a route across a threshold without anyone noticing: a table gaining row-level actions, a filter becoming live rather than form-submitted, an experiment adding a personalisation widget, and a redesign moving interactive elements above the fold. Re-run the audit when any of these happens a table gains row-level actions a filter becomes live instead of a form submission an experiment adds a personalisation widget a redesign moves interactive elements above the fold

Troubleshooting

How do I find interactive elements I forgot about?

Run a script in the console that walks the DOM and reports every element with an inline handler, a known interactive role, or a listener registered through the framework. It will find things the design never mentioned: a dismissible banner, a tooltip, an analytics click wrapper. The list is usually a third longer than the one produced from memory, and the extras are precisely the ones that ship JavaScript nobody budgeted for.

Does every interactive element need to be an island?

No, and asking the question the other way round is more productive: what would this element do if the page shipped no JavaScript at all? A sort control can be a link that changes a query parameter. A tab set can be anchors and target styling. A disclosure can be details and summary. Each of those removes a boundary entirely, which is cheaper than making the boundary small.

How precise does the byte estimate need to be?

Order of magnitude is enough to make the decision. You are choosing between one island of forty kilobytes and four islands averaging six, and that comparison survives a wide error bar. Precision matters later, when the budgets are enforced in the build β€” at audit time the goal is to notice that a candidate boundary is large enough to deserve splitting.

← Back to Understanding Partial Hydration