Skip to content

Commit b9af5a9

Browse files
authored
Improve collapsing of duplicate declarations (#6856)
* improve collapsing of duplicate properties In theory, we don't have to do anything because the browser is smart enough to figure everything out. However, leaving in duplicate properties is not that ideal for file size. Our previous method was pretty simple: if you see a declaration you already saw in this rule, delete the previous one and keep the current one. This works pretty well, but this gets rid of **all** the declarations with the same property. This is not great for overrides for older browsers. In a perfect world, we can handle this based on your target browser but this is a lot of unnecessary complexity and will slow things down performance wise. Alternative, we improved the solution by being a bit smarter: 1. Delete duplicate declarations that have the same property and value (this will get rid of **exact** duplications). 2. Delete declarations with the same property and the same **unit**. This means that we will reduce this: ```css .example { height: 50%; height: 100px; height: 20vh; height: 30%; height: 50px; height: 30vh; transform: var(--value); transform: var(--value); } ``` To: ```diff-css .example { - height: 50%; /* Another height exists later with a `%` unit */ - height: 100px; /* Another height exists later with a `px` unit */ - height: 20vh; /* Another height exists later with a `vh` unit */ height: 30%; height: 50px; height: 30vh; - transform: var(--value); /* Value is too complex, but is **exactly** the same as the one below */ transform: var(--value); } ``` This will reduce the values that we are 100% sure that can be safely removed. This will still result in some overrides but the browser can handle those for you. Fixes: #6844 * update changelog
1 parent 3149738 commit b9af5a9

File tree

4 files changed

+243
-1
lines changed

4 files changed

+243
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Ensure we can use `<` and `>` characters in modifiers ([#6851](https://github.com/tailwindlabs/tailwindcss/pull/6851))
1414
- Validate `theme()` works in arbitrary values ([#6852](https://github.com/tailwindlabs/tailwindcss/pull/6852))
1515
- Properly detect `theme()` value usage in arbitrary properties ([#6854](https://github.com/tailwindlabs/tailwindcss/pull/6854))
16+
- Improve collapsing of duplicate declarations ([#6856](https://github.com/tailwindlabs/tailwindcss/pull/6856))
1617

1718
## [3.0.8] - 2021-12-28
1819

src/lib/collapseDuplicateDeclarations.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default function collapseDuplicateDeclarations() {
33
root.walkRules((node) => {
44
let seen = new Map()
55
let droppable = new Set([])
6+
let byProperty = new Map()
67

78
node.walkDecls((decl) => {
89
// This could happen if we have nested selectors. In that case the
@@ -14,15 +15,79 @@ export default function collapseDuplicateDeclarations() {
1415
}
1516

1617
if (seen.has(decl.prop)) {
17-
droppable.add(seen.get(decl.prop))
18+
// Exact same value as what we have seen so far
19+
if (seen.get(decl.prop).value === decl.value) {
20+
// Keep the last one, drop the one we've seen so far
21+
droppable.add(seen.get(decl.prop))
22+
// Override the existing one with the new value. This is necessary
23+
// so that if we happen to have more than one declaration with the
24+
// same value, that we keep removing the previous one. Otherwise we
25+
// will only remove the *first* one.
26+
seen.set(decl.prop, decl)
27+
return
28+
}
29+
30+
// Not the same value, so we need to check if we can merge it so
31+
// let's collect it first.
32+
if (!byProperty.has(decl.prop)) {
33+
byProperty.set(decl.prop, new Set())
34+
}
35+
36+
byProperty.get(decl.prop).add(seen.get(decl.prop))
37+
byProperty.get(decl.prop).add(decl)
1838
}
1939

2040
seen.set(decl.prop, decl)
2141
})
2242

43+
// Drop all the duplicate declarations with the exact same value we've
44+
// already seen so far.
2345
for (let decl of droppable) {
2446
decl.remove()
2547
}
48+
49+
// Analyze the declarations based on its unit, drop all the declarations
50+
// with the same unit but the last one in the list.
51+
for (let declarations of byProperty.values()) {
52+
let byUnit = new Map()
53+
54+
for (let decl of declarations) {
55+
let unit = resolveUnit(decl.value)
56+
if (unit === null) {
57+
// We don't have a unit, so should never try and collapse this
58+
// value. This is because we can't know how to do it in a correct
59+
// way (e.g.: overrides for older browsers)
60+
continue
61+
}
62+
63+
if (!byUnit.has(unit)) {
64+
byUnit.set(unit, new Set())
65+
}
66+
67+
byUnit.get(unit).add(decl)
68+
}
69+
70+
for (let declarations of byUnit.values()) {
71+
// Get all but the last one
72+
let removableDeclarations = Array.from(declarations).slice(0, -1)
73+
74+
for (let decl of removableDeclarations) {
75+
decl.remove()
76+
}
77+
}
78+
}
2679
})
2780
}
2881
}
82+
83+
let UNITLESS_NUMBER = Symbol('unitless-number')
84+
85+
function resolveUnit(input) {
86+
let result = /^-?\d*.?\d+([\w%]+)?$/g.exec(input)
87+
88+
if (result) {
89+
return result[1] ?? UNITLESS_NUMBER
90+
}
91+
92+
return null
93+
}

tests/apply.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ test('@applying classes from outside a @layer respects the source order', async
351351
await run(input, config).then((result) => {
352352
return expect(result.css).toMatchFormattedCss(css`
353353
.baz {
354+
text-decoration-line: underline;
354355
text-decoration-line: none;
355356
}
356357
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { run, css, html } from './util/run'
2+
3+
it('should collapse duplicate declarations with the same units (px)', () => {
4+
let config = {
5+
content: [{ raw: html`<div class="example"></div>` }],
6+
corePlugins: { preflight: false },
7+
plugins: [],
8+
}
9+
10+
let input = css`
11+
@tailwind utilities;
12+
13+
@layer utilities {
14+
.example {
15+
height: 100px;
16+
height: 200px;
17+
}
18+
}
19+
`
20+
21+
return run(input, config).then((result) => {
22+
expect(result.css).toMatchFormattedCss(css`
23+
.example {
24+
height: 200px;
25+
}
26+
`)
27+
})
28+
})
29+
30+
it('should collapse duplicate declarations with the same units (no unit)', () => {
31+
let config = {
32+
content: [{ raw: html`<div class="example"></div>` }],
33+
corePlugins: { preflight: false },
34+
plugins: [],
35+
}
36+
37+
let input = css`
38+
@tailwind utilities;
39+
40+
@layer utilities {
41+
.example {
42+
line-height: 3;
43+
line-height: 2;
44+
}
45+
}
46+
`
47+
48+
return run(input, config).then((result) => {
49+
expect(result.css).toMatchFormattedCss(css`
50+
.example {
51+
line-height: 2;
52+
}
53+
`)
54+
})
55+
})
56+
57+
it('should not collapse duplicate declarations with the different units', () => {
58+
let config = {
59+
content: [{ raw: html`<div class="example"></div>` }],
60+
corePlugins: { preflight: false },
61+
plugins: [],
62+
}
63+
64+
let input = css`
65+
@tailwind utilities;
66+
67+
@layer utilities {
68+
.example {
69+
height: 100px;
70+
height: 50%;
71+
}
72+
}
73+
`
74+
75+
return run(input, config).then((result) => {
76+
expect(result.css).toMatchFormattedCss(css`
77+
.example {
78+
height: 100px;
79+
height: 50%;
80+
}
81+
`)
82+
})
83+
})
84+
85+
it('should collapse the duplicate declarations with the same unit, but leave the ones with different units', () => {
86+
let config = {
87+
content: [{ raw: html`<div class="example"></div>` }],
88+
corePlugins: { preflight: false },
89+
plugins: [],
90+
}
91+
92+
let input = css`
93+
@tailwind utilities;
94+
95+
@layer utilities {
96+
.example {
97+
height: 100px;
98+
height: 50%;
99+
height: 20vh;
100+
height: 200px;
101+
height: 100%;
102+
height: 30vh;
103+
}
104+
}
105+
`
106+
107+
return run(input, config).then((result) => {
108+
expect(result.css).toMatchFormattedCss(css`
109+
.example {
110+
height: 200px;
111+
height: 100%;
112+
height: 30vh;
113+
}
114+
`)
115+
})
116+
})
117+
118+
it('should collapse the duplicate declarations with the exact same value', () => {
119+
let config = {
120+
content: [{ raw: html`<div class="example"></div>` }],
121+
corePlugins: { preflight: false },
122+
plugins: [],
123+
}
124+
125+
let input = css`
126+
@tailwind utilities;
127+
128+
@layer utilities {
129+
.example {
130+
height: var(--value);
131+
color: blue;
132+
height: var(--value);
133+
}
134+
}
135+
`
136+
137+
return run(input, config).then((result) => {
138+
expect(result.css).toMatchFormattedCss(css`
139+
.example {
140+
color: blue;
141+
height: var(--value);
142+
}
143+
`)
144+
})
145+
})
146+
147+
it('should work on a real world example', () => {
148+
let config = {
149+
content: [{ raw: html`<div class="h-available"></div>` }],
150+
corePlugins: { preflight: false },
151+
plugins: [],
152+
}
153+
154+
let input = css`
155+
@tailwind utilities;
156+
157+
@layer utilities {
158+
.h-available {
159+
height: 100%;
160+
height: 100vh;
161+
height: -webkit-fill-available;
162+
}
163+
}
164+
`
165+
166+
return run(input, config).then((result) => {
167+
expect(result.css).toMatchFormattedCss(css`
168+
.h-available {
169+
height: 100%;
170+
height: 100vh;
171+
height: -webkit-fill-available;
172+
}
173+
`)
174+
})
175+
})

0 commit comments

Comments
 (0)