Replacing Heavy Dependencies in Island Code

Island modules are rarely large because of the component; they are large because of four dependency families that arrive with it. Removing them is the highest-return payload work available, and most of it is a one-line change. This walkthrough works through the four, extending the techniques in reducing JavaScript payload in islands.

Prerequisites

Implementation Steps

Four Families, Four Replacements Date libraries are replaced by server-side formatting or the Intl API, saving twenty to thirty kilobytes. Icon packs are replaced by inlining the handful of icons actually used, saving ten to fifteen. Validation libraries move to the server, saving eight to twelve. Utility barrels are replaced by direct imports, saving five to ten. Typical savings per island module date library → server formatting, or Intl for the local-zone case · −20 to 30 KB icon pack → inline the four icons this island uses · −10 to 15 KB validation schema → validate on the server · −8 to 12 KB utility barrel → direct imports · −5 to 10 KB

Step 1 — Dates: format where the data is

// Before: 28 KB of date library inside the island, to render one label.
import { formatDistanceToNow } from 'date-fns';
export function Posted({ iso }) { return <time>{formatDistanceToNow(new Date(iso))}</time>; }

// After: the server formats; the island renders a string it was given.
export function Posted({ iso, label }) { return <time dateTime={iso}>{label}</time>; }

// Where a visitor-local rendering is genuinely required, the platform does it:
const local = new Intl.DateTimeFormat(navigator.language, { dateStyle: 'medium' })
  .format(new Date(iso));                    // 0 KB, built in

This swap has a second benefit beyond size: it removes a common source of hydration mismatch, as described in fixing text content did not match warnings.

Step 2 — Icons: inline what you use

An icon package imported for four icons ships hundreds. Inline the four as SVG in the component or the server template; they compress well and cost nothing to parse.

Step 3 — Validation: keep the schema on the server

Client-side validation improves the experience and cannot be trusted, so the server validates regardless. That makes the client copy a duplicate rather than a necessity. Use the platform’s constraint validation for the immediate feedback — required, type, pattern, minlength — and let the server return field-level messages for everything structural.

Step 4 — Barrels: import the module, not the index

// Before: pulls the whole utils index, which re-exports a chart adapter.
import { formatCount } from '@/lib/utils';
// After: only this module and its own imports.
import { formatCount } from '@/lib/utils/format-count';

Add a lint rule forbidding barrel imports inside island directories. It is the cheapest permanent fix in this list and it prevents the regression rather than detecting it.

Verification

One Island, Before and After A fifty-eight kilobyte island module is reduced to nine. The date library, icon pack, validation schema and barrel-imported utilities are removed; only the component itself and two small helpers remain. The component code is unchanged in size, which is the point: the savings came entirely from what surrounded it. Composition of one island module before · 58 KB dates · icons · validation · barrel · the component itself after · 9 KB the component, unchanged, plus two small helpers The component was never the problem. Measure what surrounds it before optimising what you wrote.

Verify each swap independently rather than all at once, so a regression is attributable. After each change, rebuild the island as a standalone entry point and compare the compressed size against the previous number. Keep the two numbers in the commit message; a reviewer can then see that a claimed twenty-kilobyte saving actually happened.

Then verify behaviour, because these swaps change where work happens. Server-side formatting must produce the same string the library did, including for edge cases such as zero, negative values and the boundary between “yesterday” and a date. Platform validation must reject the same inputs the schema did, or the server will start receiving submissions the client used to block. A short table of inputs and expected outputs, run against both implementations during the migration, catches the differences that matter.

Keeping the Gains

Payload work decays. The dependency you removed returns in a different form, usually through a component that arrived from another part of the codebase where nothing constrained it.

Three mechanisms hold the line, in increasing order of effectiveness. A documented convention helps for a quarter. A lint rule against barrel imports and specific package names inside island directories helps for longer, because it fails at the point of writing rather than at review. A per-module size budget enforced in the build is the only one that survives indefinitely, because it catches every route to the same outcome — a new library, a new import style, an accidental re-export — with one check.

The budget file also becomes the record of intent. When a module legitimately needs to grow, raising its budget is a visible commit that someone approves, which is exactly the conversation that should happen. The same mechanism applied to serialised state is described in reducing Qwik serialization payload size; the two together cover the whole surface of what an island ships.

A last word on what not to replace. Not every large dependency is a mistake: a rich text editor, a charting engine, a map renderer or a video player is large because the problem is large, and reimplementing one to save bytes is a project rather than an optimisation. For those, the correct move is scheduling rather than substitution — defer the island until the visitor asks for it, load it on interaction rather than on viewport, and make sure the server-rendered fallback conveys the content even when the interactive version never loads. A twelve-kilobyte replacement for a two-hundred-kilobyte editor is not a smaller editor; it is a worse one that you now maintain.

What Not to Replace Editors, charting engines, map renderers and video players are large because the problem is large. For those the answer is scheduling — defer until requested — rather than a smaller reimplementation you now maintain. Some dependencies are large for good reasons replace: dates, icons, validation, utility barrels the platform or the server already does this work do not replace: editors, charts, maps, players defer them instead, and ship a server-rendered fallback a smaller reimplementation is a project, not an optimisation

Troubleshooting

Is a smaller library always better than a platform API?

The platform API is usually better because it costs nothing to ship, but it is not always sufficient — a date library that handles time zone arithmetic across historical rules does something Intl formatting does not. The right question is what the island actually uses. Most island code formats a value for display, which the platform does completely, and a minority does real calendar arithmetic, which justifies a dependency.

How do I stop a removed dependency from coming back?

Add a size budget per island module to the build and fail the job when it is exceeded. A comment in the file is ignored within a quarter; a failing build is not. The budget also catches the subtler version of the regression, where a different library with the same weight arrives to do the same job under a new name.

What if the dependency is needed on the server too?

Then keep it on the server, where its size costs nothing beyond deploy time, and ship the result rather than the tool. A currency formatted on the server is a string; a schema validated on the server is a boolean plus messages. The island receives the output and stays small, which is the same rule that prevents whole classes of hydration mismatch.

← Back to Reducing JavaScript Payload in Islands