Skip to content

Minize css with at property - #320

Merged
ameerf-wix merged 8 commits into
masterfrom
minize_css_with_at_property
Sep 6, 2026
Merged

Minize css with at property#320
ameerf-wix merged 8 commits into
masterfrom
minize_css_with_at_property

Conversation

@ameerf-wix

@ameerf-wix ameerf-wix commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

generate() CSS output — what minize_css_with_at_property buys us

Four stacked changes to the CSS emitted by generate(). Numbers below come from running
generate(config) over 11 example configs at each revision, with @wix/motion-presets
registered so named effects expand to real keyframes.

1. @property + short global names + one shared lists rule

Per-target hashed custom properties become short global indexed names, the per-target
coordinated-list rules collapse into a single rule with a joined selector list, and the
repeated inline var(--x, fallback) fallbacks move into one @property preamble:

/* before */
[data-interact-key="hero"] > :first-child {
  animation: var(--animation-0-8diz7tv2pl, none);
  animation-composition: var(--animation-composition-0-8diz7tv2pl, replace);
  animation-timeline: var(--animation-timeline-0-8diz7tv2pl, auto);
  animation-range: var(--animation-range-0-8diz7tv2pl, normal);
}

/* after */
@property --anm-0 { syntax: "*"; inherits: false; initial-value: none; }
@property --anm-cmps-0 { syntax: "*"; inherits: false; initial-value: replace; }
@property --anm-tmln-0 { syntax: "*"; inherits: false; initial-value: auto; }
@property --anm-rng-0 { syntax: "*"; inherits: false; initial-value: normal; }

[data-interact-key="hero"] > :first-child {
  animation: var(--anm-0);
  animation-composition: var(--anm-cmps-0);
  animation-timeline: var(--anm-tmln-0);
  animation-range: var(--anm-rng-0);
}

The names no longer depend on the target, so the cost is paid once per page instead of
once per target.

2. Default elision

Once @property supplies an initial-value, writing that same value back is redundant.
A custom property is now skipped when its value is exactly the default and nothing set
that same property to a non-default value earlier on the same target
(tracked as a
per-target assigned set).

In practice this deletes the --anm-cmps-N: replace; --anm-tmln-N: auto; --anm-rng-N: normal;
triplet from every ordinary time-based animation — the common case — while leaving
scroll-driven effects, which set a real timeline and range, untouched. Across the 11
example configs: 73 declarations / 1825 bytes removed, with no change to slot
allocation or to the @property preamble.

Both halves of the condition are load-bearing. The lists are coordinated positionally, so
a slot holding two animations must contribute two entries to each list; eliding
--anm-tmln-0: auto, auto in favour of the one-entry initial-value would let CSS cycle
the shorter list and hand the wrong timeline to a later animation. Comparing against the
exact single-value default is what keeps the arity honest; assigned is what keeps the
cascade honest.

3. Single-slot sequence collapse

Slots exist because the effects of a sequence run together: each needs its own entry in
the target's animation list, so effect N writes --anm-slot-N and a per-target
coordinated-list rule assembles the slots into the interaction-level --anm-M. A target
that appears only once in the sequence needs none of that machinery — one value, one
property — and can write --anm-M directly, exactly as a non-sequence effect does:

/* before */                                   /* after */
[data-interact-key="hero-title"] > :first-child {
  --anm-slot-0: motion-fadeIn 600ms …;           --anm-0: motion-fadeIn 600ms …;
}
[data-interact-key="hero-title"] > :first-child {
  --anm-0: var(--anm-slot-0);                  ← whole rule dropped
  --anm-cmps-0: var(--anm-cmps-slot-0);
  --anm-tmln-0: var(--anm-tmln-slot-0);
  --anm-rng-0: var(--anm-rng-slot-0);
}

parseSequence pre-counts slot usage per (target, list kind) before walking the effects —
the tally is an upper bound, so a count of ≤ 1 is a guarantee, not a guess — and threads
useSlots: Record<ListKind, boolean> down to getCustomProps. Top-level effects pass
NO_SLOTS, which is the old non-sequence path unchanged.

Each collapsed target drops a four-declaration coordinated-list rule, and once no target
in the config needs slots at all the four @property --*-slot-N registrations go with it.
The decision is per target and per list kind, so a sequence that repeats one target
and touches another once keeps slots for the first and collapses the second.

4. Structural refactor (no output change)

src/core/css.ts / cssUtils.ts cleanup — all example outputs are byte-identical before
and after:

  • TargetContext's ten duplicated counters (one set per list kind) collapse into two
    ListCounters, with names that distinguish the two namespaces they index
    (listIndex for the shorthand list, slotCursor for sequence slots). Every
    name === 'transition' ? … : … ternary became target[listKind(name)].
  • The effect → sequence → interaction hierarchy became explicit endEffect /
    endSequence / endInteraction helpers.
  • Parameter lists shrank: buildSequenceListsRule from ten positional params to two,
    parseEffect/parseSequence/parseInteraction from 9/8/7 to a shared
    GenerateContext built once.
  • effectToCSS reports what it wrote instead of mutating shared state and letting the
    caller infer it — which also makes inert effects uniformly not consume a slot, rather
    than doing so only when an earlier sequence happened to fill that index.

Size

Characters of generated CSS, master → this branch:

example master branch Δ
01-entrance-single 858 942 +84 (10%)
02-hover-transition 368 410 +42 (11%)
03-two-interactions-same-target 1146 1270 +124 (11%)
04-three-animations-same-target 2197 2486 +289 (13%)
05-sequence 1461 1803 +342 (23%)
06-sequence-plus-effect 2423 2841 +418 (17%)
07-multiple-targets 3493 3087 -406 (-12%)
08-conditions-media 876 960 +84 (10%)
09-page-scale 27896 19852 -8044 (-29%)
10-sequence-across-targets 3835 2226 -1609 (-42%)
11-sequence-mixed-targets 2420 2007 -413 (-17%)
total 46973 37884 -9089 (-19%)

Whitespace-normalized (leading indentation stripped from both sides, isolating the
structural change from the pretty-printing that landed alongside it) the total is
46973 → 36720, -10253 (-22%).

The @property preamble is a fixed per-page cost, so the small single-interaction
fixtures come out slightly larger; the elision pays back 58% of that overhead, leaving
~4% over master for examples 01–08 combined. The trade turns clearly positive as soon as
a page has several targets — 09-page-scale (25 targets, 10 with an extra hover
transition) is the realistic shape, and it drops -29% (-31% whitespace-normalized).

The collapse only moves examples 10 and 11, the two fixtures that contain a sequence with
a single-slot target, but it moves them hard: 10-sequence-across-targets — one sequence,
one effect per target, the shape a staggered entrance actually has — drops a further
-972 (-30%) on top of the elision, and is the only example where the slot
@property registrations disappear entirely.

Tests

packages/interact: 477 passed / 477 — 473 before the branch, 474 after the elision
and refactor, 477 after the collapse. Rest of the monorepo is green: @wix/motion 342,
@wix/motion-presets 516, @wix/interact-validate 170, @wix/splittext 131.

Eight assertions in test/css.spec.ts changed for the elision — none a rendering
regression. Each either asserted a default override the branch now deliberately omits, or
used an inert effect as an incidental fixture for something unrelated (one of those was
passing vacuously and now can't). Two new tests pin the two halves of the elision guard,
and one covers buildSequenceListsRule with both list kinds at non-zero indices.

For the collapse, one existing test was re-baselined —
should apply sequence-level conditions to the coordinated-list rule used a single-effect
sequence, which now emits no coordinated-list rule, so its fixture grew a second effect on
the same target and its assertions are unchanged — and three were added: the animation
kind, the transition kind, and the mixed shape where only one of two targets collapses.

One intended behaviour change worth calling out: an interaction whose only effect is inert
used to emit four reset declarations plus a lists rule, and now emits an empty stylesheet —
correct, since nothing ever turned those slots on, but visible to anything inspecting
_generate().cssRules.

ee4fa44 re-baselines the one CSS assertion in
packages/splittext/test/splitText.integration.spec.ts onto the indented output this
branch emits. That spec imports generate from the built @wix/interact, so it needs
a rebuilt dist/ to pass locally; CI runs yarn build before yarn test, so it is
unaffected.

Unrelated pre-existing issue: packages/interact/tsconfig.json includes only src/**, so
yarn lint never typechecks test files; test/css.spec.ts has a TS2537 that no CI step
currently surfaces.

@ameerf-wix
ameerf-wix marked this pull request as ready for review September 6, 2026 02:23
@ameerf-wix
ameerf-wix merged commit 660a598 into master Sep 6, 2026
1 check passed
@ameerf-wix
ameerf-wix deleted the minize_css_with_at_property branch September 6, 2026 08:24
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.

2 participants