Skip to content

Commit 5cd65ec

Browse files
perf(css,html): reduce syntax parser overhead
1 parent f9817c9 commit 5cd65ec

4 files changed

Lines changed: 72 additions & 31 deletions

File tree

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+
Reduce CSS and HTML syntax processing time and memory use.

‎lib/css/syntax.js‎

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,8 +1155,9 @@ const _identNamesUrl = (input, start, end) => {
11551155
function consumeAnIdentLikeToken(input, pos, out) {
11561156
const start = pos;
11571157
pos = _consumeAnIdentSequence(input, pos);
1158+
const nextCodePoint = input.charCodeAt(pos);
11581159
if (
1159-
input.charCodeAt(pos) === CC_LEFT_PARENTHESIS &&
1160+
nextCodePoint === CC_LEFT_PARENTHESIS &&
11601161
_identNamesUrl(input, start, pos)
11611162
) {
11621163
pos++;
@@ -1182,7 +1183,7 @@ function consumeAnIdentLikeToken(input, pos, out) {
11821183
}
11831184
return consumeAUrlToken(input, pos, start, out);
11841185
}
1185-
if (input.charCodeAt(pos) === CC_LEFT_PARENTHESIS) {
1186+
if (nextCodePoint === CC_LEFT_PARENTHESIS) {
11861187
pos++;
11871188
return fill(out, TT_FUNCTION, start, pos);
11881189
}
@@ -2866,7 +2867,7 @@ const parseACommaSeparatedListOfComponentValues = (
28662867
/** @type {Node[]} */ (consumeAListOfComponentValues(ts, TT_COMMA))
28672868
);
28682869
// 3.2 Discard a token from input.
2869-
ts.discard();
2870+
if (ts.next().type !== TT_EOF) ts.advance();
28702871
}
28712872
// 4. Return groups — wrap each group's refs against the retained snapshot.
28722873
const store = _finishStore();
@@ -2904,7 +2905,7 @@ const consumeAStylesheetsContents = (ts, onRule) => {
29042905
// <whitespace-token> / <CDO-token> / <CDC-token>
29052906
// Discard a token from input.
29062907
if (t.type === TT_WHITESPACE || t.type === TT_CDO || t.type === TT_CDC) {
2907-
ts.discard();
2908+
ts.advance();
29082909
}
29092910
// <EOF-token>
29102911
// Return rules.
@@ -2941,7 +2942,8 @@ const consumeAStylesheetsContents = (ts, onRule) => {
29412942
const consumeAnAtRule = (ts, nested = false) => {
29422943
// Assert (spec): the next token is an <at-keyword-token>.
29432944
// Consume a token from input, and let rule be a new at-rule with its name set to the returned token’s value, its prelude initially set to an empty list, and no declarations or child rules.
2944-
const head = ts.consume();
2945+
const head = ts.next();
2946+
ts.advance();
29452947
const rule = /** @type {AtRule} */ (
29462948
_makeContainer(T_AT_RULE, head.start, head.end)
29472949
);
@@ -2965,7 +2967,7 @@ const consumeAnAtRule = (ts, nested = false) => {
29652967
// <EOF-token>
29662968
// Discard a token from input. If rule is valid in the current context, return it; otherwise return nothing.
29672969
if (t.type === TT_SEMICOLON || t.type === TT_EOF) {
2968-
ts.discard();
2970+
if (t.type !== TT_EOF) ts.advance();
29692971
_setPrelude(rule, prelude);
29702972
_setBlock(rule, -1);
29712973
_setEnd(rule, t.start);
@@ -2981,7 +2983,7 @@ const consumeAnAtRule = (ts, nested = false) => {
29812983
_setEnd(rule, t.start);
29822984
return rule;
29832985
}
2984-
const node = consumeATokenAsNode(ts);
2986+
const node = consumeATokenAsNode(ts, t);
29852987
if (!skip) prelude.push(node);
29862988
continue;
29872989
}
@@ -3021,11 +3023,12 @@ const consumeAnAtRule = (ts, nested = false) => {
30213023
* §5.4.2 / §5.4.3), distinct from `consumeAComponentValue` which would recurse
30223024
* into a simple block / function.
30233025
* @param {TokenStream} ts token stream
3026+
* @param {MutableToken} token the already-peeked token
30243027
* @returns {Token} the consumed token as a leaf node
30253028
*/
3026-
const consumeATokenAsNode = (ts) => {
3027-
const t = ts.consume();
3028-
return /** @type {Token} */ (tokenToNode(t));
3029+
const consumeATokenAsNode = (ts, token) => {
3030+
ts.advance();
3031+
return /** @type {Token} */ (tokenToNode(token));
30293032
};
30303033

30313034
/**
@@ -3088,7 +3091,7 @@ const consumeAQualifiedRule = (ts, stopToken, nested = false) => {
30883091
}
30893092
ts.advance();
30903093
} else {
3091-
(prelude || (prelude = _takeList())).push(consumeATokenAsNode(ts));
3094+
(prelude || (prelude = _takeList())).push(consumeATokenAsNode(ts, t));
30923095
}
30933096
continue;
30943097
}
@@ -3208,14 +3211,14 @@ const consumeABlock = (ts) => {
32083211
const blockStart = ts.next().start;
32093212
// Assert (spec): the next token is <{-token>.
32103213
// Discard a token from input. Consume a block's contents from input and let result be the result. Discard a token from input.
3211-
ts.discard();
3214+
ts.advance();
32123215
consumeABlocksContentsInto(ts);
32133216
// Read before anything else runs: a nested block would overwrite these.
32143217
const decls = _bcDecls;
32153218
const rules = _bcRules;
32163219
const close = ts.next();
32173220
const end = close.type === TT_RIGHT_CURLY_BRACKET ? close.end : close.start;
3218-
ts.discard();
3221+
if (close.type !== TT_EOF) ts.advance();
32193222
_blockDecls = decls;
32203223
_blockRules = rules;
32213224
_blockStart = blockStart;
@@ -3485,14 +3488,14 @@ const consumeTheRemnantsOfABadDeclaration = (ts, nested) => {
34853488
// <eof-token> / <semicolon-token>
34863489
// Discard a token from input, and return.
34873490
if (t.type === TT_EOF || t.type === TT_SEMICOLON) {
3488-
ts.discard();
3491+
if (t.type !== TT_EOF) ts.advance();
34893492
return;
34903493
}
34913494
// <}-token>
34923495
// If nested is true, return. Otherwise, discard a token.
34933496
if (t.type === TT_RIGHT_CURLY_BRACKET) {
34943497
if (nested) return;
3495-
ts.discard();
3498+
ts.advance();
34963499
continue;
34973500
}
34983501
// anything else
@@ -3510,7 +3513,8 @@ const consumeTheRemnantsOfABadDeclaration = (ts, nested) => {
35103513
const consumeADeclaration = (ts, nested = false) => {
35113514
const { input } = ts;
35123515
// Let decl be a new declaration, with an initially empty name and a value set to an empty list.
3513-
const start = ts.next().start;
3516+
const head = ts.next();
3517+
const start = head.start;
35143518
// nameEnd (= start) / important (unset) keep their container defaults;
35153519
// `value` is set unconditionally at step 5 below.
35163520
const decl = /** @type {Declaration} */ (
@@ -3519,8 +3523,8 @@ const consumeADeclaration = (ts, nested = false) => {
35193523

35203524
// 1. If the next token is an <ident-token>, consume a token from input and set decl's name to the returned token's value.
35213525
// Otherwise, consume the remnants of a bad declaration from input, with nested, and return nothing.
3522-
if (ts.next().type === TT_IDENTIFIER) {
3523-
const head = ts.consume();
3526+
if (head.type === TT_IDENTIFIER) {
3527+
ts.advance();
35243528
_setName(decl, head.start, head.end);
35253529
} else {
35263530
consumeTheRemnantsOfABadDeclaration(ts, nested);
@@ -3648,7 +3652,7 @@ const consumeAListOfComponentValues = (
36483652
// Otherwise, this is a parse error. Consume a token from input and append the result to values.
36493653
if (t.type === TT_RIGHT_CURLY_BRACKET) {
36503654
if (nested) return values;
3651-
const closer = consumeATokenAsNode(ts);
3655+
const closer = consumeATokenAsNode(ts, t);
36523656
// Keep unless the type is explicitly marked skip (1); an out-of-range
36533657
// lookup on a short `skip.types` yields `undefined`, which must not drop.
36543658
if (!_skipActive || _skipTypes[_nodeTypeOf(closer)] !== 1) {
@@ -3740,7 +3744,7 @@ const consumeASimpleBlock = (ts) => {
37403744
const val = _takeList();
37413745

37423746
// Discard a token from input.
3743-
ts.discard();
3747+
ts.advance();
37443748

37453749
// Process input
37463750
for (;;) {
@@ -3750,7 +3754,7 @@ const consumeASimpleBlock = (ts) => {
37503754
// ending token
37513755
// Discard a token from input. Return block.
37523756
if (t.type === TT_EOF || t.type === ending) {
3753-
ts.discard();
3757+
if (t.type !== TT_EOF) ts.advance();
37543758
_setValue(block, val);
37553759
_setEnd(block, t.end);
37563760
return block;
@@ -3770,7 +3774,8 @@ const consumeASimpleBlock = (ts) => {
37703774
const consumeAFunction = (ts) => {
37713775
// Assert (spec): the next token is a <function-token>.
37723776
// Consume a token from input, and let function be a new function with its name equal the returned token’s value, and a value set to an empty list.
3773-
const tFn = ts.consume();
3777+
const tFn = ts.next();
3778+
ts.advance();
37743779
const fn = /** @type {FunctionNode} */ (
37753780
_makeContainer(T_FUNCTION, tFn.start, tFn.end)
37763781
);
@@ -3786,7 +3791,7 @@ const consumeAFunction = (ts) => {
37863791
// <eof-token>
37873792
// <)-token>
37883793
// Discard a token from input. Return function.
3789-
ts.discard();
3794+
if (t.type !== TT_EOF) ts.advance();
37903795
_setValue(fn, val);
37913796
_setEnd(fn, t.end);
37923797
return fn;
@@ -5309,14 +5314,14 @@ const _streamOnNode = (node) => {
53095314
const _streamConsumeBlock = (ts, rule) => {
53105315
const d = _depth;
53115316
const blockStart = ts.next().start;
5312-
ts.discard();
5317+
ts.advance();
53135318
if (d >= _STREAM_MAX_DEPTH) {
53145319
// Deeper than the frame table: fall back to the materializing path.
53155320
consumeABlocksContentsInto(ts);
53165321
const decls = _bcDecls;
53175322
const rules = _bcRules;
53185323
const c = ts.next();
5319-
ts.discard();
5324+
if (c.type !== TT_EOF) ts.advance();
53205325
_setBody(rule, decls, rules);
53215326
_setBlock(rule, blockStart);
53225327
_setEnd(rule, c.type === TT_RIGHT_CURLY_BRACKET ? c.end : c.start);
@@ -5334,7 +5339,7 @@ const _streamConsumeBlock = (ts, rule) => {
53345339
const rules = _bcRules;
53355340
const close = ts.next();
53365341
const end = close.type === TT_RIGHT_CURLY_BRACKET ? close.end : close.start;
5337-
ts.discard();
5342+
if (close.type !== TT_EOF) ts.advance();
53385343
_setBlock(rule, blockStart);
53395344
_setEnd(rule, end);
53405345
if (!streamed) {

‎lib/html/syntax.js‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5164,21 +5164,22 @@ const _resetAstColumns = () => {
51645164
// tens of MB at module level for the process lifetime (~46 bytes/node,
51655165
// ~17 bytes/attribute of capacity across the columns).
51665166
const _COLUMN_SHRINK_CAPACITY = 65536;
5167+
const _SPARSE_MARKUP_LIMIT = 512;
51675168

51685169
// Release the side arrays' heap references (strings, attribute names/values)
51695170
// once a walk has consumed the tree, so the retained columns don't pin the
51705171
// parsed source until the next parse.
51715172
const _releaseAstColumns = () => {
51725173
// Kept regime: overwrite the used prefix in place (see `_resetAstColumns`);
51735174
// oversized arrays are dropped entirely, mirroring the typed columns below.
5174-
if (_nodeStrings.length > _COLUMN_SHRINK_CAPACITY) {
5175+
if (_nodeStrings.length >= _COLUMN_SHRINK_CAPACITY) {
51755176
_nodeStrings.length = 0;
51765177
} else {
51775178
const written = _nodeCount > _nodeHighWater ? _nodeCount : _nodeHighWater;
51785179
const usedNodes = Math.min(written, _nodeStrings.length - 1);
51795180
for (let i = 0; i <= usedNodes; i++) _nodeStrings[i] = "";
51805181
}
5181-
if (_attrNames.length > _COLUMN_SHRINK_CAPACITY) {
5182+
if (_attrNames.length >= _COLUMN_SHRINK_CAPACITY) {
51825183
_attrNames.length = 0;
51835184
_attrValues.length = 0;
51845185
} else {
@@ -5203,7 +5204,7 @@ const _releaseAstColumns = () => {
52035204
_htmlSource = "";
52045205
_doctypePublicId = null;
52055206
_doctypeSystemId = null;
5206-
if (_nodeCapacity > _COLUMN_SHRINK_CAPACITY) {
5207+
if (_nodeCapacity >= _COLUMN_SHRINK_CAPACITY) {
52075208
_nodeCapacity = 0;
52085209
_nodeTypes = new Uint8Array(0);
52095210
_nodeFlags = new Uint8Array(0);
@@ -5219,7 +5220,7 @@ const _releaseAstColumns = () => {
52195220
_nodeAttrStarts = new Int32Array(0);
52205221
_nodeAttrCounts = new Int32Array(0);
52215222
}
5222-
if (_attrCapacity > _COLUMN_SHRINK_CAPACITY) {
5223+
if (_attrCapacity >= _COLUMN_SHRINK_CAPACITY) {
52235224
_attrCapacity = 0;
52245225
_attrNameStarts = new Int32Array(0);
52255226
_attrNameEnds = new Int32Array(0);
@@ -8753,7 +8754,24 @@ const parseHtml = (input, pos = 0, options = {}) => {
87538754
// its node estimate uses `len/24`; attributes are unaffected by the skip and
87548755
// keep `len/48`.
87558756
let nodeEstimate = (input.length / (skipText ? 24 : 12)) | 0;
8756-
const attrEstimate = (input.length / 48) | 0;
8757+
let attrEstimate = (input.length / 48) | 0;
8758+
// A long inline style/script or comment has very few markup delimiters, so
8759+
// estimating columns from its byte length wastes memory. Probe only until the
8760+
// document proves dense; `indexOf` skips the long raw-text spans natively.
8761+
if (nodeEstimate > _COLUMN_SHRINK_CAPACITY) {
8762+
let markupCount = 0;
8763+
let searchFrom = 0;
8764+
while (markupCount <= _SPARSE_MARKUP_LIMIT) {
8765+
const next = input.indexOf("<", searchFrom);
8766+
if (next < 0) break;
8767+
markupCount++;
8768+
searchFrom = next + 1;
8769+
}
8770+
if (markupCount <= _SPARSE_MARKUP_LIMIT) {
8771+
nodeEstimate = Math.min(nodeEstimate, markupCount * 2 + 16);
8772+
attrEstimate = Math.min(attrEstimate, markupCount * 8 + 16);
8773+
}
8774+
}
87578775
if (_streamEligible) {
87588776
// Node ids are recycled once streaming starts, so a document past the
87598777
// threshold never needs the whole estimate — one allocation covering the

‎test/unitCases/HtmlSyntax.unittest.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6939,6 +6939,19 @@ describe("parseHtml — tree-construction edge cases (SoA columns)", () => {
69396939
expect(small[0].attributes[0].value).toBe("c");
69406940
});
69416941

6942+
it("handles a large structurally sparse document", () => {
6943+
// Exercise both byte-length estimate caps without needing enough nodes or
6944+
// attributes to fill the speculative columns.
6945+
const nodes = body(`<!--${"x".repeat(3200000)}--><p>x</p>`);
6946+
expect(nodes).toHaveLength(1);
6947+
expect(nodes[0]).toEqual(
6948+
expect.objectContaining({ tagName: "p", children: [expect.any(Object)] })
6949+
);
6950+
expect(body("<b>y</b>")[0]).toEqual(
6951+
expect.objectContaining({ tagName: "b" })
6952+
);
6953+
});
6954+
69426955
it("merges texts left adjacent by a skipped comment", () => {
69436956
const nodes = bodyOf("a<!--c-->b", { comments: true });
69446957
expect(nodes).toEqual([

0 commit comments

Comments
 (0)