Fix performance of splitCssText (#1615)

- Fix bug where the right split point was not being picked for the 3rd section onwards
- Fix that it wasn't able to find a split when both halves were identical
- Add test to put splitCssText through it's paces with a large file
- Introduce a limit on the iteration which causes the 'efficiently' test to fail
- Fix poor 'crawling' performance in the 'matching' algorithm for large css texts - e.g. for a (doubled) benchmark.css, we were running `normalizeCssText` 9480 times before `k` got to the right place
- Further algorithm efficiency: need to take larger jumps; use the scaling factor to make better guess at how big a jump to make
This commit is contained in:
Eoghan Murray
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 4c8dfb0440
commit 65eb7c80aa
5 changed files with 121 additions and 14 deletions

View File

@@ -456,8 +456,9 @@ export function normalizeCssString(cssText: string): string {
/**
* Maps the output of stringifyStylesheet to individual text nodes of a <style> element
* performance is not considered as this is anticipated to be very much an edge case
* (javascript is needed to add extra text nodes to a <style>)
* which occurs when javascript is used to append to the style element
* and may also occur when browsers opt to break up large text nodes
* performance needs to be considered, see e.g. #1603
*/
export function splitCssText(
cssText: string,
@@ -465,27 +466,69 @@ export function splitCssText(
): string[] {
const childNodes = Array.from(style.childNodes);
const splits: string[] = [];
let iterLimit = 0;
if (childNodes.length > 1 && cssText && typeof cssText === 'string') {
const cssTextNorm = normalizeCssString(cssText);
let cssTextNorm = normalizeCssString(cssText);
const normFactor = cssTextNorm.length / cssText.length;
for (let i = 1; i < childNodes.length; i++) {
if (
childNodes[i].textContent &&
typeof childNodes[i].textContent === 'string'
) {
const textContentNorm = normalizeCssString(childNodes[i].textContent!);
for (let j = 3; j < textContentNorm.length; j++) {
// find a substring that appears only once
let j = 3;
for (; j < textContentNorm.length; j++) {
if (
// keep consuming css identifiers (to get a decent chunk more quickly)
textContentNorm[j].match(/[a-zA-Z0-9]/) ||
// substring needs to be unique to this section
textContentNorm.indexOf(textContentNorm.substring(0, j), 1) !== -1
) {
continue;
}
break;
}
for (; j < textContentNorm.length; j++) {
const bit = textContentNorm.substring(0, j);
if (cssTextNorm.split(bit).length === 2) {
const splitNorm = cssTextNorm.indexOf(bit);
// this substring should appears only once in overall text too
const bits = cssTextNorm.split(bit);
let splitNorm = -1;
if (bits.length === 2) {
splitNorm = cssTextNorm.indexOf(bit);
} else if (
bits.length > 2 &&
bits[0] === '' &&
childNodes[i - 1].textContent !== ''
) {
// this childNode has same starting content as previous
splitNorm = cssTextNorm.indexOf(bit, 1);
}
if (splitNorm !== -1) {
// find the split point in the original text
for (let k = splitNorm; k < cssText.length; k++) {
if (
normalizeCssString(cssText.substring(0, k)).length === splitNorm
) {
let k = Math.floor(splitNorm / normFactor);
for (; k > 0 && k < cssText.length; ) {
iterLimit += 1;
if (iterLimit > 50 * childNodes.length) {
// quit for performance purposes
splits.push(cssText);
return splits;
}
const normPart = normalizeCssString(cssText.substring(0, k));
if (normPart.length === splitNorm) {
splits.push(cssText.substring(0, k));
cssText = cssText.substring(k);
cssTextNorm = cssTextNorm.substring(splitNorm);
break;
} else if (normPart.length < splitNorm) {
k += Math.max(
1,
Math.floor((splitNorm - normPart.length) / normFactor),
);
} else {
k -= Math.max(
1,
Math.floor((normPart.length - splitNorm) * normFactor),
);
}
}
break;