jsdom and parse5 are both foundational tools for working with HTML in JavaScript outside the browser, but they serve different layers of the stack. jsdom provides a full-featured, standards-compliant implementation of the browser's Document Object Model (DOM) and related web APIs, enabling you to run browser-like code in Node.js. It includes its own HTML parser but delegates parsing to parse5 under the hood. parse5, by contrast, is a low-level, spec-compliant HTML parser that focuses exclusively on converting HTML strings into structured syntax trees (like ASTs or DOM-like trees) without implementing browser APIs like document.querySelector() or event handling. While jsdom gives you a simulated browser environment, parse5 gives you precise control over the parsing process itself.
Both jsdom and parse5 let you work with HTML in Node.js, but they operate at very different levels of abstraction. Understanding where each fits in the toolchain is key to making the right architectural choice.
jsdom aims to replicate the browser’s DOM environment as closely as possible. It implements web standards like Document, Element, EventTarget, XMLHttpRequest, and more — so your frontend code can run unchanged in Node.js.
// jsdom: Full DOM API available
import { JSDOM } from 'jsdom';
const dom = new JSDOM(`<div id="app"><p>Hello</p></div>`);
const doc = dom.window.document;
const el = doc.querySelector('#app');
el.innerHTML = '<span>Updated</span>';
console.log(el.outerHTML); // <div id="app"><span>Updated</span></div>
parse5 does one thing well: parse HTML according to the WHATWG specification and output structured representations (like an AST or a tree format). It doesn’t provide DOM methods or simulate browser behavior.
// parse5: Only parsing/serialization
import * as parse5 from 'parse5';
const html = `<div id="app"><p>Hello</p></div>`;
const ast = parse5.parse(html);
// AST is a plain object tree — no querySelector, no innerHTML
console.log(ast.childNodes[0].tagName); // 'div'
// To modify, you must traverse and mutate the tree manually
const serialized = parse5.serialize(ast);
Interestingly, jsdom uses parse5 internally for HTML parsing. This means parse5 is the lower-level engine that powers jsdom’s HTML ingestion. If you’re using jsdom, you’re already indirectly relying on parse5.
However, jsdom adds significant layers on top:
window and global scope emulationThis makes jsdom much heavier, but far more convenient for browser-like workflows.
You’re testing a utility that calls document.getElementById() or attaches event listeners.
jsdom: It provides the document and window globals your code expects.// In a Jest test (which uses jsdom by default)
document.body.innerHTML = '<button id="btn">Click</button>';
const btn = document.getElementById('btn');
btn.click(); // Works because jsdom simulates events
parse5: It won’t give you a document object or event system.You need to parse HTML, inspect tag nesting, validate attributes, or pretty-print output.
parse5: Its AST format is designed for programmatic traversal and mutation.import * as parse5 from 'parse5';
function findEmptyDivs(node) {
if (node.nodeName === 'div' && !node.childNodes?.length) {
console.log('Empty div found');
}
node.childNodes?.forEach(findEmptyDivs);
}
const ast = parse5.parse('<div></div><p>Text</p>');
findEmptyDivs(ast);
jsdom: Overkill for static analysis; harder to traverse the internal tree structure.parse5 gives you multiple tree formats and serialization options:
parse5.parse() → returns a “tree adapter”-based AST (default)parse5.parseFragment() → parse partial HTML (e.g., inside a <div>)// Parse a fragment (no <html> wrapper)
const fragment = parse5.parseFragment('<li>Item 1</li><li>Item 2</li>');
const html = parse5.serialize(fragment);
jsdom always produces a full document (with <html>, <head>, <body>) unless you explicitly create a fragment:
// jsdom fragment creation
const dom = new JSDOM();
const frag = dom.window.document.createRange().createContextualFragment('<li>A</li><li>B</li>');
But you can’t easily access the raw parse tree — you’re locked into the DOM API.
Both libraries follow the WHATWG HTML spec closely, but their error reporting differs:
parse5 throws clear parsing errors and allows fine control over error tolerance via options.jsdom suppresses many parse errors to mimic browser behavior (browsers never crash on malformed HTML).If you need to detect invalid HTML during a build step, parse5 is more transparent.
parse5 to Preprocess HTML Before jsdomSometimes you want to clean or transform HTML before handing it to jsdom:
import * as parse5 from 'parse5';
import { JSDOM } from 'jsdom';
// Step 1: Parse and sanitize with parse5
const ast = parse5.parse(dirtyHtml);
sanitizeAst(ast); // custom function
const cleanHtml = parse5.serialize(ast);
// Step 2: Load into jsdom for DOM manipulation
const dom = new JSDOM(cleanHtml);
// ... now use document.querySelector(), etc.
This hybrid approach leverages the strengths of both.
| Feature | jsdom | parse5 |
|---|---|---|
| Primary Role | Simulated browser environment | HTML parser/serializer |
Provides document? | ✅ Yes | ❌ No |
| Event Simulation | ✅ Yes | ❌ No |
| AST Access | ❌ Hidden behind DOM API | ✅ Direct tree manipulation |
| Fragment Parsing | Possible, but indirect | ✅ Built-in parseFragment() |
| Use in Testing | ✅ Ideal for DOM-dependent tests | ❌ Not suitable |
| Use in Build Tools | ❌ Heavy, slow | ✅ Lightweight, fast |
Reach for jsdom when your code thinks it’s in a browser. This includes most frontend test suites, SSR debugging, or any logic that uses native DOM methods.
Reach for parse5 when you’re processing HTML as data — transforming, analyzing, or generating it programmatically without needing browser APIs.
In practice, many large projects use both: parse5 in build pipelines for speed and control, and jsdom in test environments for realism. Knowing the boundary between parsing and DOM simulation helps you pick the right tool — and avoid bloating your bundle with unnecessary browser emulation.
Choose parse5 when you only need to parse or serialize HTML accurately and efficiently, without the overhead of a full DOM implementation. It’s ideal for static analysis, HTML transformation pipelines, custom renderers, or building higher-level tools (like jsdom itself). If you’re writing a linter, formatter, or compiler that works directly with HTML syntax trees, parse5 gives you fine-grained control and better performance.
Choose jsdom when you need a realistic browser-like environment in Node.js — for example, to run unit tests that rely on DOM APIs (document, window, querySelector, events), to scrape pages that require JavaScript execution simulation, or to manipulate HTML using familiar web platform methods. It’s the right tool when your code assumes it’s running in a browser, even if it isn’t.
npm install --save parse5
📖 Documentation 📖