Skip to content

Commit d6a2c73

Browse files
authored
Merge pull request #5 from node-projects/copilot/improve-css-parser-performance
Perf: eliminate hot-path allocations in parser and deduplicate compiler
2 parents c11ec8f + 3ec2307 commit d6a2c73

3 files changed

Lines changed: 192 additions & 227 deletions

File tree

src/parse/index.ts

Lines changed: 69 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,15 @@ export const parse = (
266266
* Parse selector.
267267
*/
268268
function selector() {
269-
const bracePos = indexOfArrayWithBracketAndQuoteSupport(lexer.remaining, [
270-
'{',
271-
]);
272-
if (bracePos === -1 || bracePos === 0) {
269+
const absPos = indexOfArrayWithBracketAndQuoteSupport(
270+
lexer.input,
271+
['{'],
272+
lexer.pos,
273+
);
274+
if (absPos === -1 || absPos === lexer.pos) {
273275
return;
274276
}
275-
const selectorStr = lexer.consume(bracePos);
277+
const selectorStr = lexer.consumeTo(absPos);
276278

277279
// remove comment in selector;
278280
const res = trim(selectorStr).replace(commentRegex, '');
@@ -300,12 +302,13 @@ export const parse = (
300302

301303
// val
302304
let value = '';
303-
const endValuePosition = indexOfArrayWithBracketAndQuoteSupport(
304-
lexer.remaining,
305+
const absEndPos = indexOfArrayWithBracketAndQuoteSupport(
306+
lexer.input,
305307
[';', '}'],
308+
lexer.pos,
306309
);
307-
if (endValuePosition !== -1) {
308-
value = lexer.consume(endValuePosition);
310+
if (absEndPos !== -1) {
311+
value = lexer.consumeTo(absEndPos);
309312
value = trim(value).replace(commentRegex, '');
310313
}
311314

@@ -346,11 +349,10 @@ export const parse = (
346349
lexer.hasMore &&
347350
lexer.charCodeAt() !== Ch_CLOSE
348351
) {
349-
const remaining = lexer.remaining;
350-
const semiPos = remaining.indexOf(';');
351-
const bracePos = remaining.indexOf('}');
352+
const semiPos = lexer.input.indexOf(';', lexer.pos);
353+
const bracePos = lexer.input.indexOf('}', lexer.pos);
352354
if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
353-
lexer.consume(semiPos + 1);
355+
lexer.consumeTo(semiPos + 1);
354356
whitespace();
355357
comments(decls);
356358
decl = declaration();
@@ -375,13 +377,25 @@ export const parse = (
375377
* ('{' appears before ';' and '}' at the top level).
376378
*/
377379
function looksLikeNestedRule(): boolean {
378-
const remaining = lexer.remaining;
379-
const bracePos = indexOfArrayWithBracketAndQuoteSupport(remaining, ['{']);
380+
const pos = lexer.pos;
381+
const bracePos = indexOfArrayWithBracketAndQuoteSupport(
382+
lexer.input,
383+
['{'],
384+
pos,
385+
);
380386
if (bracePos === -1) {
381387
return false;
382388
}
383-
const semiPos = indexOfArrayWithBracketAndQuoteSupport(remaining, [';']);
384-
const closePos = indexOfArrayWithBracketAndQuoteSupport(remaining, ['}']);
389+
const semiPos = indexOfArrayWithBracketAndQuoteSupport(
390+
lexer.input,
391+
[';'],
392+
pos,
393+
);
394+
const closePos = indexOfArrayWithBracketAndQuoteSupport(
395+
lexer.input,
396+
['}'],
397+
pos,
398+
);
385399

386400
if (semiPos !== -1 && semiPos < bracePos) {
387401
return false;
@@ -437,11 +451,10 @@ export const parse = (
437451

438452
// nothing matched — skip to next semicolon or closing brace to recover
439453
if (options?.silent) {
440-
const remaining = lexer.remaining;
441-
const semiPos = remaining.indexOf(';');
442-
const bracePos = remaining.indexOf('}');
454+
const semiPos = lexer.input.indexOf(';', lexer.pos);
455+
const bracePos = lexer.input.indexOf('}', lexer.pos);
443456
if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
444-
lexer.consume(semiPos + 1);
457+
lexer.consumeTo(semiPos + 1);
445458
whitespace();
446459
comments(items);
447460
continue;
@@ -496,11 +509,10 @@ export const parse = (
496509

497510
// nothing matched — skip to next semicolon or closing brace to recover
498511
if (options?.silent) {
499-
const remaining = lexer.remaining;
500-
const semiPos = remaining.indexOf(';');
501-
const bracePos = remaining.indexOf('}');
512+
const semiPos = lexer.input.indexOf(';', lexer.pos);
513+
const bracePos = lexer.input.indexOf('}', lexer.pos);
502514
if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
503-
lexer.consume(semiPos + 1);
515+
lexer.consumeTo(semiPos + 1);
504516
whitespace();
505517
comments(items);
506518
continue;
@@ -559,11 +571,11 @@ export const parse = (
559571
return error("@keyframes missing '{'");
560572
}
561573

562-
let frames: Array<CssKeyframeAST | CssCommentAST> = comments();
574+
const frames: Array<CssKeyframeAST | CssCommentAST> = comments();
563575
let frame: CssKeyframeAST | undefined = keyframe();
564576
while (frame) {
565577
frames.push(frame);
566-
frames = frames.concat(comments());
578+
comments(frames);
567579
frame = keyframe();
568580
}
569581

@@ -757,11 +769,11 @@ export const parse = (
757769
if (!open()) {
758770
return error(`@${name} missing '{'`);
759771
}
760-
let decls = comments<CssDeclarationAST>();
772+
const decls = comments<CssDeclarationAST>();
761773
let decl: CssDeclarationAST | undefined = declaration();
762774
while (decl) {
763775
decls.push(decl);
764-
decls = decls.concat(comments());
776+
comments(decls);
765777
decl = declaration();
766778
}
767779
if (!close()) {
@@ -867,13 +879,13 @@ export const parse = (
867879
if (!open()) {
868880
return error("@font-face missing '{'");
869881
}
870-
let decls = comments<CssDeclarationAST>();
882+
const decls = comments<CssDeclarationAST>();
871883

872884
// declarations
873885
let decl: CssDeclarationAST | undefined = declaration();
874886
while (decl) {
875887
decls.push(decl);
876-
decls = decls.concat(comments());
888+
comments(decls);
877889
decl = declaration();
878890
}
879891

@@ -901,11 +913,11 @@ export const parse = (
901913
if (!open()) {
902914
return error("@property missing '{'");
903915
}
904-
let decls = comments<CssDeclarationAST>();
916+
const decls = comments<CssDeclarationAST>();
905917
let decl: CssDeclarationAST | undefined = declaration();
906918
while (decl) {
907919
decls.push(decl);
908-
decls = decls.concat(comments());
920+
comments(decls);
909921
decl = declaration();
910922
}
911923
if (!close()) {
@@ -933,11 +945,11 @@ export const parse = (
933945
if (!open()) {
934946
return error("@counter-style missing '{'");
935947
}
936-
let decls = comments<CssDeclarationAST>();
948+
const decls = comments<CssDeclarationAST>();
937949
let decl: CssDeclarationAST | undefined = declaration();
938950
while (decl) {
939951
decls.push(decl);
940-
decls = decls.concat(comments());
952+
comments(decls);
941953
decl = declaration();
942954
}
943955
if (!close()) {
@@ -1020,11 +1032,11 @@ export const parse = (
10201032
if (!open()) {
10211033
return error("@view-transition missing '{'");
10221034
}
1023-
let decls = comments<CssDeclarationAST>();
1035+
const decls = comments<CssDeclarationAST>();
10241036
let decl: CssDeclarationAST | undefined = declaration();
10251037
while (decl) {
10261038
decls.push(decl);
1027-
decls = decls.concat(comments());
1039+
comments(decls);
10281040
decl = declaration();
10291041
}
10301042
if (!close()) {
@@ -1051,11 +1063,11 @@ export const parse = (
10511063
if (!open()) {
10521064
return error("@position-try missing '{'");
10531065
}
1054-
let decls = comments<CssDeclarationAST>();
1066+
const decls = comments<CssDeclarationAST>();
10551067
let decl: CssDeclarationAST | undefined = declaration();
10561068
while (decl) {
10571069
decls.push(decl);
1058-
decls = decls.concat(comments());
1070+
comments(decls);
10591071
decl = declaration();
10601072
}
10611073
if (!close()) {
@@ -1153,12 +1165,13 @@ export const parse = (
11531165

11541166
// Capture prelude (everything between the name and '{' or ';')
11551167
let prelude = '';
1156-
const preludeEnd = indexOfArrayWithBracketAndQuoteSupport(lexer.remaining, [
1157-
'{',
1158-
';',
1159-
]);
1160-
if (preludeEnd !== -1 && preludeEnd > 0) {
1161-
prelude = trim(lexer.consume(preludeEnd));
1168+
const preludeEnd = indexOfArrayWithBracketAndQuoteSupport(
1169+
lexer.input,
1170+
['{', ';'],
1171+
lexer.pos,
1172+
);
1173+
if (preludeEnd !== -1 && preludeEnd > lexer.pos) {
1174+
prelude = trim(lexer.consumeTo(preludeEnd));
11621175
}
11631176

11641177
// Block at-rule
@@ -1252,6 +1265,8 @@ function trim(str: string) {
12521265

12531266
/**
12541267
* Adds non-enumerable parent node reference to each node.
1268+
* Only recurses into array and object properties that can contain child nodes,
1269+
* skipping primitive values and known leaf objects (Position, etc.).
12551270
*/
12561271
function addParent<T1 extends { type?: string }>(
12571272
obj: T1,
@@ -1263,11 +1278,15 @@ function addParent<T1 extends { type?: string }>(
12631278
for (const k in obj) {
12641279
const value = obj[k];
12651280
if (Array.isArray(value)) {
1266-
value.forEach((v) => {
1267-
addParent(v, childParent);
1268-
});
1269-
} else if (value && typeof value === 'object') {
1270-
addParent(value, childParent);
1281+
for (let i = 0; i < value.length; i++) {
1282+
addParent(value[i], childParent);
1283+
}
1284+
} else if (
1285+
value &&
1286+
typeof value === 'object' &&
1287+
!(value instanceof Position)
1288+
) {
1289+
addParent(value as T1, childParent);
12711290
}
12721291
}
12731292

0 commit comments

Comments
 (0)