Estimating the Cost of a Hydration Boundary

β€œMake it an island” sounds free and is not: every boundary has a fixed cost that exists before any of its own code runs. Knowing that number is what lets you decide between four small islands and one merged one. This walkthrough prices a boundary properly, extending the threshold work in when to use islands versus full hydration.

Prerequisites

Implementation Steps

Three Costs per Boundary A boundary costs fixed overhead paid by every island regardless of size, a module payload paid only by visitors whose trigger fires, and serialised props paid by every visitor in the document whether or not the island ever activates. The diagram shows a typical distribution where props are the largest of the three for a data-heavy island. Who pays for what fixed overhead β€” marker, registry entry, one request, one mount task paid once per boundary Β· roughly 1–2 KB and a few milliseconds module payload β€” the component and everything it imports paid only by visitors whose trigger fires Β· the number people estimate serialised props β€” bytes in the HTML for every visitor paid by everyone, always Β· the number people forget

Step 1 β€” Measure the module in isolation

Build the component as its own entry point and read the output size. Doing this before the boundary exists avoids the usual mistake of measuring after integration, when shared chunks make the marginal cost impossible to see.

# Rollup or esbuild, one entry, production settings, no shared chunks.
npx esbuild src/components/Typeahead.tsx --bundle --minify --format=esm   --outfile=/tmp/typeahead.js && gzip -c /tmp/typeahead.js | wc -c
# β†’ 11208   ← the marginal payload, compressed

Step 2 β€” Measure the props payload from a real page

// In the console on a page rendering the candidate: how many bytes do the
// props add to the document, for every visitor, whether or not this activates?
const el = document.querySelector('[data-island="typeahead"]');
const props = el.querySelector(':scope > script[type="application/json"]');
console.log(new Blob([props.textContent]).size, 'bytes of props');
// β†’ 4820 bytes   ← paid by 100% of visitors; the module is paid by maybe 12%

That ratio is the crux. A boundary whose props are five kilobytes and whose module is eleven costs more in aggregate through the props than through the code, once you weight by how many visitors pay each.

Step 3 β€” Compare against the merged alternative

separate:  save-button   1.6 KB module + 0.2 KB props + 1.5 KB overhead
           quantity      1.2 KB module + 0.1 KB props + 1.5 KB overhead
           compare-box   0.9 KB module + 0.1 KB props + 1.5 KB overhead
           total         3.7 KB modules + 0.4 KB props + 4.5 KB overhead = 8.6 KB

merged:    row-controls  3.9 KB module + 0.4 KB props + 1.5 KB overhead = 5.8 KB

Three tiny islands that always activate together cost nearly half again as much as one merged island, entirely in overhead. This is the fragmentation failure described in understanding partial hydration, expressed as a number rather than a warning.

Step 4 β€” Record the estimate where it will be checked

Put the three numbers in the island manifest beside the boundary. When the module doubles two quarters later, the diff shows a number changing rather than a file changing, and someone asks why.

Verification

Aggregate Cost per Thousand Visitors Weighting each cost by the share of visitors who pay it changes the ranking. Props are paid by everyone, so a small props payload multiplied by every visit outweighs a larger module fetched by a minority. The chart compares three separate boundaries against one merged boundary on that weighted basis. Weighted by who actually pays three islands props + overhead dominate one merged island same code, one third of the overhead module bytes, paid by the minority who trigger it props and overhead, paid by everyone Multiply each cost by the share of visitors who incur it before comparing designs.

Validate the estimate after implementation with two measurements. Compare the route’s transferred document size before and after adding the boundary β€” the delta should match your props estimate closely, and a large discrepancy means more was serialised than intended. Then compare transferred JavaScript with the island triggered and untriggered; the difference is the module cost, and it should match the isolated build within a few per cent.

If either number is far from the estimate, the cause is almost always a shared dependency being counted differently. An island that is the first to import a library carries its full weight; the second island to import it appears free. That accounting artefact is why estimates should be made against a clean baseline and why the manifest should record which islands share which large dependencies.

Using the Numbers to Decide

Three decisions follow directly from a priced boundary.

Split or merge. If two candidates activate together and their combined module is smaller than the overhead of keeping them apart, merge. The threshold is lower than intuition suggests: at roughly one and a half kilobytes of overhead per boundary, anything under about three kilobytes of module code is mostly overhead.

Defer or not. A boundary whose props are large and whose module is small is a candidate for moving data out of props and into a fetch on activation β€” the props are paid by everyone, the fetch only by those who interact. A boundary with the opposite shape is a candidate for a later trigger, since the payload only lands when it is needed.

Island or no island. If the priced boundary is larger than the value of the interaction it enables, the honest answer may be a link. That comparison is uncomfortable to make in the abstract and easy once there is a number: a five-kilobyte island for a control used by two per cent of visitors is a poor trade, and the audit in auditing which components need hydration exists to surface exactly those.

A Worked Merge Decision Three adjacent controls priced separately and merged. Separately they cost three fixed overheads plus their modules; merged they cost one overhead and the same module code, which is a saving of roughly a third with no behavioural difference because all three activate together. Three adjacent controls, priced both ways separate 8.6 KB total merged 5.8 KB total β€” one overhead instead of three module code fixed overhead per boundary Merging is correct here only because all three activate at the same moment.

Troubleshooting

What exactly is the fixed overhead of a boundary?

Four things: the marker markup and its props script in the document, an entry in whatever registry the loader keeps, one network request for the module, and one mount task on the main thread. On a warm cache the request is nearly free and the mount task dominates; on a cold one the request dominates. As a planning figure, treat a boundary as costing a couple of kilobytes and a few milliseconds before its own code is counted.

Do props really matter if the island never activates?

Yes, and this is the cost teams forget. Serialised props are bytes in the HTML, paid by every visitor on every request, including the majority who never scroll to the island. A boundary with a large props payload can cost more in aggregate than its module does, because the module is fetched by a minority and the props are downloaded by everyone.

When is merging two islands the right call?

When they always activate at the same moment, sit adjacent in the layout and share a runtime. Then merging removes one overhead and one round trip at no behavioural cost. Keep them separate when their triggers differ, when one is far larger than the other, or when they belong to different teams β€” the coordination cost of a shared boundary can outweigh a couple of kilobytes.

← Back to When to Use Islands vs Full Hydration