wcb/size-change-log.md
Ayo 5503e57897 feat: buffer attribute changes until end of onInit
The assumption of everything starting at `onInit` breaks when browsers
triggers `attributeChanged` first after parsing HTML. This buffers all
property initialization after `onInit` logic for more predictable
behavior
2026-07-05 11:54:44 +02:00

4.7 KiB

Size change log

A running record of how each correctness/feature change affects the WebComponent base class bundle, measured as min + brotli via size-limit. Baseline before the Phase 0 correctness work: 1.19 kB (limit 1.2 kB).

Change Size (min+brotli) Limit Reason & benefit
Move default-attribute reflection out of the constructor ~1.19 kB (≈0) 1.2 kB Custom Elements Spec compliance. Constructors may not mutate attributes, so document.createElement on a class with static props threw NotSupportedError in real browsers. Defaults now reflect on first connect, and markup/SSR-provided attributes win. Pure code movement — no net size cost.
Safe prop cloning (functions/instances) + define-time validation 1.31 kB (+0.12 kB) 1.2 → 1.35 kB No more DataCloneError. structuredClone of a function/class-instance default crashed construction. Defaults are now cloned per key (plain data deep-copied so instances don't share object/array state; non-cloneable values kept by reference), and a one-time warning names any default whose type can't reflect to an attribute. Limit raised to fit.
Derive prop types from defaults only; log type violations instead of throwing (+ static strictProps opt-in) 1.33 kB (+0.02 kB) 1.35 kB Loud, not fatal. The proxy setter locked a prop's type on first write and threw on mismatch; inside attributeChangedCallback that TypeError vanished into window.onerror and silently skipped rendering. Types are now taken from static props defaults only (undeclared props stay untyped), and a declared-type violation is logged via console.error and skipped so render()/onChanges() still run. Teams wanting hard enforcement can set static strictProps = true to restore throwing.
Correct empty-string / removed attribute handling; drop native-setter writes 1.33 kB (≈0) 1.35 kB State no longer corrupts on common attribute changes. attr="" coerced to boolean true (echoed as attr="true"); removeAttribute wrote null into the proxy and threw before render()/onChanges(); deserialize('', 'number') threw. attributeChangedCallback now keeps strings as-is ('' stays ''), resets removed attributes to the declared default, wraps deserialize so a malformed value falls back to the raw string instead of skipping render, and no longer writes vestigial this[prop] fields that clobbered native setters like title/id/lang. Logic swap — no net size cost.
onChanges gains a clear attribute-vs-property distinction (breaking) 1.34 kB (+0.01 kB) 1.35 kB Self-documenting change payload. onChanges previously passed only property, holding the kebab-case attribute name — confusing in a library where props are camelCase. property now holds the camelCase prop key (matching this.props access) and the kebab attribute name moves to a new attribute field. Handlers can read this.props[property] directly without re-deriving the key. Breaking: code reading changes.property for the attribute name must switch to changes.attribute (see the migration note on the life-cycle-hooks docs page).
Buffer attribute-driven render/onChanges until after onInit 1.35 kB (+0.01 kB) 1.35 kB onInit is guaranteed to run first. Per spec, authored attributes fire attributeChangedCallback (→ render/onChanges) before connectedCallback, so components that assume onInit already ran break in real browsers (happy-dom hides this). A #connected flag now buffers the render/onChanges side effects until after onInit, while the prop value is still applied immediately so props is correct inside onInit. Pre-connect changes are reflected by the first render and not replayed through onChanges.