Looking for the best JavaScript web scraping library in 2026? The answer depends on what you're scraping.
For static HTML, a lightweight parser such as Cheerio may be all you need. For JavaScript-heavy websites, browser automation tools such as Playwright, Puppeteer, or Selenium make more sense. And once you start crawling hundreds or thousands of pages, frameworks such as Crawlee can handle queues, retries, concurrency, and sessions for you.
In this guide, we'll compare the most useful JavaScript web scraping tools and libraries available in 2026, including browser automation tools, HTML parsers, crawling frameworks, native Node.js options, and hosted scraping APIs.
For each tool, we'll cover what it's actually good at, show a working example, and look at the main pros and cons — so you can pick the lightest tool that gets the job done.

Quick answer (TL;DR)
Pick the tool based on what the target website actually needs:
| If you need... | Use... |
|---|---|
| A modern browser automation tool for JavaScript-heavy sites | Playwright |
| Mature browser automation with broad browser support and Selenium Grid | Selenium WebDriver |
| Chrome-focused browser automation with a mature API | Puppeteer |
| Fast scraping of static or server-rendered HTML | Cheerio |
| Browser-like DOM APIs without launching a real browser | JSDOM |
| A full crawling framework with queues, retries, sessions, storage, and browser support | Crawlee |
| Lightweight HTTP crawling with queues and Cheerio | Crawler |
| Fast, low-level, or streaming HTML parsing | htmlparser2 |
| Standards-compliant parsing of malformed HTML | parse5 |
| Simple HTTP requests with no extra dependency | Node.js fetch() |
| Hosted browsers, proxies, extraction, and scraping infrastructure | ScrapingBee |
The simplest rule is: if the page does not require JavaScript, don't launch a browser. Start with fetch() plus Cheerio or another parser. If the content only appears after JavaScript runs or requires real user interaction, move to Playwright, Puppeteer, or Selenium. For multi-page crawls, reach for Crawlee or Crawler instead of building the queueing and retry logic yourself.
1. Playwright
Playwright is one of the first tools worth reaching for when a website refuses to behave like a nice pile of static HTML.
Instead of just downloading the page source, Playwright launches a real browser, runs the site's JavaScript, waits for things to appear, and lets you click, scroll, fill forms, log in, and generally interact with the page like an actual user. It supports Chromium, Firefox, and WebKit and can run both headless and with a visible browser window.
That makes it a good fit for single-page applications, infinite scrolling, authenticated pages, client-side rendering, and other cases where the data simply isn't present in the initial HTML response.
If you've used Selenium, the basic idea will feel familiar. Both tools automate real browsers and cover most of the same scraping scenarios. Playwright just tends to feel more modern in a new JavaScript project: automatic waiting, locators, isolated browser contexts, and network interception are all baked into the API, so you usually spend less time writing glue code around the browser.
That doesn't mean Playwright magically replaces every other scraper, though. If the data is already sitting in the HTML or available from an API, launching a whole browser is like taking a taxi to cross the street.
Quick start
Install Playwright and the browser you want to use:
npm install playwright
npx playwright install chromium
The following example scrapes quotes from a page that renders its content with JavaScript:
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://quotes.toscrape.com/js/');
const quotes = page.locator('.quote');
await quotes.first().waitFor();
const data = await quotes.evaluateAll((elements) =>
elements.map((quote) => ({
text: quote.querySelector('.text')?.textContent?.trim(),
author: quote.querySelector('.author')?.textContent?.trim(),
})),
);
console.log(data);
} finally {
await browser.close();
}
Here, Playwright launches Chromium, lets the page run its JavaScript, waits until the quotes appear, and then pulls the data straight from the rendered DOM.
Find more information in our in-depth tutorial: Playwright web scraping.
Playwright pros
- Supports Chromium, Firefox, and WebKit
- Handles JavaScript-heavy and interactive pages
- Automatic waiting cuts down on manual synchronization
- Browser contexts make isolated sessions easy to manage
- Can inspect, intercept, and modify network requests
- Handles clicks, forms, scrolling, downloads, screenshots, and other browser interactions
Playwright cons
- Uses much more CPU and memory than HTTP-based scraping
- Requires browser binaries and sometimes additional system dependencies
- Overkill when the data is already available in HTML or through an API
- A real browser does not automatically make your scraper invisible to anti-bot systems
2. Selenium WebDriver
Selenium WebDriver is the old warhorse of browser automation. It has been around forever, it supports pretty much every major browser, and despite what some newer-tool evangelists might tell you, it is still very much alive.
For scraping, Selenium can do most of the same things as Playwright: open real browsers, run JavaScript, click buttons, fill forms, scroll pages, wait for dynamic content, and generally behave like a user with suspiciously perfect timing.
If you already use Selenium for testing, there is often no good reason to introduce another browser automation stack just for scraping. It also has a few tricks that newer tools do not fully replace, including native Safari support and Selenium Grid for running browser sessions across multiple machines.
Playwright often feels nicer in a fresh JavaScript project. Its API is newer, automatic waiting is more deeply baked in, and browser contexts make isolated sessions easy to manage. But that does not mean Selenium is automatically slower or worse. A 2025 test-automation study comparing Selenium, Playwright, and Cypress found that performance differences were fairly small and depended on the workload: Playwright won some tests, Selenium won others, and Selenium often used less memory.
In other words: Playwright may feel more modern, but Selenium is not some rusty tractor you keep around for nostalgia.
Quick start
Install the JavaScript bindings:
npm install selenium-webdriver
Modern Selenium also comes with Selenium Manager, which handles browser drivers automatically in most cases. No more hunting down the correct ChromeDriver version and praying that your PATH is having a good day.
Here's a small example that scrapes a JavaScript-rendered page with Chrome:
import { Builder, Browser, By, until } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome.js';
const options = new chrome.Options().addArguments('--headless=new');
const driver = await new Builder()
.forBrowser(Browser.CHROME)
.setChromeOptions(options)
.build();
try {
await driver.get('https://quotes.toscrape.com/js/');
await driver.wait(
until.elementLocated(By.css('.quote')),
10_000,
);
const quotes = await driver.findElements(By.css('.quote'));
const data = await Promise.all(
quotes.map(async (quote) => ({
text: await quote.findElement(By.css('.text')).getText(),
author: await quote.findElement(By.css('.author')).getText(),
})),
);
console.log(data);
} finally {
await driver.quit();
}
The code is a little more ceremony-heavy than the equivalent Playwright version, but the basic idea is exactly the same: launch a browser, wait for the page to render, grab the elements, extract the data, and shut everything down cleanly.
If you also use Python, check our complete guide to web scraping with Selenium and Python.
Selenium WebDriver pros
- Supports Chrome, Firefox, Edge, and Safari
- Handles JavaScript-heavy and interactive pages
- Available in several programming languages
- Selenium Manager handles browser drivers automatically
- Selenium Grid is built for distributed browser automation
- Huge ecosystem and years of battle testing
Selenium WebDriver cons
- The JavaScript API is more verbose than Playwright's
- You often need to be more explicit about waiting and synchronization
- Real browsers are expensive in CPU and memory
- Overkill when the data is already sitting in plain HTML or behind an API
3. Puppeteer
Puppeteer is another heavyweight option when scraping means actually opening a browser instead of just downloading some HTML.
Originally built around Chrome and the Chrome DevTools Protocol (CDP), Puppeteer is still very Chrome-friendly, but it is no longer Chrome-only. Current versions support both Chrome and Firefox. Chrome uses CDP by default, while Firefox automation runs through WebDriver BiDi.
For scraping, the basic idea is close to Playwright and Selenium: open a browser, let the page execute JavaScript, interact with it if necessary, and grab the rendered data. Puppeteer can handle SPAs, lazy-loaded content, forms, scrolling, screenshots, PDFs, network requests, and most other things you'd expect from a full browser automation tool.
So why pick Puppeteer over Playwright? If your work is mostly Chrome-based, Puppeteer gives you a mature API with very direct access to Chrome's automation capabilities. It also has modern locators with automatic waiting, browser contexts for isolated sessions, and plenty of lower-level browser and network controls.
Playwright offers broader browser-engine coverage thanks to WebKit and often feels a little more batteries-included, but for Chrome-heavy scraping the gap is much smaller than it used to be. Puppeteer is very much a current tool, not Playwright's forgotten older sibling.
Quick start
Install Puppeteer:
npm install puppeteer
The regular puppeteer package normally downloads a compatible Chrome for Testing during installation. Some package managers may block Puppeteer’s install script, in which case you can install the browser manually with npx puppeteer browsers install chrome.
Here's the same JavaScript-rendered quotes page we used earlier:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://quotes.toscrape.com/js/');
await page.waitForSelector('.quote');
const data = await page.$$eval('.quote', (quotes) =>
quotes.map((quote) => ({
text: quote.querySelector('.text')?.textContent?.trim(),
author: quote.querySelector('.author')?.textContent?.trim(),
})),
);
console.log(data);
} finally {
await browser.close();
}
Puppeteer runs headless by default, so there's no need to pass headless: true unless you want to make that choice explicit. Set headless: false when you actually want to watch the browser do its thing.
Learn more in our Guide to Puppeteer Scraping.
Puppeteer pros
- Supports Chrome and Firefox
- Excellent fit for Chrome-based browser automation
- Handles JavaScript-heavy and interactive pages
- Modern locators include automatic waiting
- Fine-grained access to browser and network behavior
- Supports isolated browser contexts
- Handles screenshots, PDFs, forms, scrolling, downloads, and other browser tasks
Puppeteer cons
- No WebKit or Safari support
- Running full browsers costs considerably more CPU and memory than HTTP-based scraping
- Some features available through Chrome's CDP are not yet available when using Firefox through WebDriver BiDi
- Usually overkill when the data can be fetched directly as HTML or from an API
4. Cheerio
Cheerio is what you reach for when the data is already in the HTML and launching an entire browser would be massive overkill.
Instead of running Chrome, executing JavaScript, and pretending to be a user, Cheerio simply parses HTML or XML and gives you a familiar jQuery-style API for querying and traversing it. That makes it much lighter than Playwright, Puppeteer, or Selenium and a great fit for blogs, product pages, documentation sites, server-rendered pages, and plenty of other scraping jobs.
The trade-off is simple: Cheerio is a parser, not a browser. It doesn't execute the page's JavaScript or wait for client-side content to magically appear. If the data only shows up after React, Vue, or some other frontend code runs, you'll need to fetch that data from the underlying API or bring in a real browser automation tool.
Modern Cheerio is also a bit more capable than the old "download HTML with Axios, then pass it to Cheerio" examples you may have seen. It can fetch a URL directly with fromURL(), load strings and buffers, parse streams, and extract structured data with extract().
Quick start
Install Cheerio:
npm install cheerio
Here's a small scraper for the static version of Quotes to Scrape:
import * as cheerio from 'cheerio';
const $ = await cheerio.fromURL('https://quotes.toscrape.com/');
const data = $.extract({
quotes: [
{
selector: '.quote',
value: {
text: '.text',
author: '.author',
},
},
],
});
console.log(data.quotes);
fromURL() downloads and parses the document for us, while extract() turns matching elements into structured data. No browser, no page lifecycle, no waiting for selectors — just grab the HTML and get to work.
You can still use fetch, Axios, Undici, or another HTTP client when you need more control over requests. Cheerio's traditional load() API also remains useful when you already have the HTML:
import * as cheerio from 'cheerio';
const response = await fetch('https://example.com');
const html = await response.text();
const $ = cheerio.load(html);
console.log($('h1').text());
Learn more in our guide: Using the Cheerio NPM Package for Web Scraping.
Cheerio pros
- Much lighter than running a real browser
- Familiar jQuery-style selectors and traversal
- Can fetch and parse URLs directly
- Supports HTML, XML, buffers, and streams
- Built-in
extract()API makes structured extraction convenient - Great fit for static and server-rendered pages
Cheerio cons
- Does not execute client-side JavaScript
- Cannot perform real browser interactions such as clicking or scrolling
- Won't help when the content only appears after frontend code runs
- For full crawling workflows, you'll need to build or bring in things like queues, retries, sessions, and concurrency management
5. JSDOM
JSDOM sits in an interesting spot between a lightweight HTML parser such as Cheerio and full browser automation with Playwright or Puppeteer.
It implements a large chunk of the web platform in Node.js, including familiar APIs such as document, querySelector(), events, cookies, and localStorage. So if you'd rather work with a browser-like DOM than jQuery-style selectors, JSDOM can feel very natural.
Just don't mistake it for a tiny headless Chrome. JSDOM emulates parts of a browser environment, but it does not contain an actual browser engine, and plenty of web APIs are missing or incomplete. Navigation is one example: changing window.location does not make JSDOM load a new page the way a real browser would.
For straightforward scraping, JSDOM can fetch a page itself with JSDOM.fromURL() and then let you inspect it through standard DOM APIs. This makes it a nice option when Cheerio feels a little too minimal but Playwright would be bringing a tank to a knife fight.
And yes, JSDOM can execute JavaScript inside a page — despite what some older comparisons claim. Script execution is disabled by default, though, and there is a very good reason for that. Enabling runScripts: "dangerously" on arbitrary pages from the internet can expose your Node.js environment to untrusted code. For general-purpose web scraping, a real browser is the much saner option when you actually need client-side JavaScript execution.
Quick start
Install JSDOM:
npm install jsdom
Here's the static Quotes to Scrape page again, this time using standard DOM APIs:
import { JSDOM } from 'jsdom';
const dom = await JSDOM.fromURL('https://quotes.toscrape.com/');
const { document } = dom.window;
const data = [...document.querySelectorAll('.quote')].map((quote) => ({
text: quote.querySelector('.text')?.textContent?.trim(),
author: quote.querySelector('.author')?.textContent?.trim(),
}));
console.log(data);
dom.window.close();
JSDOM.fromURL() fetches and parses the page, after which you're working with familiar objects such as document, elements, and NodeList.
Closing the window at the end is a good habit, especially in larger applications where JSDOM pages may create timers or event listeners that would otherwise stick around.
Make sure to check our detailed guide: Master Web Scraping With JavaScript and Node.js in 2026.
JSDOM pros
- Familiar browser-style DOM APIs
- No real browser process to launch
- Can fetch URLs directly with
JSDOM.fromURL() - Supports cookies, storage, events, and many other web APIs
- Can execute scripts when explicitly enabled
- Useful when you need more browser-like behavior than a basic HTML parser provides
JSDOM cons
- Not a full browser and does not implement every web API
- More heavyweight than simpler parsers such as Cheerio
- Page navigation and some browser behaviors are not implemented
- External resources are not loaded by default
- Executing scripts from untrusted websites with
runScripts: "dangerously"is unsafe - A poor substitute for Playwright or Puppeteer when a site genuinely depends on complex client-side JavaScript
6. Crawlee
Crawlee is what you reach for when your scraper starts becoming less of a script and more of a small crawling system.
Playwright, Puppeteer, and Cheerio are great at opening or parsing individual pages. Crawlee sits one level higher and handles the boring infrastructure around that: request queues, retries, concurrency, sessions, proxy configuration, persistent datasets, and discovering more URLs as the crawl progresses.
It also doesn't force you into one scraping engine. Crawlee provides CheerioCrawler for fast HTTP-based crawling, PlaywrightCrawler when you need a real browser, and PuppeteerCrawler if Puppeteer is your weapon of choice. The APIs are intentionally similar, so you can change the underlying approach without rebuilding your whole crawler from scratch.
That makes Crawlee especially handy once "scrape these five URLs" turns into "start here, follow these links, retry failures, don't visit the same page twice, control concurrency, and please don't eat all my RAM."
For pages that don't require JavaScript rendering, CheerioCrawler is usually the cheapest place to start. You still get Crawlee's crawling machinery, but without paying the cost of launching a browser for every job.
Quick start
Install Crawlee:
npm install crawlee
Here's a small crawler that starts on the first Quotes to Scrape page, extracts the quotes, and automatically follows the Next link until there are no more pages:
import { CheerioCrawler } from 'crawlee';
const crawler = new CheerioCrawler({
maxRequestsPerCrawl: 10,
async requestHandler({ $, request, enqueueLinks, pushData }) {
const quotes = $('.quote')
.map((_, quote) => ({
text: $(quote).find('.text').text().trim(),
author: $(quote).find('.author').text().trim(),
source: request.url,
}))
.get();
await pushData(quotes);
await enqueueLinks({
selector: '.next a',
});
},
});
await crawler.run(['https://quotes.toscrape.com/']);
The interesting part here isn't the HTML parsing — Cheerio already handles that. It's enqueueLinks(). Crawlee finds the next-page URL, adds it to its request queue, avoids processing duplicate requests, and keeps going until the queue is empty or the crawl limit is reached.
The extracted data is also written to Crawlee's default dataset, so you don't have to build your own storage plumbing just to get started.
If the target site later turns out to require JavaScript, you can switch to PlaywrightCrawler:
npm install crawlee playwright
npx playwright install chromium
Crawlee does not bundle Playwright or Puppeteer automatically, so browser-based crawlers require the corresponding package separately.
If you also work with Python, check our in-depth guide: Crawlee for Python Tutorial.
Crawlee pros
- Built-in request queues, retries, and concurrency management
- Supports HTTP-based and browser-based crawling
- Works with Cheerio, Playwright, and Puppeteer
- Automatically avoids duplicate requests
- Built-in datasets and persistent storage
- Supports sessions and proxy configuration
enqueueLinks()makes recursive crawling much easier- Can automatically adjust concurrency based on available system resources
Crawlee cons
- More abstraction and configuration than you need for a tiny one-page scraper
- Browser-based crawlers still carry the same CPU and memory costs as Playwright or Puppeteer
- Adds its own concepts such as request queues, datasets, sessions, and routers that you need to learn
- Playwright and Puppeteer must be installed separately when you use their crawlers
7. Crawler
Crawler, also known as node-crawler, is a lightweight option for crawling lots of pages without bringing a real browser along for the ride.
Think of it as the step between "I'll just fetch this one page" and a larger crawling framework such as Crawlee. Crawler gives you a request queue, configurable concurrency, retries, rate limiting, priorities, proxy support, and HTML parsing through Cheerio by default.
Unlike Playwright, Puppeteer, or Selenium, it stays firmly on the HTTP side of things. That makes it a good fit for crawling lots of static or server-rendered pages, but it won't execute client-side JavaScript or click its way through an SPA.
Crawler v2 also got a fairly substantial modernization. It is now native ESM, uses got for HTTP requests, supports HTTP/2, and requires Node.js 22 or newer. So if you remember node-crawler as some ancient CommonJS package from years ago, it's worth taking another look.
Compared with Crawlee, Crawler is a smaller and more focused tool. You don't get the same browser integrations, storage abstractions, or larger crawling framework, but you also have less machinery to learn when all you need is a queue plus HTTP requests and Cheerio.
Quick start
Install the package:
npm install crawler
Here's a small crawler that follows the pagination on Quotes to Scrape and extracts quotes from each page:
import Crawler from 'crawler';
const crawler = new Crawler({
maxConnections: 5,
skipDuplicates: true,
callback(error, response, done) {
if (error) {
console.error(error);
done();
return;
}
const $ = response.$;
$('.quote').each((_, quote) => {
console.log({
text: $(quote).find('.text').text().trim(),
author: $(quote).find('.author').text().trim(),
});
});
const next = $('.next a').attr('href');
if (next) {
crawler.add(new URL(next, response.options.url).href);
}
done();
},
});
crawler.add('https://quotes.toscrape.com/');
Crawler fetches the page over HTTP, passes the response through Cheerio, and gives you the resulting $ object directly. When we find the next-page link, we simply add it back to the queue and let Crawler keep going.
maxConnections controls how many requests can run at once, while skipDuplicates prevents the same URL from being queued repeatedly.
You can also slow a crawler down with rateLimit, configure retries, assign request priorities, rotate proxies, or enable HTTP/2 when you need more control over how the crawl runs.
Crawler pros
- Built-in request queue and concurrency control
- Retries and rate limiting out of the box
- Cheerio parsing enabled by default
- Supports request priorities and duplicate skipping
- Proxy support and HTTP/2 support
- Much lighter than browser-based crawling
- Modern ESM package with TypeScript support
Crawler cons
- Requires Node.js 22 or newer
- Native ESM only in the current stable v2 release
- Does not execute client-side JavaScript
- No built-in browser automation
- Less full-featured than Crawlee for complex crawling workflows
- You have to discover and enqueue links yourself
8. htmlparser2
htmlparser2 is for those moments when you care less about a cozy jQuery-style API and more about parsing HTML fast and with as little ceremony as possible.
At its core, htmlparser2 is a low-level HTML and XML parser. You can feed it a complete document, process input as a stream, react to individual tags and text nodes through callbacks, or build a DOM tree when that's more convenient.
That makes it a good fit for large HTML documents, feeds, streaming input, and custom extraction pipelines where loading everything into a browser — or even building a full DOM — would be unnecessary.
Compared with Cheerio, htmlparser2 sits closer to the metal. Cheerio gives you a much friendlier jQuery-style interface; htmlparser2 gives you more direct control over how the document is processed.
Current versions also include parseDocument() for building a DOM and re-export DomUtils for traversing it, so you don't have to wire up DomHandler manually for ordinary use cases.
Quick start
Install htmlparser2:
npm install htmlparser2
Here's a small example: stream a page directly from fetch() and collect every external link without first loading the whole response into memory:
import { WebWritableStream } from 'htmlparser2/WebWritableStream';
const url = 'https://example.com';
const baseUrl = new URL(url);
const links = new Set();
const parser = new WebWritableStream({
onopentag(name, attributes) {
if (name !== 'a' || !attributes.href) {
return;
}
try {
const link = new URL(attributes.href, baseUrl);
if (
link.protocol === 'http:' ||
link.protocol === 'https:'
) {
links.add(link.href);
}
} catch {
// Ignore malformed URLs.
}
},
});
const response = await fetch(url);
if (!response.ok || !response.body) {
throw new Error(`Request failed: ${response.status}`);
}
await response.body.pipeTo(parser);
console.log([...links]);
The important bit is response.body.pipeTo(parser). htmlparser2 starts processing the response as chunks arrive instead of waiting for the entire document to download and then building a DOM tree.
That can be useful when you're dealing with very large pages or only care about a small piece of the document, such as links, image URLs, metadata, or specific tags.
If you do want a regular DOM instead, parseDocument() is still there:
import { parseDocument, DomUtils } from 'htmlparser2';
const response = await fetch('https://example.com');
const html = await response.text();
const document = parseDocument(html);
const [heading] = DomUtils.getElementsByTagName('h1', document);
console.log(DomUtils.textContent(heading));
htmlparser2 pros
- Very fast HTML and XML parsing
- Can process
fetch()responses as a stream - Does not require building a full DOM
- Low-level callback API gives you precise control
parseDocument()is available when you do want a DOMDomUtilsprovides built-in traversal helpers- Can also parse RSS, Atom, and RDF feeds
htmlparser2 cons
- Lower-level API than Cheerio or JSDOM
- No jQuery-style interface built in
- CSS selector support requires a separate package such as
css-select - Does not execute JavaScript or behave like a browser
- Prioritizes speed over strict HTML specification compliance
9. Native Node.js fetch
Sometimes the best scraping dependency is no dependency at all.
Modern Node.js ships with a built-in fetch(), so if all you need is to download HTML, call an API, or grab some JSON, you can often skip Axios, Got, and other HTTP clients entirely.
Node's fetch() uses the familiar Web API and is powered internally by Undici. That means the same basic code you would write in a browser also works in Node.js:
const response = await fetch('https://example.com');
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const html = await response.text();
console.log(html);
Of course, fetch() only gets you the response. It does not parse HTML, follow links as a crawler, or execute client-side JavaScript.
For static scraping, that's often exactly what you want. Pair it with Cheerio, JSDOM, or htmlparser2 and you have a lightweight scraping setup without launching a browser.
For example:
import * as cheerio from 'cheerio';
const response = await fetch('https://quotes.toscrape.com/');
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const html = await response.text();
const $ = cheerio.load(html);
const quotes = $('.quote')
.map((_, quote) => ({
text: $(quote).find('.text').text().trim(),
author: $(quote).find('.author').text().trim(),
}))
.get();
console.log(quotes);
This is about as simple as static scraping gets: fetch the HTML, parse it, extract the data, go home.
Node's built-in fetch() is backed by Undici, but installing Undici separately still makes sense when you need lower-level APIs, more control over connection pooling, or features that aren't exposed through the bundled version.
Learn more in our guide: Web Scraping with node-fetch.
Native fetch pros
- Built into modern Node.js
- No extra dependency required
- Familiar Web API
- Good fit for HTML, JSON, and API requests
- Works nicely with Cheerio, JSDOM, and htmlparser2
- Supports streaming response bodies
Native fetch cons
- Does not parse HTML
- Does not execute JavaScript
- No crawling features such as queues or automatic link discovery
- Less low-level control than using Undici directly
- Not enough on its own for browser-only or heavily interactive websites
10. parse5
parse5 is the parser you reach for when you want HTML interpreted the way a browser would interpret it — including all the weird, malformed markup the web has accumulated over the years.
It implements the WHATWG HTML standard and focuses on spec-compliant parsing rather than giving you a friendly scraping API. In fact, parse5 deliberately feels lower-level than Cheerio or JSDOM: there is no built-in querySelector() or jQuery-style $() waiting for you.
So why bother?
Because real-world HTML is frequently broken. Tags are left open, elements appear where they technically shouldn't, tables are malformed, and browsers quietly fix the mess while rendering the page. parse5 applies the same HTML parsing rules instead of treating the document like generic XML with angle brackets.
This is also why parse5 appears under the hood of projects such as JSDOM, Cheerio, Angular, Lit, and rehype. It's less of an all-in-one scraper and more of a very reliable HTML foundation.
Compared with htmlparser2, the priorities are slightly different. htmlparser2 focuses heavily on speed and low-level streaming, while parse5 puts standards compliance front and center.
Quick start
Install parse5:
npm install parse5
Let's give it some deliberately questionable HTML:
import { parse, serialize } from 'parse5';
const html = `
<!doctype html>
<title>Definitely valid HTML</title>
<p>First paragraph
<div>Well, this div shouldn't really be inside that paragraph.</div>
<table>
<tr>
<td>One
<td>Two
</table>
`;
const document = parse(html);
console.log(serialize(document));
The input is missing several closing tags and contains markup that a browser needs to repair while parsing.
parse5 doesn't just preserve that mess literally. It runs it through the HTML parsing algorithm and produces a proper document tree, much like a browser would. Serializing the result gives you normalized HTML with the implied elements and closing tags restored.
That behavior matters when your scraper has to deal with HTML from the real web rather than carefully handcrafted demo pages.
For actual extraction, you can walk the generated tree yourself or pair parse5 with higher-level tooling. If all you want is convenient CSS selectors, Cheerio will usually make your life easier.
parse5 also supports useful lower-level features such as parsing document fragments, serializing trees back to HTML, custom tree adapters, and optional source-location information when you need to know exactly where an element came from in the original document.
parse5 pros
- Follows the WHATWG HTML parsing specification
- Handles malformed HTML much like a browser would
- Parses complete documents and HTML fragments
- Can serialize parsed trees back to HTML
- Can preserve source-location information
- Used as parsing infrastructure by several major JavaScript projects
- No browser process required
parse5 cons
- No built-in CSS selector API
- Lower-level than Cheerio or JSDOM for everyday data extraction
- Does not execute JavaScript
- Does not fetch pages or provide crawling features
- Usually unnecessary if you just need a convenient scraper for ordinary static HTML
ScrapingBee: when you don't want to run the scraping stack yourself
Everything we've covered so far runs on your side. You install the parser, launch the browser, manage concurrency, configure proxies, and deal with whatever the target website throws at you.
ScrapingBee takes a different approach. It's a hosted scraping API: you send it a URL, and ScrapingBee handles things such as headless browsers, proxy rotation, geolocation, and browser infrastructure on its servers.
That can be especially useful once running Playwright yourself stops being the interesting part of the project and starts being an infrastructure problem.
You can ask ScrapingBee to render JavaScript, interact with the page, use premium or stealth proxies, return screenshots, or extract structured data before the response even reaches your application. For simpler pages, you can also disable JavaScript rendering and use it as a regular HTTP scraper.
Quick start
You don't need an SDK to use ScrapingBee. The API works perfectly well with Node.js's built-in fetch().
Here's the same JavaScript-rendered Quotes to Scrape page we used with Playwright and Puppeteer:
const params = new URLSearchParams({
url: 'https://quotes.toscrape.com/js/',
render_js: 'true',
extract_rules: JSON.stringify({
texts: {
selector: '.quote .text',
type: 'list',
},
authors: {
selector: '.quote .author',
type: 'list',
},
}),
});
const response = await fetch(
`https://app.scrapingbee.com/api/v1/?${params}`,
{
headers: {
Authorization: `Bearer ${process.env.SCRAPINGBEE_API_KEY}`,
},
},
);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const data = await response.json();
console.log(data);
Compare that with the Playwright version from earlier. There is no Chromium installation, no browser process, no page lifecycle, and no selector waiting on your side.
render_js tells ScrapingBee to render the page in a browser, while extract_rules applies CSS or XPath selectors remotely and returns structured JSON instead of making you parse the resulting HTML yourself.
Of course, you can still request the rendered HTML and run Cheerio, JSDOM, or another parser locally if that's a better fit for your application.
What else can ScrapingBee handle?
ScrapingBee's HTML API includes quite a bit more than remote JavaScript rendering:
- Rotating proxies: use classic, premium, or stealth proxy pools, with geolocation when needed.
- Auto-Mode: let ScrapingBee try increasingly capable configurations and stop at the cheapest one that succeeds.
- JavaScript scenarios: click elements, fill inputs, scroll, wait for content, or execute custom JavaScript before returning the page.
- Structured extraction: extract data with CSS or XPath selectors without parsing the HTML yourself.
- AI extraction: describe the information you want in natural language or define an AI extraction schema.
- Screenshots: capture the viewport, the full page, or a particular element.
- Sessions and cookies: reuse the same IP across related requests and pass cookies explicitly when you need to preserve application state.
- Alternative output formats: return rendered HTML, original page source, plain text, Markdown, or JSON depending on what you're building.
So ScrapingBee isn't really a replacement for Cheerio or Playwright in the same sense that those tools compete with one another. It's another deployment model: instead of owning the browser and proxy infrastructure yourself, you call an API and let the service run it for you.
Try ScrapingBee
You can sign up for a free ScrapingBee trial with 1,000 API credits and no credit card required.
Keep in mind that these are credits, not necessarily 1,000 requests. The cost of a request depends on the features you use — JavaScript rendering, premium or stealth proxies, and AI extraction can consume additional credits.
Introduction to Web Scraping
At its simplest, web scraping means requesting a web page and extracting the data you need from the response. The interesting part is that "web page" can mean very different things in practice.
Sometimes the server sends you complete HTML containing everything you want. That's the easy case: fetch the page, parse the markup with something like Cheerio or htmlparser2, extract the data, done.
Other sites send little more than an empty shell and let JavaScript fetch the actual content after the page loads. In that case, you have three main options: call the underlying API directly if you can identify it, reproduce the necessary HTTP requests yourself, or use a browser automation tool such as Playwright, Puppeteer, or Selenium and let the page run normally.
There is also a difference between scraping and crawling. Scraping is mostly about extracting data from a page; crawling is about discovering and processing many pages, which introduces things like queues, retries, concurrency limits, duplicate URLs, sessions, and rate limiting. That's where tools such as Crawlee or Crawler start making more sense than a standalone parser.
So before picking a library, answer one question first: what is the cheapest way to get the data you need?
If it's already in the HTML, don't launch Chrome for fun. If it's available through an API, you may not need to parse HTML at all. And if the site genuinely requires browser behavior, that's when the heavier browser automation tools earn their keep.
Key Features of Web Scraping Libraries
There isn't one checklist of features that every scraping library needs. A good HTML parser and a good browser automation tool solve different problems, so judging them by the same criteria doesn't make much sense.
What matters is whether the tool gives you the features required by your particular scraping job.
For static HTML, look for fast parsing, convenient selectors, low memory usage, and support for streams or large documents if necessary. Cheerio, htmlparser2, and parse5 all live in this part of the toolbox, but with different priorities.
For dynamic websites, browser automation becomes more important. Useful features include reliable element waiting, JavaScript execution, browser contexts or sessions, network interception, and the ability to interact with forms, scrolling, and other UI elements.
For crawling many pages, the important features change again. Request queues, retries, concurrency limits, duplicate detection, rate limiting, session management, and persistent storage can save you from rebuilding the same infrastructure yourself.
At larger scales, you may also care about proxy support, geolocation, observability, and how easily the scraper can run across multiple machines.
The key is not to find the library with the longest feature list. It's to avoid paying for complexity you don't need while making sure the boring parts of your particular scraping workflow are already handled.
Scraping Library Best Practices
Picking the right library is only half the job. A scraper that works perfectly on ten pages can still fall apart once you point it at ten thousand.
Start with the simplest approach that works. If the data is available in HTML or through an API, use HTTP requests and a parser instead of launching a browser. Browser automation is powerful, but it is also slower, heavier, and more expensive to scale.
A few other habits will save you plenty of pain:
- Limit concurrency. Sending hundreds of requests at once can overload both your machine and the target website.
- Use retries carefully. Retry temporary failures such as timeouts or
5xxresponses, but don't blindly retry every error forever. - Respect rate limits. Slow down when necessary and pay attention to
429 Too Many Requestsresponses andRetry-Afterheaders. - Reuse sessions and connections. Creating a fresh browser, TCP connection, or login session for every request wastes resources.
- Set sensible timeouts. A broken page should not be able to stall an entire crawl indefinitely.
- Expect the HTML to change. Keep selectors reasonably specific, validate extracted data, and log failures so broken scrapers are easy to diagnose.
- Store progress for larger crawls. Request queues and persistent state make it much easier to resume after crashes instead of starting from zero.
- Don't assume a browser solves blocking. Headless Chrome can still be detected and blocked. Proxy infrastructure, request behavior, sessions, and target-specific restrictions may matter too.
Finally, test your scraper against realistic workloads before calling it done. The interesting bugs usually appear after the first few hundred requests, not on the carefully chosen example page.
Conclusion
There is no single best JavaScript web scraping library. Use a lightweight parser for static HTML, a browser automation tool for JavaScript-heavy pages, and a crawling framework when you need queues, retries, and concurrency.
If you'd rather skip browser setup, proxy management, and scraping infrastructure altogether, try ScrapingBee and handle it through an API instead.
FAQ
What is the best JavaScript web scraping library in 2026?
There is no single best option for every project. Playwright is a strong choice for JavaScript-heavy websites, while Cheerio is better for fast static HTML parsing. For larger crawls, Crawlee provides queues, retries, sessions, and concurrency management.
What is the best JavaScript web scraping library for dynamic websites?
For websites that require JavaScript rendering or user interaction, Playwright, Puppeteer, and Selenium WebDriver are the main options. Playwright is often the easiest starting point for a new Node.js project because of its modern API and built-in browser automation features.
What is the best JavaScript web scraping library for static websites?
For static or server-rendered pages, Cheerio is usually the simplest choice. You can combine it with Node.js fetch() to download HTML and extract data without launching a browser.
Is Playwright better than Selenium for web scraping?
Not always. Playwright often feels more convenient in modern JavaScript projects, but Selenium supports a broad range of browsers, has a mature ecosystem, and can perform many of the same scraping tasks. The better choice depends on your browser requirements, existing stack, and workflow.
Can I scrape websites in Node.js without using a headless browser?
Yes. If the data is already available in the HTML or through an API, you can use Node.js fetch() together with libraries such as Cheerio, htmlparser2, JSDOM, or parse5. This is usually faster and uses fewer resources than running Playwright or Puppeteer.

Ilya is an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, tweets, goes in for sports and plays music.


