mrkeyoor.com_
Wed 23 Sept 04:18 UTC
npmUtilsupdated 23 Sept 2026

node-html-parser review

`node-html-parser` turns an HTML string into a small mutable tree with CSS selectors, attributes, classes, parent and sibling navigation, text extraction, source ranges, and serialization. It intentionally implements a simplified DOM and prioritizes parse speed over browser-perfect recovery of broken markup. Version 9.0 changed the packaging build, and 9.0.1 switched declaration generation back to TypeScript after the new bundler produced bad types. Our Node 22 checks loaded its CommonJS and ESM entries, but a full browser import was 108.4 KB minified and 47.3 KB gzipped. Use it for trusted extraction and rewriting jobs whose input behavior you test; use an HTML5 parser when the exact browser tree matters.

Verdict

Our node-html-parser 9.0.1 install used 4 MB across 10 packages with 0 audit findings, while its full browser import reached 47.3 KB gzipped. Install it for fast Node-side extraction or controlled HTML rewrites; do not install it for sanitizer duty, browser emulation, or conformance-critical malformed HTML.

We installed it

Lab card: what happened when we installed node-html-parserScreenshot of node-html-parser documentation
Install✓ · 1.8s10 packages on disk · 4 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser47.3 KBgzipped (108.4 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does node-html-parser install cleanly?

Yes. In a fresh container with an empty cache, npm install node-html-parser finished in 2 seconds, leaving 10 packages and 4 MB on disk. npm audit reported no known vulnerabilities.

How much does node-html-parser add to a browser bundle?

47.3 KB gzipped (108.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does node-html-parser work with both ESM and CommonJS?

Yes. Both import 'node-html-parser' and require('node-html-parser') worked in Node 22 in our run. The package is published as CommonJS with an exports map.

Does node-html-parser include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

node-html-parser or parse5: which should you use?

parse5: Choose it when WHATWG HTML parsing and browser-like malformed-markup recovery matter more than a compact mutable query API. Our node-html-parser 9.0.1 install used 4 MB across 10 packages with 0 audit findings, while its full browser import reached 47.3 KB gzipped.

When should you not use node-html-parser?

Correct HTML5 tree construction for arbitrary malformed pages is a requirement. The README says some malformed HTML may parse incorrectly; parse5 is the safer conformance choice.

API stability4/5The main `parse()` entry, wrapper root, selector methods, mutable HTMLElement methods, text properties, class list, relatives, and source ranges remain available in 9.0.1. The latest major changed packaging rather than the DOM-shaped API, and 9.0.1 promptly repaired declaration generation. Stability is not a 5 because parser recovery flags and serialization details can change output on malformed input, and the project does not claim browser-equivalent tree construction as a fixed compatibility contract.
Docs4/5The README documents parse options, every major node method and property, selector support, mutation, class handling, text variants, source ranges, legacy and current imports, and a benchmark with its date and tool. It plainly states that malformed HTML can parse incorrectly, which is useful. The guide is weaker on security boundaries, exact HTML5 differences, memory use, streaming, browser bundle cost, serialization round trips, and migration notes for packaging majors. Several behaviors are clearer in tests and types than in prose.
Maintenance4/5The repository is unarchived, was pushed on 2026-07-29, and reports 19 open issues and pull requests with 1,245 stars. Version 9.0.1 shipped that day to fix TypeScript declaration generation after the 9.0 packaging rewrite. The preceding releases corrected attribute backslash round trips, whitespace serialization, missing closing tags, CDN output, and dependency choices. The release page lags behind tags after 7.1, so current changes require reading commits or package history rather than relying on GitHub Releases alone.
Ecosystem4/5The npm endpoint counted 8,959,765 downloads in the latest week. The package offers bundled types, an exports map, distinct ESM and CommonJS files, broad CSS selector support through css-select, and a mutable tree that fits scrapers and build tools. Its 10 installed packages and 47.3 KB gzipped full browser build are less appealing on the client. It also occupies a specific middle ground: more convenient than a tokenizer, less faithful than parse5, and far less browser-like than jsdom.

Use it if

  • A Node process must query and edit HTML fragments with familiar selectors but does not need layout, scripts, events, or browser globals.
  • Inputs come from a controlled template or a known set of pages whose malformed-markup behavior can be covered with fixtures.
  • Source ranges, mutable nodes, text extraction, and serialization are needed in one small in-memory parser.
  • Both CommonJS and ESM consumers need documented entry points and bundled TypeScript declarations.
Skip it if

Setup reality

We installed node-html-parser 9.0.1 in a clean Node 22 Bookworm container. npm took 1.8 seconds and left 10 packages using 4 MB on disk. npm audit found 0 vulnerabilities at all four severities. The package itself declares 2 direct dependencies and 0 peers and is 716 KB unpacked. It is CommonJS with an exports map, includes TypeScript declarations, and passed both our require() and ESM import checks.

No credentials or configuration file are involved. Parsing returns a wrapper root, so the first input element is usually root.firstChild, not the root itself. querySelector() can return null and getAttribute() can return undefined. Comments are discarded unless enabled. Script, style, pre, and noscript text behavior is configurable. Recovery flags such as fixNestedATags, parseNoneClosedTags, and preserveTagNesting change the tree and need fixture tests against your actual inputs.

A full esbuild browser import measured 108.4 KB minified and 47.3 KB gzipped in our sandbox. Version 9 exposes separate import and require targets through one exports map. The package has no engines declaration, so npm does not enforce a Node floor. TypeScript 4.1.2 or newer is the README's stated minimum. Version 9.0.1 exists specifically because declaration generation in 9.0 needed repair, making 9.0.0 a poor pin for typed consumers.

The tree is mutable and serialization is not a byte-preserving round trip. Attribute quoting, entity decoding, tag repair, whitespace removal, and inserted HTML can change output. set_content() parses strings as markup; use a text-oriented assignment when user input must remain text. Source range values refer to the original input and are not a patch map after mutation. Extracted URLs and JSON-LD remain untrusted data. Parse, validate, sanitize where appropriate, and only then use or render the result.

Patterns

Parse a fragment and find an element parse-and-query

import { parse } from 'node-html-parser';

const root = parse('<ul id="list"><li>Hello</li></ul>');
const list = root.querySelector('#list');
console.log(list?.text, list?.tagName);

`parse()` creates a wrapper root, and `querySelector()` may return null. Default tag names are uppercase.

Open the CommonJS entry require-commonjs

const { parse } = require('node-html-parser');
const root = parse(html);

Version 9 declares separate require and import targets. Both loaded successfully in our Node 22 sandbox.

Collect text and href values extract-links

const links = root.querySelectorAll('a.result').map(node => ({
  href: node.getAttribute('href'),
  label: node.text.trim(),
}));

A missing attribute produces `undefined`. Resolve and validate each URL before using it for another request.

Enable selected recovery options configure-recovery

const root = parse(html, {
  comment: true,
  fixNestedATags: true,
  parseNoneClosedTags: true,
  preserveTagNesting: false,
});

Each option changes the resulting tree. None promises the same error recovery as a conforming browser parser.

Read a JSON-LD script extract-json-ld

const script = root.querySelector('script[type="application/ld+json"]');
const data = script ? JSON.parse(script.rawText) : null;

`rawText` avoids HTML entity decoding. Catch JSON errors and validate the object before trusting embedded metadata.

Change attributes and child markup mutate-element

const price = root.querySelector('.price');
if (price) {
  price.setAttribute('data-checked', 'yes');
  price.set_content('<span>19.99</span>');
}
console.log(root.toString());

`set_content()` interprets a string as HTML. Do not pass unsanitized user text when the result will be rendered.

Move from a cell to nearby rows walk-relatives

const cell = root.querySelector('td.total');
const row = cell?.closest('tr');
const nextRow = row?.nextElementSibling;
const elements = row?.children ?? [];

`children` returns elements only; `childNodes` also includes text and any retained comments.

Slice an element from the source read-source-range

const target = root.querySelector('#target');
if (target) {
  const [start, end] = target.range;
  console.log(html.slice(start, end));
}

Ranges index the original input. Mutating the tree does not update them into coordinates for the serialized output.

Run the parser's validity check validate-html

import { valid } from 'node-html-parser';

if (!valid(fragment)) {
  throw new Error('Fragment failed parser validation');
}

This check reflects node-html-parser's grammar and recovery rules, not full WHATWG conformance or sanitization.

Drop script and style nodes remove-elements

for (const node of root.querySelectorAll('script, style')) {
  node.remove();
}
const text = root.structuredText;

Removing two tag types does not sanitize HTML. Event attributes, dangerous URLs, and other active content remain.

Edit a copy of one subtree clone-node

const card = root.querySelector('.card');
const copy = card?.clone();
copy?.classList.add('preview');

Clone before applying mutations that should not affect the original tree. Serialize the copy separately.

Read retained comments preserve-comments

const root = parse(html, { comment: true });
const comments = root.childNodes.filter(node => node.nodeType === 8);

Comments are omitted by default. Enabling them costs work and exposes comment nodes through `childNodes`.

Alternatives

PackageRegistryPick it when
parse5npmChoose it when WHATWG HTML parsing and browser-like malformed-markup recovery matter more than a compact mutable query API.
cheerionpmChoose it for a jQuery-style traversal and manipulation surface with a wider scraping ecosystem.
htmlparser2npmChoose it for event-driven parsing, lower-level DOM handlers, or large inputs that should not start as one string tree.
jsdomnpmChoose it when tests or tools need broader browser DOM APIs, URL handling, events, and optional script behavior.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.