Skip to content

Commit 211a4f2

Browse files
authored
Lower memory usage when comparing strings (#227)
1 parent d7771e6 commit 211a4f2

19 files changed

+316
-105
lines changed

benchmark/memory.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import * as fs from 'fs'
44
const files = [
55
['bol-com-20190617', 'Bol.com', 117],
66
['bootstrap-5.0.0', 'Bootstrap 5.0.0', 49],
7-
['css-tricks-20190319', 'CSS-Tricks', 50],
87
['cnn-20220403', 'CNN', 360],
8+
['css-tricks-20190319', 'CSS-Tricks', 50],
99
['facebook-20190319', 'Facebook.com', 71],
1010
['github-20210501', 'GitHub.com', 95],
1111
['gazelle-20210905', 'Gazelle.nl', 312],
@@ -47,4 +47,4 @@ suite.forEach(([name, fn, memory]) => {
4747
)
4848
})
4949

50-
console.log(((process.memoryUsage().heapUsed - startMemory) / 1024 / 1024).toFixed(2) + 'MB')
50+
console.log(((process.memoryUsage().heapUsed - startMemory) / 1024 / 1024).toFixed(2) + 'MB')

benchmark/parse-analyze-ratio.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ files.forEach(([, name]) => {
2323
})
2424

2525
console.log('Running benchmark on /dist/analyzer.js:')
26-
const header = `${'File'.padEnd(maxLen + 2)} | ${'Size'.padEnd(6)} | total | parse | Analyze |`
26+
const header = `${'File'.padEnd(maxLen + 2)} | ${'Size'.padStart(7)} | total | parse | Analyze |`
2727
console.log(''.padEnd(header.length, '='))
2828
console.log(header)
2929
console.log(''.padEnd(header.length, '='))
3030

3131
files.forEach(([filename, name]) => {
3232
const css = fs.readFileSync(`./src/__fixtures__/${filename}.css`, 'utf-8')
33-
const fileSize = byteSize(css.length).padStart(6)
33+
const fileSize = byteSize(css.length).padStart(7)
3434
const result = analyzeCss(css)
3535

3636
name = name.padEnd(maxLen + 2)

benchmark/readme.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,17 @@ Trello.com (312 kB): 13.15 ops/sec
2121
`node benchmark/parse-analyze-ratio.js`
2222

2323
```
24-
====================================================================
25-
File | Size | total | parse | Analyze |
26-
====================================================================
27-
Bol.com | 468 kB | 328ms | 207ms | 120ms (36.6%) |
28-
Bootstrap 5.0.0 | 195 kB | 120ms | 80ms | 39ms (32.5%) |
29-
CSS-Tricks | 195 kB | 96ms | 64ms | 32ms (33.3%) |
30-
Facebook.com | 268 kB | 134ms | 78ms | 56ms (41.8%) |
31-
GitHub.com | 514 kB | 172ms | 91ms | 79ms (45.9%) |
32-
Gazelle.nl | 972 kB | 363ms | 225ms | 134ms (36.9%) |
33-
Lego.com | 246 kB | 77ms | 40ms | 36ms (46.8%) |
34-
Smashing Magazine.com | 1.1 MB | 350ms | 210ms | 139ms (39.7%) |
35-
Trello.com | 312 kB | 92ms | 52ms | 40ms (43.5%) |
24+
=====================================================================
25+
File | Size | total | parse | Analyze |
26+
=====================================================================
27+
Bol.com | 468 kB | 330ms | 207ms | 122ms (37.0%) |
28+
Bootstrap 5.0.0 | 195 kB | 122ms | 87ms | 34ms (27.9%) |
29+
CSS-Tricks | 195 kB | 98ms | 64ms | 34ms (34.7%) |
30+
CNN | 1.77 MB | 563ms | 367ms | 193ms (34.3%) |
31+
Facebook.com | 268 kB | 96ms | 49ms | 47ms (49.0%) |
32+
GitHub.com | 514 kB | 161ms | 79ms | 80ms (49.7%) |
33+
Gazelle.nl | 972 kB | 343ms | 215ms | 124ms (36.2%) |
34+
Lego.com | 246 kB | 73ms | 37ms | 35ms (47.9%) |
35+
Smashing Magazine.com | 1.1 MB | 300ms | 162ms | 136ms (45.3%) |
36+
Trello.com | 312 kB | 84ms | 49ms | 34ms (40.5%) |
3637
```

benchmark/run.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const files = [
66
['bootstrap-5.0.0', 'Bootstrap 5.0.0', 49],
77
['cnn-20220403', 'CNN', 360],
88
['css-tricks-20190319', 'CSS-Tricks', 50],
9+
['cnn-20220403', 'CNN', 360],
910
['facebook-20190319', 'Facebook.com', 71],
1011
['github-20210501', 'GitHub.com', 95],
1112
['gazelle-20210905', 'Gazelle.nl', 312],

src/__fixtures__/cnn-20220403.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143417,11 +143417,12 @@
143417143417
}
143418143418
},
143419143419
"fontFamilies": {
143420-
"total": 816,
143421-
"totalUnique": 38,
143420+
"total": 817,
143421+
"totalUnique": 39,
143422143422
"unique": {
143423143423
"sans-serif": 2,
143424143424
"monospace,serif": 1,
143425+
"courier new,monospace": 1,
143425143426
"CNN": 28,
143426143427
"CNN Condensed": 4,
143427143428
"CNN Clock": 14,
@@ -143459,7 +143460,7 @@
143459143460
"cnn-icons ": 2,
143460143461
"CNN, \"Helvetica Neue\", Verdana, Helvetica, Arial, Utkal, sans-serif": 1
143461143462
},
143462-
"uniquenessRatio": 0.04656862745098039
143463+
"uniquenessRatio": 0.04773561811505508
143463143464
},
143464143465
"fontSizes": {
143465143466
"total": 2328,

src/atrules/atrules.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CountableCollection } from '../countable-collection.js'
22
import { hasVendorPrefix } from '../vendor-prefix.js'
3+
import { endsWith } from '../string-utils.js'
34

45
const analyzeAtRules = ({ atrules, stringifyNode }) => {
56
/** @type {{[index: string]: string}[]} */
@@ -49,7 +50,7 @@ const analyzeAtRules = ({ atrules, stringifyNode }) => {
4950
continue
5051
}
5152

52-
if (atRuleName.endsWith('keyframes')) {
53+
if (endsWith('keyframes', atRuleName)) {
5354
const name = `@${atRuleName} ${node.prelude}`
5455
keyframes.push(name)
5556

src/index.js

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import parse from 'css-tree/parser'
22
import walk from 'css-tree/walker'
3-
import { property as getProperty } from 'css-tree/utils'
43
import { analyzeRule } from './rules/rules.js'
54
import { analyzeSpecificity, compareSpecificity } from './selectors/specificity.js'
65
import { colorFunctions, colorNames } from './values/colors.js'
@@ -13,6 +12,9 @@ import { analyzeAtRules } from './atrules/atrules.js'
1312
import { ContextCollection } from './context-collection.js'
1413
import { CountableCollection } from './countable-collection.js'
1514
import { AggregateCollection } from './aggregate-collection.js'
15+
import { strEquals, startsWith, endsWith } from './string-utils.js'
16+
import { hasVendorPrefix } from './vendor-prefix.js'
17+
import { isCustom, isHack, isProperty } from './properties/property-utils.js'
1618

1719
/**
1820
* Analyze CSS
@@ -133,7 +135,7 @@ const analyze = (css) => {
133135
atrules.push({
134136
name: node.name,
135137
prelude: node.prelude && node.prelude.value,
136-
block: node.name === 'font-face' && node.block,
138+
block: strEquals('font-face', node.name) && node.block,
137139
})
138140
break
139141
}
@@ -153,7 +155,7 @@ const analyze = (css) => {
153155
case 'Selector': {
154156
const selector = stringifyNode(node)
155157

156-
if (this.atrule && this.atrule.name.endsWith('keyframes')) {
158+
if (this.atrule && endsWith('keyframes', this.atrule.name)) {
157159
keyframeSelectors.push(selector)
158160
return this.skip
159161
}
@@ -215,7 +217,7 @@ const analyze = (css) => {
215217
return this.skip
216218
}
217219
case 'Url': {
218-
if (node.value.startsWith('data:')) {
220+
if (startsWith('data:', node.value)) {
219221
embeds.push(node.value)
220222
}
221223
break
@@ -233,7 +235,7 @@ const analyze = (css) => {
233235
if (node.important) {
234236
importantDeclarations++
235237

236-
if (this.atrule && this.atrule.name.endsWith('keyframes')) {
238+
if (this.atrule && endsWith('keyframes', this.atrule.name)) {
237239
importantsInKeyframes++
238240
}
239241
}
@@ -243,59 +245,43 @@ const analyze = (css) => {
243245
properties.push(property)
244246
values.push(value)
245247

248+
if (hasVendorPrefix(property)) {
249+
propertyVendorPrefixes.push(property)
250+
} else if (isHack(property)) {
251+
propertyHacks.push(property)
252+
} else if (isCustom(property)) {
253+
customProperties.push(property)
254+
}
255+
246256
// Process properties first that don't have colors,
247257
// so we can avoid further walking them;
248-
// They're also typically not vendor prefixed,
249-
if (property === 'z-index') {
258+
if (isProperty('z-index', property)) {
250259
zindex.push(value)
251260
return this.skip
252-
}
253-
if (property === 'font') {
261+
} else if (isProperty('font', property)) {
254262
fontValues.push(value)
255263
break
256-
}
257-
if (property === 'font-size') {
264+
} else if (isProperty('font-size', property)) {
258265
fontSizeValues.push(stringifyNode(value))
259266
break
260-
}
261-
if (property === 'font-family') {
267+
} else if (isProperty('font-family', property)) {
262268
fontFamilyValues.push(stringifyNode(value))
263269
break
264-
}
265-
266-
const {
267-
vendor: isVendor,
268-
hack: isHack,
269-
custom: isCustom,
270-
basename,
271-
} = getProperty(property)
272-
273-
if (isVendor) {
274-
propertyVendorPrefixes.push(property)
275-
}
276-
if (isHack) {
277-
propertyHacks.push(property)
278-
}
279-
if (isCustom) {
280-
customProperties.push(property)
281-
}
282-
283-
if (basename === 'transition' || basename === 'animation') {
270+
} else if (isProperty('transition', property) || isProperty('animation', property)) {
284271
animations.push(value.children)
285272
break
286-
} else if (basename === 'animation-duration' || basename === 'transition-duration') {
273+
} else if (isProperty('animation-duration', property) || isProperty('transition-duration', property)) {
287274
durations.push(stringifyNode(value))
288275
break
289-
} else if (basename === 'transition-timing-function' || basename === 'animation-timing-function') {
276+
} else if (isProperty('transition-timing-function', property) || isProperty('animation-timing-function', property)) {
290277
timingFunctions.push(stringifyNode(value))
291278
break
292-
}
293-
294-
// These properties potentially contain colors
295-
if (basename === 'text-shadow') {
279+
} else if (isProperty('text-shadow', property)) {
296280
textShadows.push(value)
297-
} else if (basename === 'box-shadow') {
281+
// no break here: potentially contains colors
282+
} else if (isProperty('box-shadow', property)) {
298283
boxShadows.push(value)
284+
// no break here: potentially contains colors
299285
}
300286

301287
walk(value, function (valueNode) {

src/properties/property-utils.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { hasVendorPrefix } from '../vendor-prefix.js'
2+
import { endsWith } from '../string-utils.js'
3+
4+
/**
5+
* @param {string} property
6+
* @see https://github.com/csstree/csstree/blob/master/lib/utils/names.js#L69
7+
*/
8+
export function isHack(property) {
9+
if (isCustom(property) || hasVendorPrefix(property)) return false
10+
11+
let code = property.charCodeAt(0)
12+
13+
return code === 47 // /
14+
|| code === 95 // _
15+
|| code === 43 // +
16+
|| code === 42 // *
17+
|| code === 38 // &
18+
|| code === 36 // $
19+
|| code === 35 // #
20+
}
21+
22+
export function isCustom(property) {
23+
if (property.length < 3) return false
24+
// 45 === '-'.charCodeAt(0)
25+
return property.charCodeAt(0) === 45 && property.charCodeAt(1) === 45
26+
}
27+
28+
export function isProperty(basename, property) {
29+
if (isCustom(property)) return false
30+
return endsWith(basename, property)
31+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { suite } from 'uvu'
2+
import * as assert from 'uvu/assert'
3+
import { analyze } from '../index.js'
4+
import { isHack, isCustom, isProperty } from './property-utils.js'
5+
6+
const PropertyUtils = suite('Property Utils')
7+
8+
PropertyUtils('isHack', () => {
9+
assert.ok(isHack('/property'))
10+
assert.ok(isHack('//property'))
11+
assert.ok(isHack('_property'))
12+
assert.ok(isHack('+property'))
13+
assert.ok(isHack('*property'))
14+
assert.ok(isHack('&property'))
15+
assert.ok(isHack('#property'))
16+
assert.ok(isHack('$property'))
17+
18+
assert.not.ok(isHack('property'))
19+
assert.not.ok(isHack('-property'))
20+
assert.not.ok(isHack('--property'))
21+
})
22+
23+
PropertyUtils('isCustom', () => {
24+
assert.ok(isCustom('--property'))
25+
assert.ok(isCustom('--MY-PROPERTY'))
26+
assert.ok(isCustom('--x'))
27+
28+
assert.not.ok(isCustom('property'))
29+
assert.not.ok(isCustom('-property'))
30+
assert.not.ok(isCustom('-webkit-property'))
31+
assert.not.ok(isCustom('-moz-property'))
32+
})
33+
34+
PropertyUtils('isProperty', () => {
35+
assert.ok(isProperty('animation', 'animation'))
36+
assert.ok(isProperty('animation', 'ANIMATION'))
37+
assert.ok(isProperty('animation', '-webkit-animation'))
38+
assert.ok(isProperty('font', '_font'))
39+
assert.ok(isProperty('width', '*width'))
40+
41+
assert.not.ok(isProperty('animation', '--animation'))
42+
assert.not.ok(isProperty('property', '--Property'))
43+
})
44+
45+
PropertyUtils.run()

src/selectors/selectors.test.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ Selectors('have their specificity calculated', () => {
113113
color: green;
114114
}
115115
116+
* {
117+
color: brown;
118+
}
119+
116120
@media print {
117121
@media (min-width: 1000px) {
118122
@supports (display: grid) {
@@ -125,12 +129,13 @@ Selectors('have their specificity calculated', () => {
125129
`
126130
const actual = analyze(fixture)
127131
const expected = [
128-
[0, 0, 1,],
129-
[0, 0, 1,]
132+
[0, 0, 1],
133+
[0, 0, 0],
134+
[0, 0, 1]
130135
]
131136

132137
assert.equal(actual.selectors.specificity.items, expected)
133-
assert.equal(actual.selectors.total, 2)
138+
assert.equal(actual.selectors.total, 3)
134139
})
135140

136141
Selectors('calculates selector uniqueness', () => {

0 commit comments

Comments
 (0)