-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Performance regression in jsdom@27 #3985
Copy link
Copy link
Open
jsdom/cssstyle
#254Labels
Description
Node.js version
20.19.1
jsdom version
27.1.0
Minimal reproduction case
// package.json
{
"name": "jsdom-regression-repro",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node compare.js"
},
"dependencies": {
"jsdom-26": "npm:jsdom@26.0.0",
"jsdom-27": "npm:jsdom@27.1.0"
}
}// compare.js
/**
* jsdom Performance Regression - Pure DOM Style Operations
*/
async function testJsdomVersion(jsdomPackage, versionLabel) {
// Dynamically import the jsdom version
const { JSDOM } = await import(jsdomPackage);
// Initialize jsdom with empty HTML
const dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {
url: 'http://localhost',
pretendToBeVisual: true,
});
const document = dom.window.document;
// Test 1: innerHTML with simple elements (no styles)
function testInnerHTMLSimple() {
const container = document.createElement('div');
document.body.appendChild(container);
const start = performance.now();
container.innerHTML = '<div>Hello</div>'.repeat(20);
const duration = performance.now() - start;
document.body.removeChild(container);
return duration;
}
// Test 2: innerHTML with inline styles
function testInnerHTMLStyled() {
const container = document.createElement('div');
document.body.appendChild(container);
const start = performance.now();
container.innerHTML =
'<div style="color: blue; background-color: white; padding: 10px; border: 1px solid black;">Cell</div>'.repeat(
20,
);
const duration = performance.now() - start;
document.body.removeChild(container);
return duration;
}
// Test 3: createElement + style property setting
function testCreateElementWithStyles() {
const container = document.createElement('div');
document.body.appendChild(container);
const start = performance.now();
for (let i = 0; i < 20; i++) {
const div = document.createElement('div');
div.style.color = 'blue';
div.style.backgroundColor = 'white';
div.style.padding = '10px';
div.style.border = '1px solid black';
div.textContent = `Cell ${i}`;
container.appendChild(div);
}
const duration = performance.now() - start;
document.body.removeChild(container);
return duration;
}
// Test 4: createElement + setAttribute('style')
function testCreateElementWithStyleAttribute() {
const container = document.createElement('div');
document.body.appendChild(container);
const start = performance.now();
for (let i = 0; i < 20; i++) {
const div = document.createElement('div');
div.setAttribute(
'style',
'color: blue; background-color: white; padding: 10px; border: 1px solid black;',
);
div.textContent = `Cell ${i}`;
container.appendChild(div);
}
const duration = performance.now() - start;
document.body.removeChild(container);
return duration;
}
// Test 5: createElement + style.cssText
function testCreateElementWithCssText() {
const container = document.createElement('div');
document.body.appendChild(container);
const start = performance.now();
for (let i = 0; i < 20; i++) {
const div = document.createElement('div');
div.style.cssText =
'color: blue; background-color: white; padding: 10px; border: 1px solid black;';
div.textContent = `Cell ${i}`;
container.appendChild(div);
}
const duration = performance.now() - start;
document.body.removeChild(container);
return duration;
}
// Warmup runs
for (let i = 0; i < 5; i++) {
testInnerHTMLSimple();
testInnerHTMLStyled();
testCreateElementWithStyles();
testCreateElementWithStyleAttribute();
testCreateElementWithCssText();
}
// Run tests multiple times
const iterations = 10;
const results = {
innerHTMLSimple: [],
innerHTMLStyled: [],
createElementWithStyles: [],
createElementWithStyleAttribute: [],
createElementWithCssText: [],
};
for (let i = 0; i < iterations; i++) {
results.innerHTMLSimple.push(testInnerHTMLSimple());
results.innerHTMLStyled.push(testInnerHTMLStyled());
results.createElementWithStyles.push(testCreateElementWithStyles());
results.createElementWithStyleAttribute.push(testCreateElementWithStyleAttribute());
results.createElementWithCssText.push(testCreateElementWithCssText());
}
// Calculate averages
const avg = {
innerHTMLSimple: results.innerHTMLSimple.reduce((a, b) => a + b, 0) / iterations,
innerHTMLStyled: results.innerHTMLStyled.reduce((a, b) => a + b, 0) / iterations,
createElementWithStyles:
results.createElementWithStyles.reduce((a, b) => a + b, 0) / iterations,
createElementWithStyleAttribute:
results.createElementWithStyleAttribute.reduce((a, b) => a + b, 0) / iterations,
createElementWithCssText:
results.createElementWithCssText.reduce((a, b) => a + b, 0) / iterations,
};
return {
versionLabel,
avg,
};
}
// Run tests for both versions
console.log('jsdom Performance Regression - Pure DOM Style Operations');
console.log('='.repeat(80));
console.log('Test: Raw DOM operations with inline styles (no React, no libraries)');
console.log('='.repeat(80));
console.log('');
console.log('Testing jsdom 26.0.0...');
const results26 = await testJsdomVersion('jsdom-26', 'jsdom 26.0.0');
console.log('Testing jsdom 27.1.0...');
const results27 = await testJsdomVersion('jsdom-27', 'jsdom 27.1.0');
console.log('');
console.log('='.repeat(80));
console.log('RESULTS');
console.log('='.repeat(80));
console.log('');
function printResults(results) {
console.log(`${results.versionLabel}:`);
console.log(
` innerHTML (20 simple divs, no styles): ${results.avg.innerHTMLSimple.toFixed(3)}ms`,
);
console.log(
` innerHTML (20 divs with inline styles): ${results.avg.innerHTMLStyled.toFixed(3)}ms`,
);
console.log(
` createElement + style.property (20 divs): ${results.avg.createElementWithStyles.toFixed(
3,
)}ms`,
);
console.log(
` createElement + style.cssText (20 divs): ${results.avg.createElementWithCssText.toFixed(
3,
)}ms`,
);
console.log(
` createElement + setAttribute (20 divs): ${results.avg.createElementWithStyleAttribute.toFixed(
3,
)}ms`,
);
console.log('');
}
printResults(results26);
printResults(results27);
// Calculate regressions
console.log('='.repeat(80));
console.log('REGRESSION ANALYSIS');
console.log('='.repeat(80));
console.log('');
function analyzeRegression(label, baseline, regression) {
const diff = regression - baseline;
const percent = (diff / baseline) * 100;
const multiplier = regression / baseline;
console.log(`${label}:`);
console.log(` Difference: ${diff > 0 ? '+' : ''}${diff.toFixed(3)}ms`);
console.log(` Change: ${percent > 0 ? '+' : ''}${percent.toFixed(1)}%`);
console.log(` Multiplier: ${multiplier.toFixed(2)}x`);
console.log('');
return percent;
}
analyzeRegression(
'innerHTML (simple, no styles)',
results26.avg.innerHTMLSimple,
results27.avg.innerHTMLSimple,
);
analyzeRegression(
'innerHTML (with inline styles)',
results26.avg.innerHTMLStyled,
results27.avg.innerHTMLStyled,
);
analyzeRegression(
'createElement + style.property',
results26.avg.createElementWithStyles,
results27.avg.createElementWithStyles,
);
analyzeRegression(
'createElement + style.cssText',
results26.avg.createElementWithCssText,
results27.avg.createElementWithCssText,
);
analyzeRegression(
'createElement + setAttribute',
results26.avg.createElementWithStyleAttribute,
results27.avg.createElementWithStyleAttribute,
);See the following StackBlitz
How does similar code behave in browsers?
Didn't notice any performance regressions in browsers recently
What is the problem?
At MUI we recently upgraded JSDOM from 26 to 27 and noticed the runtime of our tests doubled. Part of the problem is slow dom accessors in @testing-library/dom. Another part of it happens in the render phase of our tests. I could narrow it down to @testing-library/react render function, the same slowness was noticable with straight react-dom, we noticed recent changes and quickly assumed it was styling related, then arrived at this reproduction.
Output of the script on my local machine
jsdom Performance Regression - Pure DOM Style Operations
================================================================================
Test: Raw DOM operations with inline styles (no React, no libraries)
================================================================================
Testing jsdom 26.0.0...
Testing jsdom 27.1.0...
================================================================================
RESULTS
================================================================================
jsdom 26.0.0:
innerHTML (20 simple divs, no styles): 0.426ms
innerHTML (20 divs with inline styles): 1.249ms
createElement + style.property (20 divs): 1.053ms
createElement + style.cssText (20 divs): 0.999ms
createElement + setAttribute (20 divs): 0.962ms
jsdom 27.1.0:
innerHTML (20 simple divs, no styles): 0.358ms
innerHTML (20 divs with inline styles): 44.727ms
createElement + style.property (20 divs): 6.114ms
createElement + style.cssText (20 divs): 43.818ms
createElement + setAttribute (20 divs): 44.261ms
================================================================================
REGRESSION ANALYSIS
================================================================================
innerHTML (simple, no styles):
Difference: -0.067ms
Change: -15.8%
Multiplier: 0.84x
innerHTML (with inline styles):
Difference: +43.478ms
Change: +3480.9%
Multiplier: 35.81x
createElement + style.property:
Difference: +5.061ms
Change: +480.5%
Multiplier: 5.81x
createElement + style.cssText:
Difference: +42.819ms
Change: +4286.4%
Multiplier: 43.86x
createElement + setAttribute:
Difference: +43.299ms
Change: +4502.7%
Multiplier: 46.03x
Reactions are currently unavailable