feat(site-kit): the flag artwork leaves the browser bundle - #54
Merged
Conversation
`@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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
@devslab/site-kit/solidno 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 dynamicimport()(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):SiteHeaderonlyImporting everything costs 13 KB more than the header alone, so Rollup drops the unused components fine. The 138 KB is
LocaleMenureachingflags.mjsstatically — theselect/flagchoice is a runtime prop, so every header reaches both branches — andFlagre-settinginnerHTMLin an effect, hydration included, so the client genuinely needed the bodies.preserveModulesor 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)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 checkbuilds 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.mjspins the packaging rules (menu imports onlyflag-countries.mjs; loader is a dynamic import; server configs alias the server loader; sprite is zero-sized, notdisplay:none).<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.flag-bodiesrequests on load and after a client-side locale change, flags render from the sprite, console clean.API
@devslab/site-kit/flagskeepsFLAG_COUNTRY,FLAGS_BY_COUNTRY,LOCALE_FLAGS,flagForand gainsflagCountryFor,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.