Cheerio vs Puppeteer: HTML parser or real browser?
Compare parsing received HTML with executing client-rendered content, using a complete inline example and a practical extraction checklist.
Cheerio parses HTML you already obtained. Puppeteer controls a real browser that can execute JavaScript and interact with a page. Choose based on where the required data is produced, not an unsupported “fastest scraper” ranking.
What each tool sees
Cheerio parses and changes HTML, but does not execute page scripts, load visual assets or reproduce browser navigation and sessions.
Puppeteer can wait for client rendering and inspect the final DOM, but brings browser installation, resources, state and security responsibilities. Use those capabilities when the task actually needs them.
Compare the tools step by step
Install the dependencies in your project:
npm install cheerio puppeteer
Both tools will receive the same inline HTML. First read it with Cheerio, then run it in a browser and wait for the change. The complete script below combines the steps and handles browser cleanup.
1. Prepare HTML that changes after loading
Import both libraries and define the shared page. Its result starts as Loading and becomes Ready only if a browser executes the script.
import { load } from "cheerio";
import puppeteer from "puppeteer";
const html = `
<p id="result">Loading</p>
<script>
setTimeout(() => {
const result =
document.querySelector("#result");
result.textContent = "Ready";
}, 100);
</script>
`;
2. Read the initial HTML with Cheerio
Cheerio parses the text it receives without running page JavaScript. It correctly finds the initial Loading value.
console.log(load(html)("#result").text()); // Loading
3. Run the page in Puppeteer and wait for Ready
The browser runs the script. Wait for the changed value, extract its text, and close the browser even if the operation fails.
const browser = await puppeteer.launch({
headless: true,
});
try {
const page = await browser.newPage();
await page.setContent(html);
await page.waitForFunction(
() => {
const result =
document.querySelector("#result");
return result?.textContent === "Ready";
},
{ timeout: 5000 },
);
const result = await page.$eval(
"#result",
(element) => element.textContent,
);
console.log(result); // Ready
} finally {
await browser.close();
}
Complete script
Here are all the pieces together, including setup and cleanup. Save this as compare.mjs and run node compare.mjs.
import { load } from "cheerio";
import puppeteer from "puppeteer";
const html = `
<p id="result">Loading</p>
<script>
setTimeout(() => {
const result =
document.querySelector("#result");
result.textContent = "Ready";
}, 100);
</script>
`;
console.log(load(html)("#result").text()); // Loading
const browser = await puppeteer.launch({
headless: true,
});
try {
const page = await browser.newPage();
await page.setContent(html);
await page.waitForFunction(
() => {
const result =
document.querySelector("#result");
return result?.textContent === "Ready";
},
{ timeout: 5000 },
);
const result = await page.$eval(
"#result",
(element) => element.textContent,
);
console.log(result); // Ready
} finally {
await browser.close();
}
Cheerio correctly reads the initial HTML. The browser executes the script, then Puppeteer waits for the changed result. This demonstrates behavior, not speed or memory usage.
Choose the required data source
If an authorized API/HTML response already contains the records, obtain that contract directly and validate status/schema/completeness. For example, before passing fetched text to Cheerio:
const response = await fetch("https://example.com");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const html = await response.text();
const heading = load(html)("h1").text();
For large or untrusted responses, enforce time/size limits too. If a click or client script produces the required data and no appropriate direct contract exists, use a browser with stable selectors and meaningful waits.
An initial empty shell does not mean Cheerio “missed” a selector; the server may not have sent the final records. Conversely, a browser adds operational complexity to a fully static parse without necessarily improving it.
Combining the tools
You can parse await page.content() with Cheerio, but it is an HTML snapshot: it does not preserve live browser objects, events, canvas pixels or every shadow-root detail. Check whether it represents the needed records.
Extract plain records directly with $eval/$$eval when that is clearer. Do not pass browser handles into Cheerio. See dynamic scraping.
Sessions and operating limits
Cheerio is not a session/authentication manager; the HTTP client must obtain authorized content. Puppeteer can carry session state, but that does not justify sharing a logged-in personal profile or bypassing access rules. Scope cookies/tokens to the intended origin.
HTTP status, deadlines, response limits and schema validation matter with either path. Browser jobs also need compatible binaries, bounded concurrency, cleanup and the sandbox.
Use Cheerio for received HTML and Puppeteer for required browser execution. Measure representative tasks if performance matters; do not infer universal rankings from this small example.