Cheerio: jQuery Selectors on a Server-Side DOM, and Where That Model Breaks
The fast, flexible, and elegant library for parsing and manipulating HTML and XML.
At a glance
- What is it?
- Cheerio gives Node and browser code a jQuery-shaped API over parse5 or htmlparser2 output. It is a good fit for extracting and rewriting markup you already have, and the wrong tool for pages that only exist after JavaScript runs.
- Who is it for?
- Adopt Cheerio if you already have HTML or XML as a string and need to query or rewrite it with jQuery-style selectors, and if you are comfortable that no JavaScript in the document will execute. Do not adopt it as a headless browser or as a substitute for one when the content you want is rendered client-side.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem Cheerio solves: querying markup without a browser
jQuery's selector API became the default vocabulary for finding elements in a document, but it assumes a browser: a window, a document, a live DOM that the page has already built. Server-side code that receives an HTML string has none of that. Cheerio's answer is to keep the API and drop the browser. The README describes it as implementing "a subset of core jQuery" while removing "all the DOM inconsistencies and browser cruft." The intended user is a Node or TypeScript developer who has markup in hand, usually from an HTTP response, a file, or a build step, and wants to reach into it with $('ul .pear') rather than with a hand-written tokenizer or a regular expression. The README also notes that Cheerio works in both browser and server environments, so the same selection code can run in a bundled front-end if needed. What it is not is a renderer. Nothing in the library fetches a page, runs scripts, or lays out content.
Two parsers underneath one selector API
The architecture is a thin adapter. Cheerio wraps parse5 for HTML parsing and can optionally use htmlparser2, which the README calls "forgiving." The parsed tree is then exposed through a jQuery-like collection object. The README gives a partial list of the node properties you can expect on those objects: tagName, parentNode, previousSibling, nextSibling, nodeValue, firstChild, childNodes, lastChild. That list is a useful signal about how close the model is to the browser DOM and where it stops: there is no mention of computed styles, no layout, no event system. Two parsers with different error tolerance is the practical reason the option exists. parse5 follows the HTML specification's error handling closely, which matters when you need the tree a browser would build from broken markup. htmlparser2 is faster and more permissive, which matters when you are processing XML-ish or machine-generated documents that do not need spec-compliant recovery. The choice is a real trade-off, and the README does not spell out which one to pick for which input, so the decision is left to the reader.
Loading, selecting, and rendering: the three calls that define the workflow
The README's example is the whole lifecycle in a few lines. You load, you select, you render. Loading is explicit, and the README explains why: in jQuery the step is implicit because jQuery operates on the one baked-in DOM, while Cheerio needs the document passed in. The canonical form is `const $ = cheerio.load('<ul id="fruits">...</ul>');`. Once loaded, the selection method is `$(selector, [context], [root])`, where the selector searches within the context scope, which searches within the root scope. Selector and context can each be a string expression, a DOM element, an array of DOM elements, or a Cheerio object. The README's examples are compact: `$('.apple', '#fruits').text()` returns Apple, `$('ul .pear').attr('class')` returns pear, and `$('li[class=orange]').html()` returns Orange. Rendering has more than one exit. `$.root().html()` serialises the whole document, `$('.pear').prop('outerHTML')` gives you the outer HTML of a single selection, and `$('body').text()` returns text content with tags stripped. Note the asymmetry in the first example: the input was a bare `<h2>` fragment, and `$.html()` returned a full document with html, head, and body elements wrapped around it. If you are round-tripping a fragment, that wrapping is something your code has to account for.
Installation and the environment-specific import
Installation is a single package-manager command. The README lists `npm install cheerio`, `bun add cheerio`, and `deno install cheerio` as equivalent options, and the package is published on npm. The import style depends on the environment. For ESM or TypeScript the README shows `import * as cheerio from 'cheerio';`, and for other environments it shows `const cheerio = require('cheerio');`. That distinction is worth noting for TypeScript users specifically, because the namespace import is what the documentation presents as the supported form rather than a default import. There is no configuration file, no plugin registry, and no build step described in the README. The one configuration-shaped decision the documentation surfaces is the parser choice between parse5 and htmlparser2, and the README does not give a config key for it in the excerpt available here. That gap is a fair criticism of the documentation as presented: the feature is mentioned as an option without the mechanism for selecting it being shown in the same section.
What Cheerio does not do, and when that disqualifies it
Cheerio parses markup. It does not execute JavaScript. A single-page application whose product listings are injected by a client-side framework will hand Cheerio an empty shell, and no selector will recover content that was never in the response body. This is the failure mode most likely to waste a developer's afternoon, because the code is correct and the result is an empty selection. The README's own framing supports this reading: it describes parsing, manipulating, and rendering, and the screencast it links is presented as a comparison against JSDOM plus jQuery, which is a different kind of tool. There is a second, quieter limitation. The DOM node properties listed are a subset of the browser's, and the README does not claim completeness. Code that depends on layout, on computed styles, or on the full Node interface will not find those here. Finally, the README's own feature list is written in promotional register ("Blazingly fast," "Incredibly flexible"), and it offers no benchmark numbers, no throughput figures, and no comparison table. Anyone choosing Cheerio on performance grounds is choosing on the basis of an adjective, not a measurement. If raw parse throughput is the deciding factor for your workload, that is a claim to verify against your own inputs rather than to accept from the README.
JSDOM is the alternative, and the difference is the execution model
The natural comparison is JSDOM. The README itself frames the screencast as showing "how much faster cheerio is than JSDOM + jQuery," so the project positions against it directly. The difference in approach is not speed, it is scope. JSDOM implements a browser-like environment: it models the document as a live, mutable DOM with the surrounding interfaces, which is why it can be paired with jQuery and why it can support code that expects a window. Cheerio implements a simpler, consistent DOM model, in the README's words, and exposes a jQuery-style API over it without pretending to be a browser. The practical consequence: if your task is to run scripts embedded in the page, or to test front-end code that manipulates a live document, JSDOM is the tool whose model matches the problem. If your task is to read values out of markup you already have, Cheerio's smaller model is the reason it does less work per document. Choosing between them is choosing whether you need a browser environment or only the tree.
Licence, releases, and what maintenance looks like from the outside
Cheerio is MIT licensed, which permits commercial use, modification, and redistribution provided the licence and copyright notice are preserved. That is a permissive arrangement with no copyleft obligation on your own code, and it is the same licence family as much of the Node ecosystem. This is not legal advice; if your organisation has a policy on third-party licences, run the package through it. On maintenance, the visible signals are the release history and the repository activity. The recent releases listed are v1.2.0 in January 2026, v1.1.2 in July 2025, and v1.1.1 a day earlier in July 2025, with the most recent push to the main branch in September 2026. The repository is not archived. Those dates describe cadence, not quality, and they say nothing about whether a given bug affects you. The upgrade cost visible from this material is low: the API shown in the README is stable across the 1.x line, and the package has no documented plugin surface or configuration file that a major release would have to migrate. The two patch releases a day apart in July 2025 are the kind of detail worth noting rather than reading into; the material here does not explain what they fixed.
A concrete adoption test before you commit
The cheapest way to decide is to run Cheerio against your actual inputs rather than the README's fruit list. Take one document from each source you intend to parse, load it with `cheerio.load()`, and run the exact selectors you plan to use in production. Then serialise with `$.html()` and compare the output against what your downstream consumer expects, paying attention to the html, head, and body wrappers that appear around fragment input. Repeat with at least one malformed or truncated document, because that is where the parse5 and htmlparser2 choice starts to matter and where the README leaves you to experiment. If any of your target pages render their content client-side, stop here: Cheerio is the wrong layer, and the test will show it as an empty selection rather than as an error.
Editorial conclusion
Adopt Cheerio if you already have HTML or XML as a string and need to query or rewrite it with jQuery-style selectors, and if you are comfortable that no JavaScript in the document will execute. Do not adopt it as a headless browser or as a substitute for one when the content you want is rendered client-side. Before committing, load one representative document from each of your real sources, including at least one malformed page, and confirm that the selectors you plan to use return the nodes you expect; then check the shape of the rendered output from $.html() against whatever consumes it downstream.
Community notes