-
Notifications
You must be signed in to change notification settings - Fork 17
Description
Summary
When DOMPurify is used through isomorphic-dompurify in a long‑running Node.js process, repeated calls to DOMPurify.sanitize(...) become progressively slower and heap usage grows without bound. The root cause appears to be that isomorphic-dompurify creates a single jsdom window, and DOMPurify keeps appending to that shared DOM state; nothing ever gets reclaimed. After a few thousand sanitizations, each sanitize call is an order of magnitude slower and heap usage climbs into the hundreds of MBs.
Environment
- Node.js v24.6.0 (also reproduced on v20)
- dompurify 3.3.0
- isomorphic-dompurify 2.31.0
- jsdom 27.1.0
Minimal reproduction
import { performance } from 'node:perf_hooks';
import DOMPurify from 'isomorphic-dompurify';
const html = '<p>Hello</p>';
for (let i = 1; ; i += 1) {
const start = performance.now();
DOMPurify.sanitize(html);
if (i % 1000 === 0) {
const durationMs = performance.now() - start;
const heapMb = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`${i} calls -> ${durationMs.toFixed(3)} ms, heap ${heapMb.toFixed(1)} MB`);
}
}Running this script produces steadily increasing latencies and heap usage:
1000 calls -> 0.320 ms, heap 87.5 MB
2000 calls -> 0.532 ms, heap 144.5 MB
3000 calls -> 0.911 ms, heap 187.0 MB
4000 calls -> 1.238 ms, heap 243.7 MB
5000 calls -> 1.754 ms, heap 278.5 MB
6000 calls -> 2.399 ms, heap 330.3 MB
7000 calls -> 3.373 ms, heap 383.3 MB
8000 calls -> 3.970 ms, heap 451.9 MB
9000 calls -> 4.847 ms, heap 476.3 MBThe growth continues until the process is manually killed.
Workaround
If I create a new jsdom window/DOMPurify instance for each sanitize and close it afterwards, the leak disappears and the per-call time stays flat:
import { performance } from 'node:perf_hooks';
import createDOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
const html = '<p>Hello</p>';
for (let i = 1; i <= 1000; i += 1) {
const start = performance.now();
const { window } = new JSDOM('<!DOCTYPE html>');
const DOMPurify = createDOMPurify(window);
DOMPurify.sanitize(html);
window.close();
if (i % 100 === 0) {
console.log(`${i} calls -> ${(performance.now() - start).toFixed(3)} ms`);
}
}100 calls -> 2.425 ms
200 calls -> 2.157 ms
300 calls -> 1.654 ms
400 calls -> 1.715 ms
500 calls -> 1.620 ms
600 calls -> 2.438 ms
700 calls -> 1.517 ms
800 calls -> 9.568 ms
900 calls -> 1.477 ms
1000 calls -> 1.476 ms
Request
Could the isomorphic-dompurify maintainers confirm the leak and either (1) clear the jsdom document between sanitizations, (2) stop reusing a shared window in Node, or (3) document that the module isn’t safe for long-lived server processes? I'd be more than happy to help test potential fixes.
Disclosure:
GPT-5-Codexhelped author this issue text and the benchmarks. I tweaked both the text and the scripts for clarity.