Skip to content

Guides

Cheerio web scraping in Node.js: fast HTML parsing without a browser

Use Cheerio with Node.js to parse server-rendered HTML, extract records, follow pagination, and know exactly when Cheerio is the wrong tool.

Last updated 2 min readBy CrawlerBros Editorial

Tested with Node.js 24.13.0, Cheerio 1.2.0 on 2 November 2026. Library versions and dates are part of the evidence, not decoration.

Cheerio is a fast HTML parser for Node.js with a jQuery-like API. It does not run JavaScript and does not fetch pages; it takes an HTML string and gives you $, so you can select elements and extract text. It is the right default for server-rendered pages.

If the data is not in the HTML, Cheerio will return an empty selection. That is not a selector bug; it is the browser boundary.

Install

mkdir cheerio-scrape && cd cheerio-scrape
npm init -y
npm pkg set type=module
npm install cheerio

Tested with Node.js 24.13.0 and Cheerio 1.2.0 on 2 November 2026.

Fetch and parse

import { load } from 'cheerio'

const response = await fetch('http://127.0.0.1:8765/items?page=1', {
  headers: {
    'User-Agent': 'CrawlerBros-Example/1.0 (+https://www.crawlerbros.com/about)',
    Accept: 'text/html',
  },
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)

const html = await response.text()
const $ = load(html)
const rows = $('ul.results li.item').map((_, element) => ({
  sku: $(element).attr('data-sku'),
  name: $(element).find('h3').text().trim(),
  price: $(element).find('.price').text().trim(),
})).get()

console.log(JSON.stringify(rows, null, 2))

Output against the fixture:

[
  {
    "sku": "A-1",
    "name": "Mechanical keyboard",
    "price": "$89.00"
  },
  {
    "sku": "B-2",
    "name": "USB-C hub",
    "price": "$39.50"
  }
]

$(element) scopes the search to one card. That prevents the “titles and prices drifted out of sync” bug you get from two independent $('h3') and $('.price') lists.

Pagination

let url = 'http://127.0.0.1:8765/items?page=1'
const records = []
const seen = new Set()

while (url && !seen.has(url)) {
  seen.add(url)
  const response = await fetch(url, {
    headers: { 'User-Agent': 'CrawlerBros-Example/1.0 (+https://www.crawlerbros.com/about)' },
  })
  if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`)
  const $ = load(await response.text())

  $('ul.results li.item').each((_, element) => {
    records.push({
      sku: $(element).attr('data-sku'),
      name: $(element).find('h3').text().trim(),
      price: $(element).find('.price').text().trim(),
    })
  })

  url = $('a[rel="next"]').attr('href')
    ? new URL($('a[rel="next"]').attr('href'), url).href
    : null
}

console.log(records.length, records.map(record => record.sku))

Tested output:

5 [ 'A-1', 'B-2', 'C-3', 'D-4', 'E-5' ]

The seen set stops a loop when a next link points back to the current page. A missing next link is the normal end.

Cheerio selectors

Cheerio uses css-select, which supports most CSS selectors:

$('article.card h2')                       // descendants
$('article.card > h2')                     // direct children
$('a[href^="/docs"]')                      // attribute prefix
$('tr:nth-child(2) td')                    // positional
$('p').filter((_, element) => $(element).text().includes('Price')) // text filter

Cheerio does not support :contains() or :has() in the same way jQuery does. For text matching, use .filter() with .text(), or select a stable parent and walk.

When Cheerio is the wrong tool

  • Client-rendered pages. If the data appears only after JavaScript, load(html) sees the shell. Find the JSON endpoint or use Playwright/Puppeteer.
  • Pages that need clicks, scrolls, or a session. Cheerio has no browser.
  • A single request that returns JSON. Skip the parser and use response.json().
  • A site that requires auth you do not have permission to use. A parser does not change the permission.

Cheerio vs BeautifulSoup

Criterion Cheerio BeautifulSoup
Language Node.js Python
Speed Fast Slower (pure Python backend)
CSS selectors Yes Yes
XPath No No (use lxml)
Tree editing Limited Yes
Best for Node HTTP pipelines Python scripts and forgiving parsing

If you are in Node and the page is server-rendered, Cheerio is the default. If you need XPath or a browser, choose a different tool.

Sources

Sources checked