Skip to content
HomePagesHomePages template kit

size-island-bundle

"A section's island (browser) bundle exceeds its byte budget."

Venue: check — A section’s island (browser) bundle exceeds its byte budget.

An island — a component whose file opens with the "use client" directive (see Server vs client) — ships to the browser as its own bundle, separate from the section’s server-rendered HTML. Each island is bundled with esbuild and minified, with React treated as external (the page’s runtime chunk supplies it once, shared by every island on the page, and is not counted against any one of them).

The gzipped result is held to two numbers, not one:

Gzipped island bundle What check does
over 50 KB reports it as a warning — never a failure, never a blocked submission
over 600 KB fails

Both are read per island: a section with three islands each at 40 KB is silent; one island at 60 KB is reported while its neighbours stay silent; one island at 610 KB fails on its own, whatever the section’s other files weigh.

A third number belongs to the template rather than to any island: every island in a template, summed, is reported past 1 MB gzip. A page of individually reasonable islands can still be a heavy page, and no per-island number can see it. Nothing is refused at the template level either.

check prints what every section weighs on every run, not only when something is over. The numbers above are where it changes its verdict, not where you find out the weight.

They are visible in template-kit check’s configuration and are not author-overridable. A template workspace cannot raise them, and there is no per-island exemption.

An island is the part of the page that has to work on a phone before the visitor does anything — a carousel, a filter, a form. Every one of its bytes is downloaded, parsed, and executed on the client, on top of whatever the page’s shared runtime already costs.

Those bytes are not on the page’s critical path. A published page fetches island bundles in the background at low priority, behind its images and its CSS, and hydrates them by viewport priority — the islands on screen first, an island the visitor approaches at the moment they approach it, the rest in idle slices (Islands). So the weight of an island below the fold is paid out of slack, not out of first paint, which is why a real runtime library is a legitimate thing to import here and a render-blocking stylesheet of the same size is not.

The two numbers therefore ask two different questions.

Over 50 KB asks whether the weight is deliberate. A bundle that crosses it usually crossed it by accident, and the accident has one shape — not an island that grew, but a single import that was more expensive than it looked:

  • a whole UI or icon library imported for one component, which drags every export into the bundle so the island can use a handful of them;
  • a heavy dependency that belongs on the server, ported into the island because it was easiest to reach for where the code was being written, not because the browser needs it.

Import a map engine on purpose and the same line is simply the report telling you what it cost. Nothing is refused, and nothing has to be justified to anyone.

Over 600 KB is the pathology line. It sits above the heaviest library the platform means to keep legal — Mapbox GL JS bundles to about 510 KB gzip — so reaching it means something is in the bundle that was never meant to reach a browser: a server-only toolchain, a dataset, a second copy of a framework.

If the weight is deliberate, there is nothing to trim. Two things are worth checking before you move on:

  • Keep a heavy island out of the top of the page. Above the fold is the one place the schedule cannot help: those islands hydrate first, so their bytes are the ones a visitor waits on. The same island further down is hydrated as they approach it, with its bytes already fetched.
  • Make sure its server-rendered frame stands on its own. The heavier the island, the longer that frame is what the visitor sees — see the server-rendered first frame.

If it is not deliberate, find the import that is not paying for itself, or narrow what ships to the browser at all.

  • Import named symbols, never a namespace. import { ArrowRight } from "…" is tree-shakeable; import * as Icons from "…" is not — a namespace object has to contain everything, so nothing can be dropped.
  • Prefer the package’s narrow entry if it offers one, instead of its default “everything” bundle.
  • Move what doesn’t need to be interactive out of the island. If only a small part of a component actually needs client-side behavior — an expand/collapse toggle inside an otherwise static block — split the interactive part into its own small island and leave the rest server-rendered. Server-rendered markup costs nothing against this budget.
  • Hand-write the residue. If you use one function out of a large utility package, copy the function instead of importing the package for it.
sections/gallery/Carousel.tsx
"use client";
import * as Icons from "lucide-react"; // ← the whole set: every icon, in this island's bundle
import { useState } from "react";
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
const [index, setIndex] = useState(0);
return (
<div>
<img src={slides[index]?.url} alt="" />
<button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
<Icons.ChevronLeft aria-hidden />
</button>
<button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
<Icons.ChevronRight aria-hidden />
</button>
</div>
);
}
sections/gallery/Carousel.tsx
"use client";
import { ChevronLeft, ChevronRight } from "lucide-react"; // ← two icons, and the bundler can drop the rest
import { useState } from "react";
export default function Carousel({ slides }: { slides: { id: string; url: string }[] }) {
const [index, setIndex] = useState(0);
return (
<div>
<img src={slides[index]?.url} alt="" />
<button onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}>
<ChevronLeft aria-hidden />
</button>
<button onClick={() => setIndex((i) => (i + 1) % slides.length)}>
<ChevronRight aria-hidden />
</button>
</div>
);
}
  • size-renderer-bundle — the same two-tier shape on the section’s server-rendered bundle, for a different reason.
  • size-section-css — a single hard budget on the section’s stylesheet, because nothing schedules CSS.
  • Islands — viewport-priority loading, and what the server-rendered frame has to do while an island waits its turn.
  • Server vs client — what makes a file an island in the first place.