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
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
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.
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.
Related
- Estimating the Cost of a Hydration Boundary β putting numbers on the candidates this audit produces.
- Understanding Partial Hydration β the boundary and marker mechanics behind the manifest.
- Measuring Interactivity Ratio to Scope Islands β the aggregate version of the same measurement.
β Back to Understanding Partial Hydration