Skip to content

feat(site-kit): the flag artwork leaves the browser bundle - #54

Merged
jlc488 merged 1 commit into
mainfrom
claude/silly-engelbart-dc4eba
Sep 7, 2026
Merged

feat(site-kit): the flag artwork leaves the browser bundle#54
jlc488 merged 1 commit into
mainfrom
claude/silly-engelbart-dc4eba

Conversation

@jlc488

@jlc488 jlc488 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What

@devslab/site-kit/solid no longer ships the fourteen vendored flag SVGs (~115 KB; Spain alone 85 KB) in the browser bundle. Each flag menu renders one <symbol> sprite and every flag is a <use> of it; the server build writes the sprite, hydration adopts it, and the browser build only reaches the bodies through a dynamic import() (its own chunk, dist/flag-bodies.js) taken when a flag menu renders with no server HTML.

Why the premise was wrong

The report was "tree-shaking is not happening: a header-only consumer gets nearly the whole 138 KB file". Reproduced with a minimal Vite + vite-plugin-solid consumer (fixtures/bundle-probe):

entry before after
baseline (router only) 9.1 kB (gzip 3.5)
SiteHeader only 147.5 kB (gzip 38.8) 42.8 kB (gzip 15.1)
every export 160.1 kB (gzip 42.3) 55.1 kB (gzip 18.2)

Importing everything costs 13 KB more than the header alone, so Rollup drops the unused components fine. The 138 KB is LocaleMenu reaching flags.mjs statically — the select/flag choice is a runtime prop, so every header reaches both branches — and Flag re-setting innerHTML in an effect, hydration included, so the client genuinely needed the bodies. preserveModules or per-component subpaths would not have removed a byte; the fix is where the data lives, not how the file is cut.

Measured

  • dist/solid.js: 138.2 KB (gzip 32.5) → 28.7 KB (gzip 8.2)
  • TraceLinq landing (packages/landing, TanStack Start, main client chunk, built against the packed tarball in a throwaway worktree): 290,997 B (gzip 86,474) → 186,280 B (gzip 62,096); the 105 KB flag chunk is emitted beside it and a hydrated page never requests it.

Guards

  • pnpm check builds the probe and fails if a flag body returns to the header-only main chunk, if that chunk exceeds 48 KB, if importing everything does not grow the bundle, or if the lazy chunk disappears.
  • tests/site-kit-contracts.test.mjs pins the packaging rules (menu imports only flag-countries.mjs; loader is a dynamic import; server configs alias the server loader; sprite is zero-sized, not display:none).
  • jsdom: client-only render fills the sprite after the chunk loads, one symbol per country, every <use> resolves, artwork ids scoped per menu, a product locale borrowing a vendored country shares its symbol. SSR: one sprite, fourteen symbols, the current locale's body written once and <use>d twice.
  • TraceLinq's own landing Playwright suite against the swapped-in kit, plus a hydration probe in Chromium: server sprite shape, zero flag-bodies requests on load and after a client-side locale change, flags render from the sprite, console clean.

API

@devslab/site-kit/flags keeps FLAG_COUNTRY, FLAGS_BY_COUNTRY, LOCALE_FLAGS, flagFor and gains flagCountryFor, FLAG_VIEWBOX. No consumer code changes; consumers take it through their caret bump. Decision D-020; backlog item 13.

Not done here

Server-rendered HTML still carries the bodies (once per country per menu, previously the current locale twice). Moving them to fetched <img src> files would need static serving in every consumer — a separate decision, noted in D-020's revisit clause.

`@devslab/site-kit/solid` was one 138 KB file and a consumer importing only
SiteHeader shipped nearly all of it. Reproduced with a minimal Vite +
vite-plugin-solid consumer (fixtures/bundle-probe): importing every export
costs 13 KB more than the header alone, so tree-shaking was fine. The bytes
were the fourteen vendored flag SVGs (~115 KB, Spain alone 85 KB): LocaleMenu
imported them statically, the select/flag choice is a runtime prop so every
header reached them, and Flag re-set innerHTML in an effect, hydration
included, so the client genuinely needed them.

Each flag menu now renders one <symbol> sprite and every flag is a <use> of
it, so a client locale change only swaps an href. The server build writes
the sprite (flag-bodies.server.ts, aliased by the server Vite and Vitest
configs); hydration adopts it; the browser build reaches the bodies only
through a dynamic import() emitted as dist/flag-bodies.js, taken when a menu
renders with no server HTML. The generator splits flags.mjs into
flag-countries.mjs and flag-bodies.mjs; the ./flags subpath keeps its API
and gains flagCountryFor and FLAG_VIEWBOX. The sprite is zero-sized rather
than display:none so referenced clip paths and gradients still resolve.

`pnpm check` builds the probe and fails if a body returns to the header-only
main chunk; tests/site-kit-contracts.test.mjs pins the packaging rules.

Measured (gzip): dist/solid.js 138.2 KB (32.5) -> 28.7 KB (8.2); header-only
consumer 147.5 KB (38.8) -> 42.8 KB (15.1); TraceLinq landing main client
chunk 291.0 KB (86.5) -> 186.3 KB (62.1), its landing Playwright suite 24/24
against the packed tarball, and a Chromium probe on /en, /ko, /ar: sprite in
the HTML, zero flag-bodies requests, trigger rendered from the sprite,
console clean. D-020.
@jlc488
jlc488 merged commit 81b3abd into main Sep 7, 2026
2 checks passed
@jlc488
jlc488 deleted the claude/silly-engelbart-dc4eba branch September 7, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant