Back to all posts

Best JavaScript and Node.js Web Scraping Libraries in 2026

Four Node.js libraries cover almost every JavaScript scraping case in 2026. fetch + Cheerio handles static HTML. playwright runs the browser when a page builds its content with JavaScript. The playwright-extra plus puppeteer-extra-plugin-stealth combination closes the automation leaks default headless Chromium ships with. Crawlee covers the cases where one scraper turns into a multi-page crawler with queues and retries.

Everything else on the npm registry is either an alternative to one of the four, or a library you can safely skip in 2026.

Common Takeaways:

  • fetch + cheerio parses a typical static HTML page in 8 ms median with about 28 MB of heap.
  • playwright cold-starts headless Chromium in 251 ms, 4x faster than Puppeteer and 2.3x lighter on RAM.
  • puppeteer-extra-plugin-stealth through playwright-extra brings sannysoft from 5 failures to 0.
  • Crawlee bundles request queue, retries, proxy rotation, and a fingerprint injector for multi-page crawls.
  • Skip x-ray, nightmare, osmosis, and selenium-webdriver for JavaScript-only projects.

You’ll need Node 22+ and basic comfort with async/await. Every library covered here ships TypeScript types.

How to pick a JavaScript scraping library

The library you need depends on three questions in order. Each answer narrows the shortlist.

The first question is whether the page is static or dynamic. Static means the HTML returned by fetch already contains the data you want. Dynamic means the data shows up only after JavaScript runs in a browser. Open view-source in Chrome and search for the data you want to scrape. If it’s there, the page is static.

view-source of a JS-rendered page (data inside an inline script) next to view-source of a static page (data already in HTML)

The second question is how many pages you need to hit. One scraper-script for a single endpoint is different from a multi-page crawler with queues, retries, and dedup. Standalone libraries cover the first case. Crawlee covers the second.

The third question is anti-bot pressure. Default headless Chromium fails 5 of 20 sannysoft fingerprint checks in 2026. If your target inspects browser fingerprints (Cloudflare, PerimeterX, DataDome), you need the stealth plugin on top of Playwright. If your IPs are getting blocked, you need residential proxies on top of that. If both fail, the trade-off shifts toward a managed scraping API.

decision flowchart for picking a JavaScript scraping library based on the three questions: static-vs-dynamic, single-vs-crawl, and anti-bot pressure

The matrix below maps each common case to the library combo that handles it.

Use caseRecommended library combo
Static HTML, single pagefetch + Cheerio
Static HTML, hundreds of pagesCrawlee (CheerioCrawler mode)
JS-rendered page exposes XHR/APIfetch directly to the endpoint
Structured data in HTML (JSON-LD, __NEXT_DATA__, inline var data)fetch + Cheerio to extract from <script>
JS-rendered, no JSON shortcutPlaywright
JS-rendered, anti-bot pressurePlaywright + playwright-extra + stealth plugin + residential proxies
Need page’s own JS without a real browserjsdom
Already on a Chromium codebasePuppeteer

Comparison table

LibraryBest forCostdl/wkLast release
fetch (built-in)Static HTML in Node 22+NoneBuilt inBundled with Node 22
axiosHTTP with interceptors, proxy rotationOne dep122M2026-06-14
cheerioParse and extract from static HTML8 ms, +28 MB heap37M2026-01-23
jsdomParse + run inline page JS60 ms, +83 MB heap77M2026-04-30
playwrightJS-rendered pages, anti-bot251 ms cold-start, 167 MB62M2026-06-15
puppeteerExisting Puppeteer codebase1004 ms cold-start, 379 MB11M2026-05-26
playwright-extra + puppeteer-extra-plugin-stealthDefeat headless fingerprintStacks on Playwright/Puppeteer1M2023-03-01
crawleeMulti-page crawlerWraps Cheerio/Playwright130K2026-06-04

playwright-extra plus stealth has the only stalled release date in this set. The plugin still passes the sannysoft test in 2026. If Chromium changes a detectable surface and the plugin doesn’t follow, replace the patches with custom addInitScript calls or move fingerprint off-host.

1. Fetch

Node 22 ships with fetch built in. For static scraping that handles the HTTP side, and you don’t install anything.

A minimal GET against books.toscrape.com looks like this.

const res = await fetch('https://books.toscrape.com', {
  headers: {
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
    'accept-language': 'en-US,en;q=0.9',
  },
});

if (!res.ok) throw new Error(`status ${res.status}`);
const html = await res.text();

Two notes on this snippet. First, the user agent. Many servers return a stripped page or a 403 against Node’s default fetch UA, so a real desktop UA gets you the same HTML view-source shows in Chrome. Second, native fetch has no retries. If the server returns 503 once you get nothing, so for multi-page work wrap the call in a retry-on-5xx loop with exponential backoff.

node-fetch was the polyfill for Node ≤ 17 and you don’t need it in 2026.

2. Axios

axios adds four capabilities native fetch doesn’t have built in.

What axios givesNative fetch equivalent
Interceptors fire on every request, the natural place to rotate proxies, attach cookies, or log URLsWrap every call site manually
axios.get(url).then(r => r.data) returns parsed JSONawait res.json() per call
Proxy config is one object passed to axios.create({ proxy: { host, port, auth } })undici.Agent with a proxy dispatcher
AbortSignal passes through interceptorsThread signal into every call site

For a one-off fetch + Cheerio script, native fetch is enough. For a long-lived scraper with proxy rotation and retry hooks, axios is less code overall.

got covers the same ground as axios with different ergonomics. request was deprecated in 2020.

3. Cheerio

cheerio is the lightest way to extract data from static HTML in Node.js. It loads an HTML string, exposes CSS and jQuery selectors, and returns parsed content in a single-digit number of milliseconds. On the books.toscrape.com catalog page, Cheerio parses and extracts every book in 8 ms median and uses about 28 MB of heap across 20 iterations. jsdom on the same page is 60 ms and 83 MB, which is 7x slower and 3x heavier, because it builds a spec-compliant DOM. Playwright on the same task launches a Chromium process that occupies 167 MB before you load a single page. Cheerio doesn’t execute JavaScript, so anything the page builds client-side after load is invisible to it.

Scraping the books.toscrape.com catalog looks like this.

import * as cheerio from 'cheerio';

const res = await fetch('https://books.toscrape.com');
const $ = cheerio.load(await res.text());

const books = $('article.product_pod').map((_, el) => ({
  title: $(el).find('h3 a').attr('title'),
  price: $(el).find('.price_color').text().trim(),
  rating: ($(el).find('.star-rating').attr('class') || '').replace('star-rating ', ''),
})).get();

console.log(books.slice(0, 2));

The pattern across most Cheerio scrapers stays this short. Find the repeating element, .map() over it, pull each field with a CSS selector and .text() or .attr().

Cheerio extracts whatever sits in the HTML you pass it. Server-rendered React (Next.js, Nuxt), classic e-commerce listings, blog posts, news articles, and JSON-LD in <script type="application/ld+json"> tags all qualify because the data is in the raw HTML response. Pages that build their content with JavaScript after load do not. The selectors return empty arrays. View-source on the target first.

4. jsdom

jsdom is a JavaScript implementation of the DOM and HTML standards. It parses an HTML string into a full Window/Document object you can query like a browser, and it can run the page’s own inline scripts. The benchmark from the Cheerio section measured it at 60 ms per parse and 83 MB of heap across 20 iterations, 7x slower and 3x heavier than Cheerio because of that DOM compliance.

CapabilitycheeriojsdomPlaywright
Parse HTML to DOM
CSS selector queries
Run inline <script> tags✓ (in-process)✓ (real browser)
Network requests from the page
Layout, fonts, real rendering
Memory cost per parse~28 MB~83 MB167 MB Chromium process

In return, jsdom does one thing Cheerio cannot. It executes the page’s JavaScript. If the data you need shows up only after a small inline script runs (a token decoder, an email deobfuscator, an inline template that hydrates from __NEXT_DATA__), jsdom can run that script without launching Chromium.

import { JSDOM } from 'jsdom';

const html = `<!doctype html>
<div id="email"></div>
<script>
  document.getElementById('email').textContent =
    atob('aGVsbG9AZXhhbXBsZS5jb20=');
</script>`;

const dom = new JSDOM(html, { runScripts: 'dangerously' });
console.log(dom.window.document.getElementById('email').textContent);

The output:

hello@example.com

The runScripts: 'dangerously' flag opts into inline script execution. jsdom runs whatever the HTML hands it in the same Node process, so on untrusted input isolate the call inside a worker or sandbox.

For most static scraping, the trade isn’t worth it. Cheerio is faster and lighter, and Playwright handles the JS-rendered cases jsdom cannot (anything that depends on network, real rendering, or a fetch from inside the page). jsdom is the right pick when the page is static HTML plus a small inline script, and you don’t want to add Chromium to your container image.

5. Playwright

playwright is the default headless browser for JavaScript scraping in 2026. I ran the cold-start test on a fresh Node process per launch (5 runs, median reported). Chromium comes up in 251 ms and uses 167 MB of RSS before the first page load. That’s 4x faster cold-start and 2.3x less RAM than Puppeteer (same benchmark, full numbers in the next section). It runs a real Chromium, so JavaScript runs, fonts render, requests look like a browser’s, and the DOM you scrape is the one a user would see.

Here’s a Playwright scraper against quotes.toscrape.com/js, a page that renders its quotes via inline JavaScript. The npm install only ships the driver, so run npx playwright install chromium first to pull Chromium into the local cache.

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://quotes.toscrape.com/js/');

const quotes = await page.locator('.quote').evaluateAll(els =>
  els.map(el => ({
    text: el.querySelector('.text').textContent,
    author: el.querySelector('.author').textContent,
  }))
);

console.log(quotes.slice(0, 2));
await browser.close();

The locator API auto-waits. Calls on page.locator('.quote') like .evaluateAll() block until the matching elements are present and stable, so most race conditions between page load and scrape disappear without writing manual waitForSelector calls.

Playwright also drives Firefox and WebKit out of the box. Most scrapers only need Chromium, but the option matters when a target’s rendering quirk only shows up in Firefox.

Default headless Chromium fails 5 of 20 sannysoft fingerprint checks in 2026. On a target with Cloudflare, PerimeterX, or DataDome, the page either won’t load or returns a CAPTCHA. The stealth plugin section below covers what to do about that.

6. Puppeteer

puppeteer is the same idea as Playwright with a different lineage. Google’s Chrome DevTools team built it first, and it drives only Chromium. In 2026 it’s slower and heavier on the same task. Cold-start runs at 1004 ms median (vs Playwright’s 251 ms) and the launched Chromium occupies 379 MB of RSS (vs Playwright’s 167 MB). Both numbers come from the same 5-run benchmark in the repo for this article.

MetricPlaywrightPuppeteerRatio
Cold-start (ms, median of 5 runs)25110044.0x
Chromium RSS at idle (MB)1673792.3x
Browsers supportedChromium, Firefox, WebKitChromium onlyn/a

A minimal Puppeteer scraper looks almost identical to Playwright, with puppeteer.launch() and slightly different method names.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://quotes.toscrape.com/js/');

const quotes = await page.$$eval('.quote', els =>
  els.map(el => ({
    text: el.querySelector('.text').textContent,
    author: el.querySelector('.author').textContent,
  }))
);

console.log(quotes.slice(0, 2));
await browser.close();

For a new project, start on Playwright. The one reason to keep Puppeteer in 2026 is an existing codebase. I’d skip the migration unless something else forces the move. The API surface is similar enough that migration is several days of test rewrites for sub-second-per-launch wins, which rarely pays back on its own. If you’re starting on Puppeteer to pair with puppeteer-extra-plugin-stealth, the same plugin works against Playwright through playwright-extra (covered below).

7. Stealth plugin

Default Playwright fails 5 of 20 sannysoft fingerprint checks. With puppeteer-extra-plugin-stealth applied through playwright-extra, all 20 pass. These are the parts Cloudflare, PerimeterX, and DataDome read on first paint.

Plugin patchesDefault Chromium leak
navigator.webdriverReturns true instead of undefined
chrome.runtime objectMissing properties a real Chrome ships
Permissions APIInconsistent response to Notification.permission query
navigator.pluginsEmpty list
iframe contentWindowWrong prototype chain
navigator.deviceMemorySuspiciously round value
UA stringContains HeadlessChrome

Command to install:

npm install playwright playwright-extra puppeteer-extra-plugin-stealth

Script example:

import { chromium } from 'playwright-extra';
import Stealth from 'puppeteer-extra-plugin-stealth';

chromium.use(Stealth());

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://bot.sannysoft.com/');
await page.screenshot({ path: 'sannysoft.png', fullPage: true });
await browser.close();

The same plugin chain works against Puppeteer with puppeteer-extra. The patches and the result are identical.

The plugin’s last npm release is from March 2023 and its last commit from July 2024. Maintenance has effectively stopped, but the patches still hold against the latest Chromium in 2026. If Chromium changes a detectable surface and the plugin doesn’t follow, the fallback is custom addInitScript patches or a managed scraping API that handles fingerprint upstream.

Stealth fixes the browser fingerprint and nothing else. Your IP and your behavior pass through untouched. If a target rate-limits by IP or watches for inhuman mouse/keyboard cadence, a clean fingerprint won’t help. Anti-bot in 2026 layers three checks. Fingerprint first, then IP, then behavior. Fixing one without the others gets you a slightly more polite rejection. The section on managed scraping APIs below covers what to do when all three layers fail.

8. Crawlee

crawlee is the framework for the case where one scraper turns into a multi-page crawler. It bundles a request queue, retry-on-failure, proxy rotation, auto-scaling concurrency, a session pool, and a fingerprint injector. You pick a crawler class to match the target and pass it a request handler.

Crawler classWrapsBest for
CheerioCrawlerfetch + cheerioStatic HTML at scale
PlaywrightCrawlerPlaywrightJS-rendered pages at scale
PuppeteerCrawlerPuppeteerExisting Chromium-only codebase
HttpCrawlerraw HTTPWhen the response is already JSON

Here’s a CheerioCrawler against the books.toscrape.com catalog with pagination.

import { CheerioCrawler } from 'crawlee';

const crawler = new CheerioCrawler({
  maxRequestsPerCrawl: 50,
  async requestHandler({ $, request, enqueueLinks, pushData }) {
    const books = $('article.product_pod').map((_, el) => ({
      title: $(el).find('h3 a').attr('title'),
      price: $(el).find('.price_color').text().trim(),
    })).get();

    await pushData({ url: request.url, count: books.length, books });
    await enqueueLinks({ selector: '.next a' });
  },
});

await crawler.run(['https://books.toscrape.com']);

enqueueLinks walks the next-page links and adds them to the queue. pushData writes one JSON file per crawled page into ./storage/datasets/default/. maxRequestsPerCrawl caps the run so you don’t accidentally fetch 1000 pages on first try.

The PlaywrightCrawler swap is one import change and the same handler shape, with page instead of $. Crawlee bundles its own fingerprint injector that handles navigator.webdriver and the other tells the stealth section listed, so a PlaywrightCrawler doesn’t need playwright-extra on top.

For a one-shot scrape of three URLs, Crawlee is overkill. The setup cost only pays back when you need persistent state between runs, proxy rotation across hundreds of requests, or the auto-scaling concurrency.

Libraries to skip

A few libraries appear in older “best web scraping libraries” lists. Skip them.

LibraryReason to skipdl/wkLast release
x-rayReplaced by Crawlee + CheerioCrawler3.5K2019-07-15
nightmareReplaced by Playwright/Puppeteer12K2019-04-27
osmosisReplaced by Cheerio + async loop1202019-03-01
selenium-webdriverCross-language test stack, not JS-only2.1M2026-06-16

x-ray

x-ray was a Cheerio-plus-pagination wrapper. The last release shipped in July 2019. Crawlee with CheerioCrawler does the same job and is actively maintained.

nightmare

nightmare was Electron-based browser automation predating Puppeteer. Last release April 2019. Playwright and Puppeteer replaced it in the JS ecosystem.

osmosis

osmosis was a stream-based scraper. Last release March 2019, with 120 weekly downloads now. Modern Cheerio with async iteration covers the same use cases.

selenium-webdriver

selenium-webdriver is the one entry on this list that isn’t dead. It’s built for cross-language test infrastructure (Python, Java, Ruby, .NET). For a JavaScript-only stack, Playwright has a tighter API, faster cold-start, and 30x the npm install base. Pick Selenium only if you’re maintaining a polyglot test suite that already uses it.

Managed scraping APIs

When fingerprint patches and rotating proxies still get you blocked, the next layer up is a managed scraping API. HasData’s Web Scraping API handles JS rendering, residential proxies, fingerprint patches, and CAPTCHA solving in one endpoint. Pass a URL and get HTML or structured data back.

import * as cheerio from 'cheerio';

const res = await fetch('https://api.hasdata.com/scrape/web', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.HASDATA_API_KEY,
  },
  body: JSON.stringify({
    url: 'https://quotes.toscrape.com/js/',
    jsRendering: true,
    proxyType: 'residential',
  }),
});

const { content } = await res.json();
const $ = cheerio.load(content);

const quotes = $('.quote').map((_, el) => ({
  text: $(el).find('.text').text(),
  author: $(el).find('.author').text(),
})).get();

console.log(quotes.slice(0, 2));

The managed API is worth the cost when blocking eats more developer time than building the scraper. If a custom stealth stack costs you a week a month chasing blocks, the API removes that week.

FAQ

Which JavaScript web scraping library is fastest?

cheerio for parsing, by a wide margin. Parsing the books.toscrape.com catalog takes 8 ms median in Cheerio and 60 ms in jsdom, about 7x slower for the same task. For dynamic pages where you need a browser, playwright cold-starts in 251 ms versus Puppeteer’s 1004 ms. Across all stages of scraping, the fastest stack is fetch + cheerio for static HTML and playwright for JS-rendered pages.

Cheerio vs jsdom, what’s the difference?

Cheerio parses HTML and exposes jQuery-style selectors. jsdom builds a spec-compliant DOM and can execute inline <script> tags in-process. On the same page, jsdom is roughly 7x slower and 3x heavier on memory than Cheerio. Use Cheerio when the data is already in the HTML. Use jsdom only when the page builds data via inline scripts and you don’t want to launch Chromium.

Playwright vs Puppeteer in 2026, which to pick?

Playwright for new projects. It cold-starts 4x faster than Puppeteer (251 ms vs 1004 ms), uses 2.3x less RAM (167 MB vs 379 MB), drives Firefox and WebKit in addition to Chromium, and has Microsoft maintenance. Keep Puppeteer only on an existing codebase, since the migration costs days of test rewrites for sub-second-per-launch wins.

Does default Playwright bypass Cloudflare?

No. Default headless Chromium fails 5 of 20 sannysoft fingerprint checks and gets blocked by Cloudflare, PerimeterX, and DataDome on first paint. Apply puppeteer-extra-plugin-stealth through playwright-extra and all 20 checks pass. If the target also rate-limits by IP, add residential proxies. If both fail, move to a managed scraping API.

Conclusion

The pick order on any new JS scraping target.

  1. Open view-source on the URL. If the data is there, fetch + Cheerio is enough.
  2. If the data is missing from view-source, check the network tab for an XHR or API endpoint that returns it. If found, fetch directly to the JSON.
  3. If there is no JSON shortcut, use Playwright for the rendered DOM.
  4. If default Playwright gets blocked on Cloudflare, PerimeterX, or DataDome, add puppeteer-extra-plugin-stealth through playwright-extra.
  5. If stealth plus a rotating proxy pool still gets blocked, switch to a managed scraping API.
  6. If the scraper crawls 100+ pages with retries, dedup, and persistent state, wrap any of the above in Crawlee.
Valentina Skakun
Valentina Skakun
Valentina is a software engineer who builds data extraction tools before writing about them. With a strong background in Python, she also leverages her experience in JavaScript, PHP, R, and Ruby to reverse-engineer complex web architectures.If data renders in a browser, she will find a way to script its extraction.
Articles

Might Be Interesting