cheerio 在 Node 中处理 10 MB 页面耗时 2.9 秒

最后更新于 August 17, 2026
cheerio 在 Node 中处理 10 MB 页面耗时 2.9 秒
AI 摘要
在一台机器上,Node 里的 cheerio 用 2,927.89 ms 从一份 10 MB 的合成 HTML 文档中解析并提取了标题和 href。运行在 CPython 上、带 C 后端解析器的 selectolax 用 158 ms 完成了同样的字段提取。排序后的标题文本和 href 哈希一致。这是一场跨运行时的端到端技术栈对比,不是单纯对解析算法下结论。若页面只有 10 KB,差距大约是 2 倍,几乎没人会在意。真正的问题是:你的页面落在这条曲线的哪个位置。若你在 Node 环境里,jQuery 风格 API 很重要,而且代表性页面规模接近测试中的 1 MB 以内范围,那么可以使用 cheerio。

在一台机器上,Node 里的 cheerio 用 2,927.89 ms 从一份 10 MB 的合成 HTML 文档中解析并提取了标题和 href。与此同时,运行在 CPython 上、并使用 C 语言后端解析器的 selectolax 完成同样的字段提取只用了 158 ms。排序后的标题文本和 href 哈希完全一致。这是一场跨运行时的端到端技术栈对比,不是单纯对某个解析算法下结论。

如果页面只有 10 KB,这个差距大概也就 2 倍,几乎没人会在意。真正的问题是:你的页面落在这条曲线的哪个位置。

什么是 cheerio

cheerio 是 Node 生态里使用 jQuery 风格语法的 HTML 解析器,而且它之所以成为这个生态中的默认答案是有原因的:30,449 个 GitHub stars、MIT 许可证,以及我开始测试的前一天仓库还有新的提交。测试版本:1.2.0

官方参考:Cheerio 官方介绍

import * as cheerio from "cheerio";
const $ = cheerio.load(html);
const titles = $("h3.title").map((_, e) => $(e).text()).get();
const hrefs = $("a").map((_, e) => $(e).attr("href")).get();

如果你写过 jQuery,上手这个 API 几乎没有门槛。也正是这种熟悉感,让它赢得了这个生态。

不过它的底层并不是单一解析器,而是一整套组件:解析使用 htmlparser2parse5,DOM 树由 domhandlerdomutils 处理,选择器依赖 cheerio-select,再加上 undiciencoding-sniffer 等等——11 个直接依赖,最终展开成 22 个顶层包,占用 9.0 MiB 磁盘空间。这些包提供了解析和编码相关能力,同时也带来了依赖体量。本次评测测试了异常 HTML 的输出,但没有测试编码正确性,也没有把两个解析后端分别隔离出来比较。

测量方法,以及为什么它可信

这套研究基线本来就已经有一套解析器基准测试:从 1 KB 到 10 MB 的五种页面尺寸、50 次迭代、3 轮独立运行,以及最关键的一点——内容哈希校验门槛,会把提取结果(排序后的标题和排序后的 href)与参考解析器进行比对。一个偷偷漏做工作的解析器不可能拿到漂亮的速度结果。

把 cheerio 加进去之前,必须先过两个检查。

参考结果是否仍然落在原来的位置? selectolax 在同一会话、同一测试样本上重新跑了一遍。它的内容哈希在 5/5 个尺寸上都复现成功,p50 结果与已发布数据相比落在 0.989× 到 1.079× 之间。所以,这就是那台生成原始表格的机器。

cheerio 是否提取出了相同的评分字段? 它在 Node 中按完全相同的规则计算内容哈希——对排序后的标题文本和排序后的 href 做 SHA-256——结果在 5/5 个尺寸上都与参考值一致。这证明了这些样本上排序字段的一致性,但并不能证明 DOM 形状、文档顺序、属性、文本规范化或错误恢复机制完全相同。

只有在这之后,时间数据才真正有意义。

页面大小selectolaxlxmlPyQuerycheerio (Node)cheerio vs selectolax
1 KB0.0286 ms0.05080.04560.1147 ms4.0×
10 KB0.1725 ms0.18020.17280.3490 ms2.0×
100 KB1.4855 ms1.41451.40933.8399 ms2.6×
1 MB14.97 ms15.0314.9659.37 ms4.0×
10 MB158.10 ms165.25162.862,927.89 ms18.5×

p50 毫秒,三次运行的中位数。parser-bench.json。三个 Python 解析器是在同一个进程中运行的;cheerio 则运行在 Node 22 中,这既是运行时边界,也是库边界——见下文。

如何客观看这张表

测量结果图表:不同页面大小下的解析时间

1 KB 这一行基本是噪声。 在三个 Python 解析器之间,这个尺寸下的差异达到 77.6%,而且单次运行结果重叠得很厉害——比如 selectolax 的三次结果在 0.0267 到 0.0404 ms 之间。28 微秒这个量级里,计时器分辨率和调度开销占主导地位。我不会拿 1 KB 的结果给任何工具排序,包括 cheerio。

表格中间部分没什么戏剧性。 在 10 KB 到 1 MB 之间,差距大致在 2 倍到 4 倍。对于一个只抓几百个页面的爬虫来说,这意味着每页 45 毫秒而不是 15 毫秒,你根本感觉不到。

10 MB 这一行不是噪声。 cheerio 的三次结果分别是 2,839、2,928 和 2,954 ms——很稳定,而且明显和其他尺寸拉开了距离。10 MB 的端到端结果与较小尺寸时的模式明显不同。但仅凭五个尺寸点,不能证明其渐近复杂度,也不能判断到底是运行时、解析器、选择器、内存分配还是垃圾回收层导致了这个跃升。

它落在 BeautifulSoup 的区间里。 公开基准还测了同一个 10 MB 样本上的另外四个解析器,把 cheerio 的 2,927.89 ms 放到它们旁边,是本文最有价值的一部分:

解析器(10 MB)p50
selectolax (lexbor)159.93 ms
lxml172.93 ms
parsel231.85 ms
selectolax (modest)247.95 ms
BeautifulSoup + lxml2,261.56 ms
BeautifulSoup + html.parser2,788.75 ms
cheerio2,927.89 ms

上面四行 Python 数据来自 bench_parse.json 的已发布结果;cheerio 的数据来自本次运行。参考解析器在两次运行之间复现结果落在 0.989×–1.079× 之间,因此把低于约 8% 的差异视为误差范围内更合理——例如 cheerio 与 BeautifulSoup 的 html.parser 后端相差 5%,算在这个范围内;但 cheerio 和 selectolax 相差 18 倍,显然不在此列。

BeautifulSoup 是很多人在想要省事、也接受它比较慢时会优先选择的库——在 Python 性能讨论里,它常常是“你应该换掉它”的那个对象。在 10 MB 文档上,cheerio 也落在同一性能带里,而不是那些经常被归到一起的 C 后端解析器那一档。

至于 Node 侧的替代方案,这篇文章并没有继续展开。更现代的 Node 备选项没有被测试,所以这里不能说明“换库不可能”,也不能说它们不够成熟。本文只展示了在这些 Python 技术栈对比下,cheerio 的实测表现。

这也是一场运行时对比,而不只是库对比。 cheerio 的毫秒数来自 Node 的 JIT 和垃圾回收器;其他结果来自 CPython 调用 C 后端解析器。内容哈希证明完成了相同的工作,而这些数字也正是开发者在选择技术栈时真实会遇到的成本——但没人应该把它解读成“cheerio 的算法比 selectolax 差 18 倍”。这只是本机、各自原生运行时里实际发生的结果。

环境与依赖情况

包数量磁盘占用许可证Stars最近提交
cheerio22(npm)9.0 MiBMIT30,4492026-08-11
PyQuery3(pip)20.1 MiBBSD2,3802026-07-27

官方参考:Cheerio 配置文档

metadata-snapshot.json,采集于写作当天。

npm install cheerio 用时不到两秒,下载量为 9.0 MiB。在同一台机器上,另一次单独的转换运行中,冷启动导入耗时为 0.056 秒。

11 个直接依赖对于一个解析器来说不算少,如果你要审计依赖树,这一点很值得注意:htmlparser2parse5parse5-htmlparser2-tree-adapterparse5-parser-streamdomhandlerdomutilsdom-serializercheerio-selectencoding-snifferundiciwhatwg-mimetype。里面其实包含了两套完整的解析器实现,因为 cheerio 可以根据需要使用其中任意一种。

三万多个 stars,而且在测试前一天还有提交,这在这个类别里已经是很健康的维护信号了。

内存占用,以及损坏 HTML 会带来什么影响

内存占用和异常 HTML 的处理方式会影响部署和故障处理,所以这里把它们单独测量。

更完整的压力测试背景在这篇 十个库的内存与畸形 HTML 对比 中。

峰值常驻内存 通过 /usr/bin/time -l 测得,每个单元格都使用一个全新的进程——导入基线表示库加载后空闲时的成本,峰值则把文档本身算进去。

运行时导入基线226 KB 峰值10 MB 峰值
html2textpython3.1418.719.971.2
pyquerypython3.1430.333.9172.5
resiliparsepython3.1420.525.1225.1
markdownifypython3.1423.928.9278.5
goose3python3.1444.152.4398.5
cheerionode2266.876.5398.5
justextpython3.1430.336.6431.2
newspaper4kpython3.1452.661.8668.5
trafilaturapython3.1452.564.8927.1
turndownnode2247.868.42947.1

memory-results.json。Python 和 Node 的基线彼此不能直接比较;解释器本身已经包含在两边之内。

在这个混合上下文表里,cheerio 的导入基线最高,达到 66.8 MiB,这其中包含 Node 运行时和依赖。它在 10 MB 样本上的进程峰值是 398.5 MiB。其他行里既有解析器,也有转换器和文章提取器,它们的主要工作不同,所以更适合拿来做进程占用背景,而不是横向性能排名。同一运行时下的 turndown 峰值更高,但它做的是转换,而不是 cheerio 这里测的“提取标题和链接”的契约。

畸形 HTML。 测试集包含 12 个文档,每个都只故意破坏一处:未闭合标签、嵌套错位的行内元素、带空格却未加引号的属性、多余闭合标签、根本没有 <html>、重复属性、在标签中间被截断的文档、错误实体、未闭合的 <script>、错误的字符集声明、包含标记的注释,以及 600 层嵌套;另外再加上 两个同尺寸的正常控制样本,因为“它什么都没返回”只有在该库对正常文档也沉默时,才能说明它对畸形输入的处理能力。

cheerio 在 14 个样本中 0 个报错、0 个返回空结果,并在畸形样本中恢复出了 11/22 个评分哨兵位(malformed-results.json)。对于解析器来说,评分器会检查 11 个可评分的畸形文档中的标题和链接哨兵;它不对段落哨兵评分,而且未闭合 <script> 的样本被排除在外。在这一部分没有同契约基线的情况下,11/22 不能算质量排名。我们能得出的结论是:cheerio 在所有 14 个畸形与控制输入上都返回了非空输出且没有报错,并且恢复了一半的评分标记。

优缺点

优点。 熟悉的 jQuery 风格语法。MIT 许可证。30,449 stars 和测试前一天还有提交,这些都说明它维护状态不错。它包含两个解析后端以及与编码相关的包,不过本次没有把后端恢复能力和编码准确性单独拆开测试。对所有样本尺寸来说,它提取后排序得到的标题 + href 哈希都与参考值一致。

缺点。 在 10 MB 文档上比 selectolax 慢 18.5 倍,在 1 MB 上也慢 4 倍。11 个直接依赖,其中还包括两套完整解析器实现。仅支持 Node。而且文档里也没有告诉你它在哪个尺寸下会不再是显而易见的选择。

谁适合用,谁不适合用

适合使用 cheerio 的情况:你在 Node 环境里,jQuery 式 API 很重要,而且你的代表性页面规模接近本次测试的 1 MB 以内范围。1 MB 是本次测试里最大的“常规”点,之后就是 10 MB 的明显跃升;本文并没有给出二者之间的分界点,也没有声称整个互联网有多少页面低于这个阈值。

在正式使用前先做基准测试,如果你要处理非常大的 HTML 文档,例如生成报表、目录导出或长列表页。XML sitemap 的行为没有测试。对于 10 MB 的 HTML 样本来说,每个文档 2.9 秒是会不断叠加的实打实成本。

如果你用的是 Python,这份对比告诉你的又是另一件事:selectolax、lxml 和 PyQuery 从 10 KB 开始几乎打平(差异在 0.5% 到 5.4% 之间,而且运行结果区间重叠),所以可以优先按 API 选择,而不是按速度。cheerio 跟它们三者之间的差距才是值得关注的数字,而不是它们彼此之间的细微差别。

托管 API 的位置

cheerio 处理的是你已经拿到手的 HTML。它不会去抓取页面、不会执行 JavaScript,也不会处理反爬层——而在很多真实目标站点上,后半段才是更难的部分。

托管式抓取 / 渲染 / 提取服务,包括我们自己的 Thunderbit,处在不同的责任边界上。本次没有对 Thunderbit 做基准测试。真正的区别在于:是对已有 HTML 做选择器解析,还是把采集、渲染和提取都外包出去;这篇文章并没有提供相同指标下的质量、延迟或成本对比。

更公平的说法是:如果你已经拿到了 HTML,也知道自己要什么选择器,cheerio 就是免费且顺手的。如果你要大规模抓页面,或者更愿意描述数据而不是 DOM,那就是另一类采购决策了。

想了解更广泛的选择,可以看看我们的 网页抓取 API 盘点开源爬虫总览。如果解析后的内容最终要喂给模型,用 Python 将 HTML 转成 Markdown 这篇会说明哪些细节会在转换中丢失。

试试 Thunderbit 进行网页数据提取

该不该用 cheerio?

如果你在 Node 里,而且 API 形态很重要,代表性文档又大致落在本次测试的小尺寸到 1 MB 区间,那么答案是:可以。

API 的熟悉度和当前的维护信号,都是合理的选型因素。但这项基准测试并不能证明某种支持答复一定存在,也不能证明测试中的页面尺寸分布与你的生产数据完全一致。

真正要记住的是 10 MB 这个数字。也就是说,在 1 MB 和 10 MB 之间的某个地方,cheerio 的成本不再跟随其他库,而是开始倍增——4 倍变成 18.5 倍。如果你的数据集中确实有这么大的文档,在决定之前一定要先测,因为库本身不会提醒你。

试试 Thunderbit 进行网页数据提取 Get Started Free

常见问题

拿 cheerio 跟 Python 解析器比,公平吗? 这是技术栈对比,不是算法对比。四个工具在 5/5 个页面尺寸上,都按哈希规则提取出了完全一致的排序后标题文本和 href。这并不能证明所有解析行为都等价。cheerio 的耗时包含 Node 的运行时表现,而其他结果包含 CPython 调用 C 后端解析器的成本;这个对比描述的是这些端到端选择。

为什么 1 KB 那一行不做排名? 因为在 28 微秒这个量级,测量已经被噪声主导了。三次运行里,Python 解析器的差异达到 77.6%,单次结果彼此还会重叠。这个尺寸下任何排序都只是偶然产物。从 10 KB 往上,结果才足够稳定。

10 MB 处为什么会突然跳升? 这项测试并没有说明原因。它能确认的是,这个跳升是真实存在的,不是噪声:cheerio 三次结果分别是 2,839、2,928 和 2,954 ms,明显与其他结果区分开;而 1 MB 时候的差距还只是 4 倍。要找原因,就得单独给 cheerio 的各个解析后端做性能剖析,而这超出了本次范围。

它到底有多少依赖? 11 个直接依赖,展开后是 22 个顶层包,占用 9.0 MiB 磁盘空间。其中有两个是完整解析器实现——htmlparser2parse5——因为 cheerio 可以按需使用任意一个。这就是它同时支持宽容解析和符合规范解析的代价,如果你要审计依赖树,这点很值得知道。

这里没测什么? 当前草稿确实测了一个 226 KB 文档和一个 10 MB 文档的进程峰值内存,也测了一个包含 14 个输入的畸形 + 控制样本集合,其中 cheerio 没有报错、全部返回非空输出,并恢复了 11/22 个评分哨兵。但它没有测试 parse5-parser-stream 的流式处理、编码正确性、各后端的恢复行为、1 MB 到 10 MB 之间性能跃升的具体位置、XML 解析,也没有测试更新的 Node 替代方案。原始相对资源链接在发布时还要求保持相同的公开目录结构;否则就需要稳定的公开 URL 或仓库提交引用。

Ke
Ke
Thunderbit 首席技术官 | 高级数据科学家与机器学习专家 Ke Shen 拥有近十年的机器学习和数据科学经验,毕业于哥伦比亚大学,曾任 Walmart Labs 高级数据科学家。他在 Python、R、Java 和统计学方面拥有深厚且备受同行认可的专业能力,并分享如何将复杂的 AI 算法从理论落地到生产级架构的实战经验。
目录
Thunderbit · AI 网页数据代理

一键 内提取任意页面数据

深受 250,000+ 用户信赖
提供免费方案
从网页到表格
描述你的需求——Thunderbit 的 AI 代理会帮你抓取并导出到 Excel、Google Sheets、Airtable 或 Notion。可免费开始。
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week