-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Collapse multiple utilities #19147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Collapse multiple utilities #19147
Conversation
This info is only needed during canonicalization, not when computing signatures. So let's split the options and embed the signatureOptions inside the canonicalizeOptions so we can pluck that information off.
This way we will re-use most caches in the test. We also had to update one test because `max-h-[20px]` would be converted to `max-h-5` otherwise and we want to test that we keep an arbitrary value.
This moves the canonicalization steps from within the signature to a separate function so we can use that later in other functions as well.
That's the only change here, and it's best viewed with `?w=1`. It will make future commits easier to reason about.
This will increase cache hits. E.g.: `w-4 h-4` and `h-4 w-4` will both resolve to `size-4`
But only if the internal `ExpandProperties` feature flag is enabled.
Right now all plugins are implemented using functions so they are a
literal black box. In a perfect world, we can leverage the ASTs from the
`@utility` CSS plugins.
To work around this, we use the `getClassList` Intellisense API, and
pre-compute all utilities + modifiers (~19k total). This way we can
consider each of them a "static" utility.
Next, we index by property, value and list of utilities which is also
cached in a bigger shared DefaultMap. This eventually produces:
```json
{
'width': {
'16px': ['w-4', 'size-4'],
…
},
'height': {
'16px': ['h-4', 'size-4']
},
…
}
```
Later we can use these lookups to find the intersection between the used
utilities. This means that if you are using `w-4 h-4`, that we can get
`size-4` out of it.
If there is more than 1 property and the feature flag is enabled, then we can try and collapse used utilities. To do this, we have to do some setup first: 1. Figure out what properties / values are used by each candidate 2. Figure out if 2 or more candidates can be linked to another known utility in the system based on the properties / values. This way `w-4`, `h-4` will be connected via `size-4`. But `underline` and `text-red-500` will never be linked together. 4. For each group, try each combination from high to low. E.g.: while `mt-1 mr-1 mb-1 ml-1` can be collapsed to `my-1 mx-1`, starting with the most combinations first means that we can immediately collapse this to `m-1`.
55401b1 to
2822138
Compare
|
|
||
| const internalOptionsCache = new DefaultMap((designSystem: DesignSystem) => { | ||
| return new DefaultMap((signatureOptions: SignatureOptions) => { | ||
| return new DefaultMap((features: Features) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The public API is just an object, but internally we can use a number which makes it easier to use as a cache key. One day we will have Record / Tuple types, but not yet...
|
|
||
| // Depending on the length of the value, map to different properties | ||
| let VARIADIC_EXPANSION_MAP: Record<string, Record<number, [prop: string, index: number][]>> = { | ||
| inset: createBareQuad(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These depend on the amount of arguments inset: 10px will set top, right, bottom, left to 10px. But 10px 20px will set top and bottom to 10px and left and right to 20px
|
|
||
| // The entire value is mapped to each property | ||
| let LOGICAL_EXPANSION_MAP: Record<string, string[]> = { | ||
| 'border-block': ['border-bottom', 'border-top'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These on the other hand map to the physical properties as-is.
border-block: 1px solid red;Would map to:
border-top: 1px solid red;
border-bottom: 1px solid red;There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good generally.
We should try to address the memory stuff we chatted about in a future PR though.
Some commits look like a lot of changes, but they just move things around, so best to use
?w=1when viewing commit by commit.This PR adds a new feature to the
designSystem.canonicalizeCandidatesto collapse multiple utilities to fewer utilities. To make this possible, we also have to convert some logical properties to physical properties (controllable via an option).We can already compute the signature of each utility, where if two utilities have the same signature they are considered the same.
However it's kind of impossible to generate all combinations of all utilities ever to figure out if there is a potential collapse possible. Even if we just focus on the incoming list of candidates, there could still be a lot of classes. So instead we have to be a bit more clever.
First, we group candidates together by the used variants and if the important
!flag was used. We can improve this in the future, but for now we won't even try to combinehover:w-4 h-4.Next, for each candidate, we figure out which property and value it uses. We can build up a lookup table for this. We already did this process for all utilities in the system as well.
The lookup table for
w-4 h-4 p-4might look something like this.{ "width": { "16px": ["w-4", "size-4"], }, "height": { "16px": ["h-4", "size-4"], }, "padding": { "16px": ["p-4"], } }Next, we can build groups of candidates where an intersection exists in the lookup table. In the example above, we can see that
w-4andh-4both map tosize-4for the value16px. So we can group these two candidates. Thep-4d doesn't intersect with anything else, so it remains alone. This also means that we only have to generate combinations for two candidates (2^2 = 4) instead of three (2^3 = 8). In practice, your class list might have many classes, so keeping this number low is important.When we generate combinations, we will generate the most amount of candidates first so we have the largest collapse possible. We also stop when we reach <= 1 combinations because we need at least two candidates to collapse.
Since this uses the internal design system, if you have custom
@utilitys, this will work as expected.If you then use:
Then we can collapse this to:
But even if we used:
It would still collapse to:
...because the
pt-4andpb-4can collapse topy-4, andpy-4andpx-4can collapse top-4.This is also where that logical to physical conversion comes into play, because while
pt-4andpb-4set the physicalpadding-topandpadding-bottomproperties. Thepy-4utility sets the logicalpadding-blockproperty. So we internally transformpadding-blockto thepadding-topandpadding-bottomphysical properties. The funny thing is that this logical to physical conversion actually means that we will convertpt-4andpb-4from physical to logical properties, because we converted topy-4...In most cases it's fine, and even preferred to use
py-1overpt-1 pb-1, but in case it's not, then you can disable thelogicalToPhysicaloption.Test plan
Added some dedicated tests for this new functionality.