Skip to content

Commit 28fbdce

Browse files
perf: more css optimizations (#21109)
1 parent 028c549 commit 28fbdce

9 files changed

Lines changed: 261 additions & 133 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Avoid toLowerCase allocations in CSS keyword comparisons.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Speed up CSS identifier escaping with a char-class lookup table.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Avoid quadratic line scan when building CSS module exports source maps.

.changeset/css-lazy-comment-loc.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Compute CSS comment source locations lazily.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Speed up CSS identifier unescaping with bulk run flushing.

lib/css/CssGenerator.js

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,32 @@ const buildExportsSourceMap = (
8787
) => {
8888
const lines = generatedJs.split("\n");
8989

90-
const lineByExport = new Map();
90+
// Resolve each export's generated line in a single O(L) pass instead of
91+
// scanning every line per export (O(E·L), pathological for export-heavy CSS
92+
// modules). Export entries are emitted as `\t"<name>": <value>`, so a line's
93+
// leading JSON-string key is matched against the requested names.
94+
const keyToName = new Map();
9195
for (const [exportName] of exportLocs) {
92-
const needle = `${JSON.stringify(exportName)}:`;
93-
for (let i = 0; i < lines.length; i++) {
94-
if (lines[i].includes(needle)) {
95-
lineByExport.set(exportName, i);
96-
break;
96+
keyToName.set(JSON.stringify(exportName), exportName);
97+
}
98+
const lineByExport = new Map();
99+
for (let i = 0; i < lines.length && lineByExport.size < keyToName.size; i++) {
100+
const line = lines[i];
101+
if (line.charCodeAt(0) !== 9 || line.charCodeAt(1) !== 34) continue;
102+
let j = 2;
103+
while (j < line.length) {
104+
const c = line.charCodeAt(j);
105+
if (c === 92) {
106+
j += 2;
107+
continue;
97108
}
109+
if (c === 34) break;
110+
j++;
111+
}
112+
if (line.charCodeAt(j + 1) !== 58) continue;
113+
const name = keyToName.get(line.slice(1, j + 1));
114+
if (name !== undefined && !lineByExport.has(name)) {
115+
lineByExport.set(name, i);
98116
}
99117
}
100118

@@ -522,7 +540,9 @@ class CssGenerator extends Generator {
522540
}
523541

524542
return new ConcatSource(
525-
`${RuntimeGlobals.cssInjectStyle}(${JSON.stringify(moduleId)}, `,
543+
`${RuntimeGlobals.cssInjectStyle}(${JSON.stringify(
544+
moduleId
545+
)}, `,
526546
this._cssToJsLiteral(cssSource, devtool, generateContext),
527547
");"
528548
);
@@ -671,7 +691,9 @@ class CssGenerator extends Generator {
671691
identifier
672692
);
673693
source.add(
674-
`${generateContext.runtimeTemplate.renderConst()} ${identifier} = ${JSON.stringify(v)};\n`
694+
`${generateContext.runtimeTemplate.renderConst()} ${identifier} = ${JSON.stringify(
695+
v
696+
)};\n`
675697
);
676698
}
677699
return source;

lib/css/CssParser.js

Lines changed: 99 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const topologicalSort = require("../util/topologicalSort");
3131
const {
3232
NodeType,
3333
SourceProcessor,
34+
equalsLowerCase,
3435
unescapeIdentifier
3536
} = require("./walkCssTokens");
3637

@@ -58,7 +59,6 @@ const {
5859

5960
/** @typedef {[number, number]} Range */
6061
/** @typedef {{ line: number, column: number }} Position */
61-
/** @typedef {{ value: string, range: Range, loc: { start: Position, end: Position } }} Comment */
6262

6363
const CC_COLON = ":".charCodeAt(0);
6464
const CC_SEMICOLON = ";".charCodeAt(0);
@@ -69,6 +69,37 @@ const CC_CARRIAGE_RETURN = "\r".charCodeAt(0);
6969
const CC_FORM_FEED = "\f".charCodeAt(0);
7070
const CC_LEFT_CURLY = "{".charCodeAt(0);
7171

72+
// A parsed CSS comment. `loc` is computed on demand — only magic-comment error
73+
// warnings read it, so comment-heavy CSS skips the per-comment line/col work.
74+
class Comment {
75+
/**
76+
* @param {string} value comment body (without the surrounding delimiters)
77+
* @param {Range} range `[start, end]` byte range including delimiters
78+
* @param {LocConverter} locConverter shared loc converter
79+
*/
80+
constructor(value, range, locConverter) {
81+
this.value = value;
82+
this.range = range;
83+
this._locConverter = locConverter;
84+
}
85+
86+
/**
87+
* @returns {{ start: Position, end: Position }} source location
88+
*/
89+
get loc() {
90+
const lc = this._locConverter;
91+
// `LocConverter#get` mutates and returns itself, so snapshot between calls.
92+
const s = lc.get(this.range[0]);
93+
const sl = s.line;
94+
const sc = s.column;
95+
const e = lc.get(this.range[1]);
96+
return {
97+
start: { line: sl, column: sc },
98+
end: { line: e.line, column: e.column }
99+
};
100+
}
101+
}
102+
72103
// Newlines (CSS Syntax 3 §3.3) — listed explicitly since there's no preprocessing stage.
73104
const STRING_MULTILINE = /\\[\n\r\f]/g;
74105
// https://www.w3.org/TR/css-syntax-3/#whitespace
@@ -862,17 +893,9 @@ class CssParser extends Parser {
862893
*/
863894
const comment = (input, start, end) => {
864895
if (!this.comments) this.comments = [];
865-
const { line: sl, column: sc } = locConverter.get(start);
866-
const { line: el, column: ec } = locConverter.get(end);
867-
const value = input.slice(start + 2, end - 2);
868-
this.comments.push({
869-
value,
870-
range: [start, end],
871-
loc: {
872-
start: { line: sl, column: sc },
873-
end: { line: el, column: ec }
874-
}
875-
});
896+
this.comments.push(
897+
new Comment(input.slice(start + 2, end - 2), [start, end], locConverter)
898+
);
876899
return end;
877900
};
878901

@@ -1371,7 +1394,7 @@ class CssParser extends Parser {
13711394
if (
13721395
j >= fn.value.length ||
13731396
fn.value[j].type !== NodeType.Ident ||
1374-
/** @type {Token} */ (fn.value[j]).value.toLowerCase() !== "from"
1397+
!equalsLowerCase(/** @type {Token} */ (fn.value[j]).value, "from")
13751398
) {
13761399
emitDashedIdentExport(identStart, identEnd);
13771400
return;
@@ -1521,8 +1544,10 @@ class CssParser extends Parser {
15211544
const next = values[i + 1];
15221545
if (!next) continue;
15231546
if (next.type === NodeType.Ident) {
1524-
const id = /** @type {Token} */ (next).value.toLowerCase();
1525-
if (id === "local" || id === "global") {
1547+
const raw = /** @type {Token} */ (next).value;
1548+
const isLocal = equalsLowerCase(raw, "local");
1549+
if (isLocal || equalsLowerCase(raw, "global")) {
1550+
const id = isLocal ? "local" : "global";
15261551
// Bare `:local` / `:global`: switch the segment (and top-level persistent) mode and strip the marker. The next sibling token is whitespace when any whitespace separates the marker from the next selector (comments aren't AST nodes); strip the marker plus that whitespace, but only when it's adjacent (a comment between would end the run).
15271552
const afterMarker = values[i + 2];
15281553
const afterIsWhitespace = Boolean(
@@ -1557,10 +1582,13 @@ class CssParser extends Parser {
15571582
continue;
15581583
}
15591584
} else if (next.type === NodeType.Function) {
1560-
const fname = /** @type {FunctionNode} */ (next).name
1561-
.replace(/\\/g, "")
1562-
.toLowerCase();
1563-
if (fname === "local" || fname === "global") {
1585+
const rawName = /** @type {FunctionNode} */ (next).name.replace(
1586+
/\\/g,
1587+
""
1588+
);
1589+
const isLocal = equalsLowerCase(rawName, "local");
1590+
if (isLocal || equalsLowerCase(rawName, "global")) {
1591+
const fname = isLocal ? "local" : "global";
15641592
// `:local(…)` / `:global(…)`: scope mode to the args and strip the wrapper with two source-level strip deps (leading `:name(` + whitespace, then trailing `)` — `:local` also eats whitespace before `)`).
15651593
const fn = /** @type {FunctionNode} */ (next);
15661594
if (isModules) {
@@ -1653,7 +1681,7 @@ class CssParser extends Parser {
16531681
const attrName = unescapeIdentifier(
16541682
source.slice(attrNameNode.start, attrNameNode.end)
16551683
);
1656-
if (attrName.toLowerCase() !== "class") continue;
1684+
if (!equalsLowerCase(attrName, "class")) continue;
16571685
ai++;
16581686
while (
16591687
ai < attrParts.length &&
@@ -1803,9 +1831,7 @@ class CssParser extends Parser {
18031831
const at = /** @type {AtRule & { urlRecovery?: boolean }} */ (
18041832
currentStructural
18051833
);
1806-
return (
1807-
`@${at.name.toLowerCase()}` !== "@import" || Boolean(at.urlRecovery)
1808-
);
1834+
return !equalsLowerCase(at.name, "import") || Boolean(at.urlRecovery);
18091835
}
18101836
return true;
18111837
};
@@ -1969,14 +1995,15 @@ class CssParser extends Parser {
19691995
urlNode = cv;
19701996
continue;
19711997
}
1972-
if (cv.type === NodeType.Function) {
1973-
const fname = /** @type {FunctionNode} */ (cv).name
1974-
.replace(/\\/g, "")
1975-
.toLowerCase();
1976-
if (fname === "url") {
1977-
urlNode = cv;
1978-
continue;
1979-
}
1998+
if (
1999+
cv.type === NodeType.Function &&
2000+
equalsLowerCase(
2001+
/** @type {FunctionNode} */ (cv).name.replace(/\\/g, ""),
2002+
"url"
2003+
)
2004+
) {
2005+
urlNode = cv;
2006+
continue;
19802007
}
19812008
if (cv.type === NodeType.Ident) {
19822009
// CSS Modules: bare ident is a `@value` reference.
@@ -1988,30 +2015,34 @@ class CssParser extends Parser {
19882015

19892016
if (!layerNode && !supportsNode) {
19902017
if (cv.type === NodeType.Ident) {
1991-
const ident = /** @type {Token} */ (cv).value
1992-
.replace(/\\/g, "")
1993-
.toLowerCase();
1994-
if (ident === "layer") {
1995-
layerNode = cv;
1996-
continue;
1997-
}
1998-
} else if (cv.type === NodeType.Function) {
1999-
const fname = /** @type {FunctionNode} */ (cv).name
2000-
.replace(/\\/g, "")
2001-
.toLowerCase();
2002-
if (fname === "layer") {
2018+
if (
2019+
equalsLowerCase(
2020+
/** @type {Token} */ (cv).value.replace(/\\/g, ""),
2021+
"layer"
2022+
)
2023+
) {
20032024
layerNode = cv;
20042025
continue;
20052026
}
2027+
} else if (
2028+
cv.type === NodeType.Function &&
2029+
equalsLowerCase(
2030+
/** @type {FunctionNode} */ (cv).name.replace(/\\/g, ""),
2031+
"layer"
2032+
)
2033+
) {
2034+
layerNode = cv;
2035+
continue;
20062036
}
20072037
}
20082038

20092039
if (
20102040
!supportsNode &&
20112041
cv.type === NodeType.Function &&
2012-
/** @type {FunctionNode} */ (cv).name
2013-
.replace(/\\/g, "")
2014-
.toLowerCase() === "supports"
2042+
equalsLowerCase(
2043+
/** @type {FunctionNode} */ (cv).name.replace(/\\/g, ""),
2044+
"supports"
2045+
)
20152046
) {
20162047
supportsNode = /** @type {FunctionNode} */ (cv);
20172048
continue;
@@ -2398,7 +2429,7 @@ class CssParser extends Parser {
23982429
// `@scope (.x) to (.y)` — walk the prelude as a selector list.
23992430
if (
24002431
isModules &&
2401-
`@${at.name.toLowerCase()}` === "@scope" &&
2432+
equalsLowerCase(at.name, "scope") &&
24022433
at.prelude.length > 0
24032434
) {
24042435
walkSelectorList(
@@ -2467,10 +2498,14 @@ class CssParser extends Parser {
24672498
break;
24682499
}
24692500
if (cv.type === NodeType.Function) {
2470-
const fname = /** @type {FunctionNode} */ (cv).name
2471-
.replace(/\\/g, "")
2472-
.toLowerCase();
2473-
if (fname === "local") pure.markLocal();
2501+
if (
2502+
equalsLowerCase(
2503+
/** @type {FunctionNode} */ (cv).name.replace(/\\/g, ""),
2504+
"local"
2505+
)
2506+
) {
2507+
pure.markLocal();
2508+
}
24742509
break;
24752510
}
24762511
}
@@ -2533,17 +2568,18 @@ class CssParser extends Parser {
25332568
rule.prelude[firstIdx].type === NodeType.Colon
25342569
) {
25352570
const second = rule.prelude[firstIdx + 1];
2536-
const name =
2571+
const rawName =
25372572
second.type === NodeType.Ident
2538-
? /** @type {Token} */ (second).value.toLowerCase()
2573+
? /** @type {Token} */ (second).value
25392574
: second.type === NodeType.Function
2540-
? /** @type {FunctionNode} */ (second).name.toLowerCase()
2575+
? /** @type {FunctionNode} */ (second).name
25412576
: "";
2542-
if (name === "import" || name === "export") {
2577+
const isImport = equalsLowerCase(rawName, "import");
2578+
if (isImport || equalsLowerCase(rawName, "export")) {
25432579
if (topLevel) {
25442580
const startColon = rule.prelude[firstIdx].start;
25452581
const endAfterBody = processImportOrExport(
2546-
name === "import" ? 0 : 1,
2582+
isImport ? 0 : 1,
25472583
second,
25482584
rule
25492585
);
@@ -2721,7 +2757,7 @@ class CssParser extends Parser {
27212757
}
27222758
if (
27232759
cv.type === NodeType.Ident &&
2724-
/** @type {Token} */ (cv).value.toLowerCase() === "global"
2760+
equalsLowerCase(/** @type {Token} */ (cv).value, "global")
27252761
) {
27262762
fromSource = { kind: "global" };
27272763
phase = "done";
@@ -2740,7 +2776,7 @@ class CssParser extends Parser {
27402776
if (cv.type === NodeType.Ident) {
27412777
const identValue = /** @type {Token} */ (cv).value;
27422778
if (
2743-
identValue.toLowerCase() === "from" &&
2779+
equalsLowerCase(identValue, "from") &&
27442780
classNames.length > 0 &&
27452781
nextNonWhitespace(group, i + 1) < group.length
27462782
) {
@@ -2757,8 +2793,10 @@ class CssParser extends Parser {
27572793

27582794
if (cv.type === NodeType.Function) {
27592795
const fn = /** @type {FunctionNode} */ (cv);
2760-
const fname = fn.name.replace(/\\/g, "").toLowerCase();
2761-
const isGlobal = fname === "global";
2796+
const isGlobal = equalsLowerCase(
2797+
fn.name.replace(/\\/g, ""),
2798+
"global"
2799+
);
27622800
for (const inner of fn.value) {
27632801
if (inner.type === NodeType.Ident) {
27642802
classNames.push({
@@ -3321,7 +3359,7 @@ class CssParser extends Parser {
33213359
if (
33223360
j < siblings.length &&
33233361
siblings[j].type === NodeType.Ident &&
3324-
/** @type {Token} */ (siblings[j]).value.toLowerCase() === "from"
3362+
equalsLowerCase(/** @type {Token} */ (siblings[j]).value, "from")
33253363
) {
33263364
const fromIdent = siblings[j];
33273365
const sourceNode = siblings[nextNonWhitespace(siblings, j + 1)];

0 commit comments

Comments
 (0)