size-renderer-bundle
"A section's renderer bundle exceeds its byte budget."
Venue: check — A section’s renderer bundle exceeds its byte budget.
Each section’s renderer is bundled with esbuild and minified, with React treated as external (it is a peer, shared by the whole page, and is not counted against any one section). The gzipped result is held to two numbers, not one:
| Gzipped renderer bundle | What check does |
|---|---|
| over 50 KB | reports it as a warning — never a failure, never a blocked submission |
| over 300 KB | fails |
check prints what every section weighs on every run, not only when something is over.
Both numbers are visible in template-kit check’s configuration and are not
author-overridable. A template workspace cannot raise them, and there is no per-section
exemption. If you go looking for the flag, this is the answer: there isn’t one.
Reason
Section titled “Reason”A renderer runs on the server. Its output — HTML, and the section’s stylesheet — is what a published page carries; the bundle itself is never downloaded by a browser, so its weight is not the page-weight cost an island’s or a stylesheet’s is. That is why the ceiling is high enough for a library that does real work at render time: a geo or date-and-currency toolkit that turns data into finished markup earns its bytes here in a way it never would in an island.
It is not free, though — it is paid somewhere else. Something has to load and parse this bundle to produce the HTML, so weight here buys render time rather than download time. That is a slower cost curve than a visitor’s connection, which is what the two numbers below reflect, not an absence of cost.
Over 50 KB is therefore an observation, not an accusation. It is where a renderer stops being ordinary, and almost every renderer that crosses the line crossed it by accident — not a section that grew, but a single import that was more expensive than it looked:
- a barrel import of an icon set, which drags every icon into the bundle so you can use three;
- a package that brings a font, a locale table, or a polyfill along with it, none of which you asked for.
If the weight is deliberate, the report is just telling you what your dependency costs, and there is nothing to answer for.
Over 300 KB is the pathology line. It is set from the heaviest thing that legitimately runs at render time — a full geo toolkit imported as its whole barrel, which measures 140 KB gzipped — with better than double that in headroom. Reaching it means something is in the bundle that has no business being there: a dataset, a build toolchain, a second copy of a framework.
Find the import that is not paying for itself.
- 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.
- Hand-write the residue. If you use one function out of a large utility package, copy the function.
Do not try to hoist code out of the section to dodge the budget. Each template owns its own presentation code; there is no shared library above a section to hoist it into, and the bytes would still land in the same bundle.
Watch for one thing while you trim: a dependency that is heavy because it belongs in the
browser — a carousel engine, a map SDK — is in the wrong file whatever it weighs, and
belongs behind "use client" where the page’s schedule can pay for it (see
size-island-bundle). Moving it there is the right change for
the visitor. Whether it also moves this number depends on the island: a renderer
server-renders its islands’ first frame, so their imports are part of what it loads —
unless the island declares it has no first frame to render, in which case the renderer
drops it entirely (see island-server-frame).
Before
Section titled “Before”import { bindItems, bindSlots, Section, Slot, SlotGroup } from "@homepages/template-kit";import * as Icons from "lucide-react"; // ← the whole set: every icon, in the bundle
import { schema } from "./schema.js";import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) { const slot = bindSlots(schema, slots);
return ( <Section> <Slot.List slot={slot.amenities} as="ul"> {bindItems(slot.amenities).map((amenity) => ( <SlotGroup slot={amenity} as="li" key={amenity.index}> <Icons.Check aria-hidden /> <Slot.Text slot={amenity.fields.label} as="h3" /> </SlotGroup> ))} </Slot.List> </Section> );}import { bindItems, bindSlots, defineSchema, list, Section, Slot, SlotGroup, text } from "@homepages/template-kit";import type { SectionProps } from "@homepages/template-kit";import { Check } from "lucide-react"; // ← one icon, and the bundler can drop the rest
const schema = defineSchema({ label: "Amenities", anchor: "amenities", slots: { amenities: list.of({ label: text.short({ label: "Label" }) }, { label: "Amenities" }), },});
type Props = SectionProps<typeof schema>;
export function Renderer({ slots }: Props) { const slot = bindSlots(schema, slots);
return ( <Section> <Slot.List slot={slot.amenities} as="ul"> {bindItems(slot.amenities).map((amenity) => ( <SlotGroup slot={amenity} as="li" key={amenity.index}> <Check aria-hidden /> <Slot.Text slot={amenity.fields.label} as="h3" /> </SlotGroup> ))} </Slot.List> </Section> );}See also
Section titled “See also”size-section-css— the other half of what the section ships, held to a single hard budget because CSS blocks the first paint.size-island-bundle— the same two-tier shape on the code that does reach the browser.