mrkeyoor.com_
Thu 06 Aug 05:57 UTC
npmDataupdated 06 Aug 2026

cheerio

Cheerio parses an HTML or XML string into a tree and hands you a jQuery-shaped API for querying and editing it. You call cheerio.load(html) and get back a $ function, then use the selectors and methods you already know from jQuery: $('a.link').attr('href'), .text(), .find(), .each(), .append(), .remove(). When you are done you serialize the tree back to a string with $.html(). There is no browser involved: no JavaScript execution, no CSS layout, no network requests unless you ask for them. It parses with parse5 by default for spec-correct HTML, and can switch to htmlparser2 for a faster forgiving parse or for XML documents. That combination is why it sits under most Node scrapers, HTML post-processing build steps, and server-render test helpers.

Verdict

For server-side HTML that arrives as text, cheerio is still the default answer and the 1.x line is actively maintained. The one question to settle before you install it is whether the data you want is in the raw HTML at all, because cheerio will never run the JavaScript that puts it there.

API stability4/5The jQuery subset has barely moved in a decade, but the 1.0 release changed the module entry from a default export to named exports, deleted the static cheerio.html and cheerio.text helpers, and raised the Node floor, so a lot of older snippets no longer run as written.
Docs4/5cheerio.js.org carries a tutorial, loading and traversing basics, and a full generated API reference with examples, though scraping practicalities such as encoding and pagination are left to you and some options are only visible in the TypeScript declarations.
Maintenance4/5Repo pushed 6 August 2026 with 1.2.0 released January 2026, and 29 open issues (63 counting PRs) on a project this widely used, which is a small queue; work still concentrates on a handful of maintainers funded through OpenCollective.
Ecosystem5/5Roughly 27.2M weekly downloads and it is the parsing layer inside crawlers, scrapers, static site tooling, and test helpers, so nearly every HTML-handling Node tutorial assumes it.

Use it if

  • You are pulling data out of server-rendered HTML and already think in CSS selectors, so $('table tbody tr') beats writing a regex or a hand-rolled tree walk
  • You need to rewrite HTML you did not author: injecting tracking pixels into email templates, inlining assets in a static site build, or rewriting relative links to absolute ones before publishing
  • You want to assert on the HTML output of an SSR framework or template engine in unit tests without booting a headless browser
  • You are parsing XML feeds such as RSS or Atom and want the same selector API with xml: true instead of a separate XML library
  • Throughput matters: parsing thousands of documents per minute in a worker is fine here, and would be hopeless with a real browser per page
Skip it if

Setup reality

npm install cheerio is quick and there is no native code, but it brings 11 direct dependencies including undici, which is a full HTTP client that exists only so cheerio.fromURL works. The 1.0 release changed the entry shape: there is no default export any more, so it is import * as cheerio from 'cheerio' or const cheerio = require('cheerio'), then cheerio.load(html). The old static shortcuts cheerio.html() and cheerio.text() were removed, which is why copy-pasted snippets from tutorials written against 1.0.0-rc.12 throw immediately. Node 20.18.1 or newer is enforced through engines. The package ships dual ESM and CommonJS builds plus separate cheerio/slim and cheerio/utils entry points, so bundler config that predates package exports may resolve the wrong file. If you use TypeScript, selectors are typed through SelectorType and overly dynamic selector strings will need a cast.

Patterns

Load HTML and read values with selectorsload-and-select

import * as cheerio from 'cheerio';

const $ = cheerio.load('<ul id="fruits"><li class="apple">Apple</li><li class="pear">Pear</li></ul>');

$('.apple').text();          // 'Apple'
$('li').length;              // 2
$('#fruits li').first().attr('class'); // 'apple'
$.html();                    // full document string

There is no default export in 1.x. Importing cheerio and calling cheerio('...') directly throws; you must call cheerio.load first. load also wraps fragments in html/head/body unless you pass isDocument false as the third argument.

Pull a list of records out in one call with $.extractextract-structured-data

import * as cheerio from 'cheerio';

const $ = cheerio.load(html);

const data = $.extract({
  rows: [
    {
      selector: 'li.row',
      value: {
        url: { selector: 'a', value: 'href' },
        name: 'a',
        price: '.p',
      },
    },
  ],
  pageTitle: 'h1',
});
// { rows: [{ url: '/x', name: 'X', price: '9' }, ...], pageTitle: '...' }

A bare string value means text content. To read an attribute or outerHTML you need the object form with an explicit value key; writing value: { href: 'href' } silently returns empty objects instead of erroring.

Turn a selection into a plain JavaScript arraymap-to-array

const hrefs = $('a').map((i, el) => $(el).attr('href')).get();

// nested rows: use toArray, not map().get()
const rows = $('tbody tr')
  .toArray()
  .map((tr) => $(tr).find('td').toArray().map((td) => $(td).text().trim()));

map().get() flattens one level, so a table read with nested map().get() comes back as one long flat array of cells. Use toArray() plus a normal Array.map when the result should stay nested.

Load a document straight from a URLfetch-and-parse

import * as cheerio from 'cheerio';

const $ = await cheerio.fromURL('https://example.com', {
  requestOptions: {
    headers: { 'user-agent': 'my-crawler/1.0' },
  },
});

console.log($('h1').text());

fromURL follows redirects, rejects non-2xx responses, and sniffs the charset for you. It only exists in the full build, not in cheerio/slim, and it is the reason undici is a dependency.

Edit the tree and write it back outmodify-and-serialize

const $ = cheerio.load(template);

$('script').remove();
$('h1').addClass('headline').text('New title');
$('body').append('<footer>generated</footer>');
$('img').each((i, el) => $(el).attr('loading', 'lazy'));

const out = $.html();

Every method mutates the loaded tree in place, so there is no immutable mode and no undo. $.html() re-serializes the whole document including the html/head/body wrapper load added; use $('body').html() or .prop('outerHTML') for fragments.

Parse RSS or other XML with the same APIparse-xml-feed

const $ = cheerio.load(rssText, { xml: true });

const items = $('item')
  .toArray()
  .map((el) => ({
    title: $(el).find('title').first().text(),
    link: $(el).find('link').first().text(),
  }));

$.xml(); // serialize back as XML

xml: true switches to htmlparser2 with case-sensitive tag names and no html/body wrapper. Namespaced tags such as content:encoded need an escaped selector like $('content\\:encoded') because the colon is a CSS pseudo-class character.

Rewrite relative links to absolute onesresolve-relative-urls

const base = 'https://example.com/blog/';

$('a[href], img[src]').each((i, el) => {
  const $el = $(el);
  const attr = $el.is('a') ? 'href' : 'src';
  const raw = $el.attr(attr);
  if (raw) $el.attr(attr, new URL(raw, base).href);
});

Cheerio stores attributes exactly as written and does not resolve anything, unlike a browser where element.href is already absolute. Guard against missing attributes; new URL(undefined, base) throws.

Walk the tree relative to a found elementtraverse-relative

const price = $('span:contains("Price")')
  .first()
  .closest('.product')
  .find('.amount')
  .text()
  .trim();

$('h2').each((i, el) => {
  const body = $(el).nextUntil('h2').text();
});

:contains is a jQuery extension, not standard CSS, and it matches descendants too, so a wrapper div containing the text also matches. Anchor with .first() or a tighter selector before traversing.

Read data attributes with type coercionread-data-attributes

const $ = cheerio.load('<div id="x" data-user-id="7" data-tags="[1,2]"></div>');

$('#x').data();            // { userId: 7, tags: [1, 2] }
$('#x').data('userId');    // 7
$('#x').attr('data-user-id'); // '7' (always a string)

data() parses JSON and numbers like jQuery does, so an id such as data-id="0012345" comes back as the number 12345 and loses the leading zeros. Use attr() when you need the literal string.

Use the smaller htmlparser2-only buildslim-build

import { load } from 'cheerio/slim';

const $ = load('<p>hi');
$.html(); // '<p>hi</p>'

cheerio/slim exports only load, contains, and merge, and parses with htmlparser2 instead of parse5. It is faster and much smaller, but it is more forgiving than the HTML spec on malformed markup, and fromURL and the stream helpers are not there.

Parse a file as a stream instead of a stringstream-large-documents

import * as fs from 'node:fs';
import * as cheerio from 'cheerio';

const writable = cheerio.decodeStream({}, (err, $) => {
  if (err) throw err;
  console.log($('h1').text());
});

fs.createReadStream('big-page.html').pipe(writable);

decodeStream takes buffers and sniffs the encoding; stringStream takes strings and skips the sniffing. Neither one streams results out: the callback still fires once at the end with the whole document in memory.

Fail loudly when a selector matches nothingdetect-empty-selection

function requireOne($sel, label) {
  if ($sel.length !== 1) {
    throw new Error(`${label}: expected 1 match, got ${$sel.length}`);
  }
  return $sel;
}

const title = requireOne($('h1.title'), 'title').text();

An empty selection returns '' from .text() and undefined from .attr(), never an error, so a site redesign turns into empty rows in your database instead of a failed job. Assert on .length at the boundaries of a scraper.

Alternatives

PackageRegistryPick it when
jsdomnpmYou need a real DOM implementation with document, window, and optional script execution instead of a jQuery-style wrapper
node-html-parsernpmYou want a much lighter and faster parser and are happy with a small querySelector-style API
playwrightnpmThe markup you need is produced by client-side JavaScript, so the page has to actually run in a browser
linkedomnpmYou want standard DOM methods and a lighter footprint than jsdom for server-side HTML work