Skip to content

Commit

Permalink
Improve performance of transformCss with large amounts of class names (
Browse files Browse the repository at this point in the history
  • Loading branch information
mattcompiles authored Sep 21, 2022
1 parent b53558a commit 9191d5a
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 28 deletions.
7 changes: 7 additions & 0 deletions .changeset/ninety-windows-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@vanilla-extract/css': patch
---

Improve performance of selector transforms

This issue occured on M1 Macs due to performance issues with large regex patterns.
2 changes: 1 addition & 1 deletion packages/css/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,13 @@
"dependencies": {
"@emotion/hash": "^0.8.0",
"@vanilla-extract/private": "^1.0.3",
"ahocorasick": "1.0.2",
"chalk": "^4.1.1",
"css-what": "^5.0.1",
"cssesc": "^3.0.0",
"csstype": "^3.0.7",
"deep-object-diff": "^1.1.0",
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
"media-query-parser": "^2.0.2",
"outdent": "^0.8.0"
},
Expand Down
7 changes: 7 additions & 0 deletions packages/css/src/ahocorasick.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare module 'ahocorasick' {
export default class Ahocorasick {
constructor(searchTerms: Array<string>);

search(input: string): Array<[endIndex: number, matches: Array<string>]>;
}
}
43 changes: 43 additions & 0 deletions packages/css/src/transformCss.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { setFileScope, endFileScope } from './fileScope';
import { createVar } from './vars';
import { transformCss } from './transformCss';
import { style } from './style';

setFileScope('test');

const testVar = createVar();
const style1 = style({});
const style2 = style({});

describe('transformCss', () => {
it('should escape class names', () => {
Expand Down Expand Up @@ -1564,4 +1567,44 @@ describe('transformCss', () => {
});
});

it('should handle multiple references to the same locally scoped selector', () => {
expect(
transformCss({
composedClassLists: [],
localClassNames: [style1, style2, '_1g1ptzo1', '_1g1ptzo10'],
cssObjs: [
{
type: 'local',
selector: style1,
rule: {
selectors: {
[`${style2} &:before, ${style2} &:after`]: {
background: 'black',
},

[`_1g1ptzo1_1g1ptzo10 ${style1}`]: {
background: 'blue',
},

[`_1g1ptzo10_1g1ptzo1 ${style1}`]: {
background: 'blue',
},
},
},
},
],
}).join('\n'),
).toMatchInlineSnapshot(`
".skkcyc2 .skkcyc1:before, .skkcyc2 .skkcyc1:after {
background: black;
}
._1g1ptzo1._1g1ptzo10 .skkcyc1 {
background: blue;
}
._1g1ptzo10._1g1ptzo1 .skkcyc1 {
background: blue;
}"
`);
});

endFileScope();
78 changes: 61 additions & 17 deletions packages/css/src/transformCss.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import './ahocorasick.d';

import { getVarName } from '@vanilla-extract/private';
import cssesc from 'cssesc';
import escapeStringRegexp from 'escape-string-regexp';
import AhoCorasick from 'ahocorasick';

import type {
CSS,
Expand Down Expand Up @@ -78,6 +80,18 @@ function dashify(str: string) {
.toLowerCase();
}

function replaceBetweenIndexes(
target: string,
startIndex: number,
endIndex: number,
replacement: string,
) {
const start = target.slice(0, startIndex);
const end = target.slice(endIndex);

return `${start}${replacement}${end}`;
}

const DOUBLE_SPACE = ' ';

const specialKeys = [
Expand All @@ -100,7 +114,8 @@ class Stylesheet {
currConditionalRuleset: ConditionalRuleset | undefined;
fontFaceRules: Array<GlobalFontFaceRule>;
keyframesRules: Array<CSSKeyframesBlock>;
localClassNameRegex: RegExp | null;
localClassNamesMap: Map<string, string>;
localClassNamesSearch: AhoCorasick;
composedClassLists: Array<{ identifier: string; regex: RegExp }>;

constructor(
Expand All @@ -111,10 +126,10 @@ class Stylesheet {
this.conditionalRulesets = [new ConditionalRuleset()];
this.fontFaceRules = [];
this.keyframesRules = [];
this.localClassNameRegex =
localClassNames.length > 0
? RegExp(`(${localClassNames.map(escapeStringRegexp).join('|')})`, 'g')
: null;
this.localClassNamesMap = new Map(
localClassNames.map((localClassName) => [localClassName, localClassName]),
);
this.localClassNamesSearch = new AhoCorasick(localClassNames);

// Class list compositions should be priortized by Newer > Older
// Therefore we reverse the array as they are added in sequence
Expand Down Expand Up @@ -252,6 +267,12 @@ class Stylesheet {
};
}

transformClassname(identifier: string) {
return `.${cssesc(identifier, {
isIdentifier: true,
})}`;
}

transformSelector(selector: string) {
// Map class list compositions to single identifiers
let transformedSelector = selector;
Expand All @@ -263,18 +284,41 @@ class Stylesheet {
});
}

return this.localClassNameRegex
? transformedSelector.replace(
this.localClassNameRegex,
(_, className, index) => {
if (index > 0 && transformedSelector[index - 1] === '.') {
return className;
}
if (this.localClassNamesMap.has(transformedSelector)) {
return this.transformClassname(transformedSelector);
}

return `.${cssesc(className, { isIdentifier: true })}`;
},
)
: transformedSelector;
const results = this.localClassNamesSearch.search(transformedSelector);

let lastReplaceIndex = transformedSelector.length;

// Perform replacements backwards to simplify index handling
for (let i = results.length - 1; i >= 0; i--) {
const [endIndex, [firstMatch]] = results[i];
const startIndex = endIndex - firstMatch.length + 1;

if (startIndex >= lastReplaceIndex) {
// Class names can be substrings of other class names
// e.g. '_1g1ptzo1' and '_1g1ptzo10'
// If the startIndex >= lastReplaceIndex, then
// this is the case and this replace should be skipped
continue;
}

lastReplaceIndex = startIndex;

// If class names already starts with a '.' then skip
if (transformedSelector[startIndex - 1] !== '.') {
transformedSelector = replaceBetweenIndexes(
transformedSelector,
startIndex,
endIndex + 1,
this.transformClassname(firstMatch),
);
}
}

return transformedSelector;
}

transformSelectors(
Expand Down
17 changes: 7 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9191d5a

Please sign in to comment.