What the new colors cost, and whether hashing is the fix.
76 KB
docs CSS over the wire (brotli) · 127 KB gzip · 1,263 KB minified
1,727
custom properties, 1,180 of them --colors-*
18%
of the minified CSS is the 8 tone families + dynamic (231 KB; 4.9 KB brotli)
31–47 ms
parse time for every variant, hashed or not (difference within noise)
Same design system, same sources, four hash settings. What a browser downloads is the gzip or brotli column.
Hashing class names only is the one setting that shrinks what's downloaded: 10 KB gzip,1.8 KB brotli. In exchange, devtools show .hryzeb where they now show .px_sm, and the recipe class names stop meaning anything. Hashing vars makes both compressed sizes larger.
panda lib bakes the setting into the design system: a consumer whose hash differs getsdesign_system_option_mismatch. Turning it on is a breaking change for every app, not a docs setting.
Cascade layers of the docs CSS. The token declarations themselves are a small share.
The token layer, every ramp, surface and text value in light and dark, is 111 KB (9%). Most of the rest is utilities, and 253 KB of those (20%) are251 colorPalette rules, each re-pointing63 virtual tokens. They compress to2.8 KB brotli because they are so repetitive, but the browser still parses and stores every one.
An app that uses no components at all still gets1,168 KB (67 KB brotli),92% of the docs CSS. The design system's build info carries every recipe variant, and nothing tree-shakes it today (05 shows how to).
The docs CSS minus one family's token declarations and every rule that names it.
A ramp-only family (amber … olive) is just its token declarations, about 2 KB. A tone family costs 13× that, and the extra isn't tokens:
Consumer apps that import only a few components: before this work, after the fixes alone, and with each plugin option. gzip (minified).
Panda's optimize.treeshakeDesignSystem keeps only the recipes whose *Styles export the app imports, and it only counts import { buttonStyles } from "@pindoba/styled-system" written in the app's own files. Our apps import @pindoba/astro-button; only core-button imports buttonStyles. With the option on as is, docs pages lose the CSS of 70% of their styled classes.
The fix is a Panda plugin. Its parser:before hook adds the missing import to each app file that imports a @pindoba/* package, listing every *Styles that package reaches through its dependencies. The import goes into the frontmatter of an .astro file or the <script> of a .svelte/.vue file, because Panda reads imports only there. On the docs, which render every component, it keeps the CSS for every class except 21 native-select ones. Those could never be kept: styles-select-native and styles-select both exported selectStyles, and Panda's export map keys on the name. It is now selectNativeStyles, and the styles-map build fails if two modules ever collide again.
Every size lever Panda 2.2 offers, measured on our CSS.
What the docs CSS (every component) was still made of after the fixes, and what moved Loading.
581 KB
of selectors, against 451 KB of declarations
682
rules with a selector over 200 characters (339 KB), from only 381 conditions
167 KB
what those would take written once per condition (estimate)
Panda writes component styles as atomic CSS: one tiny rule per declaration, shared by every component that uses it. For a short condition (_hover) that is compact. For a long one, each declaration repeats the whole condition twice — escaped in the class name and again as the selector. A config recipe writes one rule per slot and variant instead:
/* atomic (sva): one rule per declaration */
.d_flex { display: flex }
.px_sm { padding-inline: var(--spacing-sm) }
/* config recipe: one rule per slot and variant */
.button__root { display: flex; padding-inline: var(--spacing-sm) }Config recipes are emitted in Panda's recipes cascade layer, below utilities, where every component's atomic styles live — so a button's own background would beat the skeleton. Every treatment declaration is !important to cover them (it now also covers inline style=). The treatment still ships only with Loading: styles-loading calls each variant with literals, and panda lib records them in that module's build info.
Everything else stays atomic. 77 connects build their classes from xStyles.raw() merged by css() property by property — what passThrough and the 44 cores built on connectPanel rely on — and a config recipe is a finished class that can't be merged that way. Config recipes are for components whose CSS is dominated by long conditions.
Each recommendation below, shipped or skipped, and why.
The docs CSS, which renders every component, went from 1,263 KB to 1,077 KB minified with the palette and corner fixes, and to 901 KB once Loading's treatment became a config recipe (127 KB → 107 KB gzip). Most of what remains is atomic CSS for short conditions, which is already compact. Consumer setup is in Shipping less CSS.
As written before the work, ranked by bytes saved per unit of risk.