cheerio、domino、jsdom 和 puppeteer 都是 Node.js 生态中用于处理 HTML 和模拟浏览器环境的核心工具,但它们的实现原理和适用场景截然不同。cheerio 是一个轻量级的 HTML 解析器,它实现了 jQuery 的核心子集,专注于快速提取数据,但不具备完整的浏览器环境。domino 是一个极轻量的 DOM 实现,旨在提供标准的 DOM API 而不依赖浏览器,适合简单的服务端渲染或 DOM 操作。jsdom 则是一个功能更全面的纯 JavaScript 浏览器环境实现,支持大部分 Web 标准,常用于测试和复杂的服务端渲染。puppeteer 与众不同,它是一个通过 DevTools 协议控制真实 Headless Chrome 浏览器的库,能够执行 JavaScript、处理动态内容、截图和生成 PDF,提供最真实的浏览器行为模拟。
在 Node.js 开发中,处理 HTML 内容或模拟浏览器行为是常见需求。无论是编写网络爬虫、进行服务端渲染(SSR),还是构建自动化测试脚本,开发者都需要在 cheerio、domino、jsdom 和 puppeteer 之间做出选择。这四个工具虽然目标相似,但底层架构和适用场景有着本质区别。本文将从技术原理、API 表现和实际工程场景出发,深入剖析它们的差异。
理解这些工具的第一步是认清它们的“内核”。
cheerio 并不是一个浏览器。它只是一个快速的 HTML/XML 解析器。它将 HTML 字符串加载到内存中,构建一个类似 DOM 的树结构,并提供了类似 jQuery 的 API 供你操作。它不执行任何 JavaScript,也不关心 CSS 渲染。
// cheerio: 仅解析静态 HTML 字符串
import * as cheerio from 'cheerio';
const html = '<ul><li class="item">Hello</li></ul>';
const $ = cheerio.load(html);
// 直接操作 DOM 树,无浏览器环境
$('.item').text('Hello World');
console.log($.html());
// 输出: <ul><li class="item">Hello World</li></ul>
domino 是一个纯 JavaScript 实现的 DOM。它试图在 Node.js 中重现浏览器的 document 和 window 对象的核心部分。它比 cheerio 更接近标准 DOM API,但依然不执行 JavaScript,也不加载外部资源(如图片、CSS)。
// domino: 创建轻量级 DOM 环境
import domino from 'domino';
const html = '<div id="app">Initial</div>';
const window = domino.createWindow(html);
const document = window.document;
// 使用标准 DOM API
const app = document.getElementById('app');
app.textContent = 'Updated by Domino';
console.log(document.documentElement.outerHTML);
jsdom 是一个更宏大的项目,旨在在 Node.js 中实现 WHATWG DOM 和 HTML 标准。它不仅提供 DOM,还模拟了事件系统、XMLHttpRequest 甚至部分 CSS 解析能力。它常用于模拟浏览器环境以运行前端代码,但默认情况下它不执行页面中的 <script> 标签(需配置 runScripts: "dangerously" 且依然受限)。
// jsdom: 模拟完整的浏览器环境
import { JSDOM } from 'jsdom';
const dom = new JSDOM(`<!DOCTYPE html><body><div id="root"></div></body>`);
const { window } = dom;
const { document } = window;
// 模拟更完整的环境,支持事件等
document.getElementById('root').innerHTML = '<p>JS DOM Ready</p>';
console.log(window.document.body.innerHTML);
puppeteer 是唯一一个控制真实浏览器(Chrome/Chromium)的工具。它通过 DevTools 协议启动一个无头(Headless)浏览器实例。这意味着它能执行所有 JavaScript、加载所有资源、处理 CSS 布局,甚至模拟网络状况和设备参数。
// puppeteer: 控制真实 Chrome 浏览器
import puppeteer from 'puppeteer';
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 访问真实网站,执行所有 JS
await page.goto('https://example.com');
// 在浏览器上下文中执行代码
const title = await page.title();
console.log(title);
await browser.close();
})();
性能是选择这些库时的关键考量因素,尤其是在高并发或服务器资源受限的场景下。
cheerio 的性能是碾压级的。因为它只是解析字符串,不涉及布局计算或脚本执行,处理大型 HTML 文档通常只需几毫秒,内存占用极低。它是批量抓取静态数据的最佳选择。
domino 和 jsdom 处于中间地带。domino 非常轻快,适合简单的 DOM 操作。jsdom 由于模拟了大量 Web API,初始化和运行开销比 domino 大,但在纯 Node 环境中仍远快于启动真实浏览器。
puppeteer 是最重的。启动一个 Chrome 实例需要数百毫秒甚至数秒,每个实例消耗数十 MB 甚至上百 MB 的内存。在高并发场景下,必须严格控制实例数量或使用集群管理,否则极易拖垮服务器。
这是区分“解析器”和“自动化工具”的分水岭。
如果目标网站的内容是通过 JavaScript 动态生成的(例如 React、Vue 构建的单页应用),cheerio、domino 和默认配置的 jsdom 都将失效,因为它们看到的只是初始的空白 HTML 骨架。
cheerio / domino / jsdom (默认):只能看到服务器返回的原始 HTML。
// 假设目标页面通过 JS 渲染列表
// 使用 cheerio 只能拿到空容器
const $ = cheerio.load('<div id="app"></div>');
console.log($('#app').html()); // 输出: null 或空字符串
puppeteer:可以等待 JavaScript 执行完毕,获取渲染后的完整 DOM。
// puppeteer 等待网络空闲或特定选择器出现
await page.goto('https://dynamic-site.com');
await page.waitForSelector('.loaded-content'); // 等待 JS 渲染完成
const content = await page.content(); // 此时包含 JS 生成的内容
console.log(content);
注:jsdom 可以通过配置 runScripts: "dangerously" 尝试执行脚本,但对于依赖现代浏览器 API(如 IntersectionObserver 或复杂 CSSOM)的应用,支持往往不完整且不稳定。
不同的 API 设计影响了代码的可读性和迁移成本。
cheerio 采用了经典的 jQuery 链式调用风格。对于有前端背景的开发者来说,上手几乎零成本。
// cheerio: jQuery 风格
$('ul li').first().addClass('active').text('First Item');
domino 和 jsdom 提供了标准的 Web DOM API。如果你习惯使用 document.querySelector、element.addEventListener 等原生方法,这两个库非常自然。
// jsdom/domino: 标准 DOM API
const el = document.querySelector('ul li');
el.classList.add('active');
el.textContent = 'First Item';
puppeteer 提供了一套独特的异步 API,专注于浏览器控制。它引入了 Page、Browser、ElementHandle 等概念,操作元素时需要通过句柄(Handle)进行,且几乎所有操作都是异步的。
// puppeteer: 浏览器控制 API
const element = await page.$('ul li');
await element.evaluate((el) => {
el.classList.add('active');
el.textContent = 'First Item';
});
你需要抓取成千上万个新闻页面的标题和正文,这些页面是服务端渲染的(SSR)。
cheerio你需要测试一个前端组件的逻辑,该组件依赖 document 和 window 对象,但不需要真实的渲染。
jsdom你在 Node.js 中间件中需要微调 HTML 结构(如注入脚本标签),且希望比 cheerio 更符合标准 DOM 规范。
dominojsdom 显得过于笨重,domino 是很好的替代品。(注意:需确认项目维护状态是否满足长期需求)目标网站是 React/Vue 应用,内容完全由客户端 JS 生成,或者你需要模拟用户登录、点击按钮并截图。
puppeteer| 特性 | cheerio | domino | jsdom | puppeteer |
|---|---|---|---|---|
| 本质 | HTML 解析器 (jQuery 子集) | 轻量级 DOM 实现 | 完整浏览器环境模拟 | 真实浏览器控制器 (Chrome) |
| 执行 JS | ❌ 否 | ❌ 否 | ⚠️ 有限支持 (需配置) | ✅ 是 (完整支持) |
| CSS 渲染 | ❌ 否 | ❌ 否 | ⚠️ 部分支持 | ✅ 是 (真实渲染) |
| 速度 | 🚀 极快 | ⚡ 快 | 🐢 中等 | 🐌 慢 (启动开销大) |
| 内存占用 | 极低 | 低 | 中 | 高 |
| API 风格 | jQuery 链式调用 | 标准 DOM API | 标准 DOM API | 异步浏览器控制 API |
| 最佳场景 | 静态爬虫、数据提取 | 简单 DOM 操作、轻量 SSR | 前端单元测试、复杂 SSR | 动态爬虫、E2E 测试、截图 |
在选择技术方案时,请遵循“够用即可”的原则,避免过度设计。
cheerio。它的效率和稳定性远超任何浏览器模拟方案。jsdom 是单元测试的黄金标准,它在速度和功能之间取得了最佳平衡。jsdom 去“修补”或模拟,直接使用 puppeteer。虽然它重,但它能保证结果的准确性和一致性,减少因环境模拟不全导致的诡异 Bug。domino 等轻量库虽然好用,但需关注其社区活跃度。对于关键业务,若发现库已停止维护,应考虑迁移至 jsdom 或 cheerio 等更活跃的项目。最终,这四个工具并非互斥,而是互补的。一个成熟的 Node.js 架构往往会混合使用它们:用 cheerio 处理大部分静态抓取任务,用 puppeteer 专门攻克动态难点,用 jsdom 保障代码质量。理解它们的边界,才能在工程中游刃有余。
当你只需要从静态 HTML 中提取数据(如爬虫抓取、SEO 分析)且对性能极其敏感时,选择 cheerio。它的启动速度最快,内存占用最低,API 熟悉(类 jQuery),但它无法执行 JavaScript,也不能处理动态渲染的内容。如果目标页面依赖 JS 生成内容,此方案无效。
如果你需要在 Node.js 中拥有一个符合标准的轻量级 DOM 对象来进行简单的操作或测试,且不需要加载外部资源或执行复杂脚本,domino 是极佳选择。它比 jsdom 更轻快,适合那些只需要 DOM API 而不需要完整浏览器模拟场景(如简单的服务端 DOM 操纵)。注意:该项目维护频率较低,复杂场景需谨慎。
当你的应用需要在 Node.js 中模拟一个接近真实的浏览器环境以运行依赖于 DOM 的代码(如单元测试、复杂的服务端渲染),但不需要真实的渲染引擎或处理复杂的 CSS 布局时,应选择 jsdom。它提供了比 domino 更完整的 Web 标准支持(如事件、XMLHttpRequest),是测试框架(如 Jest)的默认底层实现,但在处理极度复杂的现代前端应用时可能仍有兼容性缺口。
如果你的任务涉及执行 JavaScript、处理单页应用(SPA)、需要截图、生成 PDF、或者需要模拟真实的用户交互(点击、输入、滚动),必须选择 puppeteer。它是唯一能运行真实浏览器内核的方案,能解决所有动态渲染问题,但代价是启动慢、资源消耗大。对于需要高保真度浏览器行为的自动化测试或爬虫,这是不可替代的选择。
import * as cheerio from 'cheerio';
const $ = cheerio.load('<h2 class="title">Hello world</h2>');
$('h2.title').text('Hello there!');
$('h2').addClass('welcome');
$.html();
//=> <html><head></head><body><h2 class="title welcome">Hello there!</h2></body></html>
Install Cheerio using a package manager like npm, yarn, or bun.
npm install cheerio
# or
bun add cheerio
❤ Proven syntax: Cheerio implements a subset of core jQuery. Cheerio removes all the DOM inconsistencies and browser cruft from the jQuery library, revealing its truly gorgeous API.
ϟ Blazingly fast: Cheerio works with a very simple, consistent DOM model. As a result parsing, manipulating, and rendering are incredibly efficient.
❁ Incredibly flexible: Cheerio wraps around parse5 for parsing HTML and can optionally use the forgiving htmlparser2. Cheerio can parse nearly any HTML or XML document. Cheerio works in both browser and server environments.
First you need to load in the HTML. This step in jQuery is implicit, since jQuery operates on the one, baked-in DOM. With Cheerio, we need to pass in the HTML document.
// ESM or TypeScript:
import * as cheerio from 'cheerio';
// In other environments:
const cheerio = require('cheerio');
const $ = cheerio.load('<ul id="fruits">...</ul>');
$.html();
//=> <html><head></head><body><ul id="fruits">...</ul></body></html>
Once you've loaded the HTML, you can use jQuery-style selectors to find elements within the document.
selector searches within the context scope which searches within the root
scope. selector and context can be a string expression, DOM Element, array
of DOM elements, or cheerio object. root, if provided, is typically the HTML
document string.
This selector method is the starting point for traversing and manipulating the document. Like in jQuery, it's the primary method for selecting elements in the document.
$('.apple', '#fruits').text();
//=> Apple
$('ul .pear').attr('class');
//=> pear
$('li[class=orange]').html();
//=> Orange
When you're ready to render the document, you can call the html method on the
"root" selection:
$.root().html();
//=> <html>
// <head></head>
// <body>
// <ul id="fruits">
// <li class="apple">Apple</li>
// <li class="orange">Orange</li>
// <li class="pear">Pear</li>
// </ul>
// </body>
// </html>
If you want to render the
outerHTML
of a selection, you can use the outerHTML prop:
$('.pear').prop('outerHTML');
//=> <li class="pear">Pear</li>
You may also render the text content of a Cheerio object using the text
method:
const $ = cheerio.load('This is <em>content</em>.');
$('body').text();
//=> This is content.
Cheerio collections are made up of objects that bear some resemblance to browser-based DOM nodes. You can expect them to define the following properties:
tagNameparentNodepreviousSiblingnextSiblingnodeValuefirstChildchildNodeslastChildThis video tutorial is a follow-up to Nettut's "How to Scrape Web Pages with Node.js and jQuery", using cheerio instead of JSDOM + jQuery. This video shows how easy it is to use cheerio and how much faster cheerio is than JSDOM + jQuery.
Are you using cheerio in production? Add it to the wiki!
Does your company use Cheerio in production? Please consider sponsoring this project! Your help will allow maintainers to dedicate more time and resources to its development and support.
Headlining Sponsors
Other Sponsors
Become a backer to show your support for Cheerio and help us maintain and improve this open source project.
MIT