あるマシンでは、Node 上の cheerio が 10 MB の合成 HTML ドキュメントからタイトルと href を解析・抽出するのに 2,927.89 ms かかりました。一方、C バックエンドのパーサーを使う CPython 経由で動かした selectolax は、同じフィールド抽出を 158 ms で完了しました。ソート済みのタイトルテキストと href のハッシュは一致しています。これは、個別のパーサーアルゴリズムの優劣ではなく、実行環境をまたいだエンドツーエンドのスタック比較です。
10 KB のページでは差は 2 倍で、誰も気にしないでしょう。結局のところ問題は、あなたのページがこの曲線のどこに位置するかです。
cheerio とは何か
cheerio は、Node 向けの jQuery 風構文を持つ HTML パーサーです。Node エコシステムでこれが定番になったのには理由があります。GitHub スター 30,449 個、MIT ライセンス、そして今回の計測前日にもリポジトリに push がありました。検証したバージョンは 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 はすでに見覚えがあるはずです。この親しみやすさこそが、cheerio がそのエコシステムで選ばれた最大の理由です。
内部は単一のパーサーではなく、複数コンポーネントの組み合わせです。解析には htmlparser2 と parse5、ツリーには domhandler と domutils、セレクタには cheerio-select、さらに undici や encoding-sniffer なども含まれ、直接依存は 11 個、解決後のトップレベルパッケージは 22 個、ディスク上では 9.0 MiB になります。これらのパッケージは解析やエンコーディング機能を提供する一方で、依存関係の重さにもつながります。このレビューでは不正 HTML への出力はテストしましたが、エンコーディングの正しさや、どちらのパーサーバックエンドにも切り分けた検証は行っていません。
計測方法と、その信頼性
この調査基盤にはすでにパーサーベンチマークがありました。1 KB から 10 MB までの 5 つのページサイズ、50 回の反復、3 つの独立した実行、そして重要なのは、抽出結果のハッシュで整合性を確認するゲートです。つまり、ソート済みタイトルとソート済み href を基準パーサーと照合します。余計な処理を省いて見かけ上速くなったパーサーは、速い数字を出せません。
cheerio を加えるには、数値を採用する前に 2 つの確認が必要でした。
基準値は以前と同じ位置に着地したか? selectolax を同じセッション、同じフィクスチャで再実行しました。コンテンツハッシュは 5/5 サイズで再現され、p50 は公開値の 0.989×〜1.079× の範囲に収まりました。つまり、これは元の表を作ったのと同じマシンです。
cheerio は同じ評価対象フィールドを出したか? Node で同一ルール、つまりソート済みタイトルテキストとソート済み href に対する SHA-256 で算出したコンテンツハッシュは、5/5 サイズで基準値と一致しました。これは、このフィクスチャ上のソート済みフィールドに関する整合性を証明するものであって、DOM の形、文書順、属性、テキスト正規化、エラー回復までを保証するものではありません。
ここまで揃って初めて、タイミングの意味があります。
| ページサイズ | selectolax | lxml | PyQuery | cheerio (Node) | cheerio 対 selectolax |
|---|---|---|---|---|---|
| 1 KB | 0.0286 ms | 0.0508 | 0.0456 | 0.1147 ms | 4.0× |
| 10 KB | 0.1725 ms | 0.1802 | 0.1728 | 0.3490 ms | 2.0× |
| 100 KB | 1.4855 ms | 1.4145 | 1.4093 | 3.8399 ms | 2.6× |
| 1 MB | 14.97 ms | 15.03 | 14.96 | 59.37 ms | 4.0× |
| 10 MB | 158.10 ms | 165.25 | 162.86 | 2,927.89 ms | 18.5× |
p50 はミリ秒、3 回実行の中央値です。parser-bench.json。3 つの Python パーサーは 1 つのプロセスで実行し、cheerio は Node 22 で実行しました。これはライブラリ境界であると同時にランタイム境界でもあります。詳細は下記を参照してください。
この表を正直に読む

1 KB の行は誤差の範囲です。 3 つの Python パーサーではそのサイズで 77.6% のばらつきがあり、個々の実行結果も大きく重なっています。selectolax は 3 回の実行で 0.0267〜0.0404 ms でした。28 マイクロ秒という領域では、タイマー分解能とスケジューリングの影響が支配的です。1 KB で何かを順位付けするつもりはありません。cheerio も同様です。
表の中央は特に目立ちません。 10 KB〜1 MB のページでは 2 倍〜4 倍です。数百ページを処理するスクレイパーなら、1 ページ 15 ms の代わりに 45 ms になるだけで、体感はほぼありません。
10 MB の行は誤差ではありません。 cheerio の 3 回の実行は 2,839 ms、2,928 ms、2,954 ms で、いずれも近接しつつ他のサイズ帯からは明確に分離されています。10 MB のエンドツーエンド結果は、小さいサイズのパターンから大きく外れています。とはいえ、5 つのサイズ点だけでは漸近計算量は断定できませんし、どのランタイム、パーサー、セレクタ、メモリ確保、GC の層が跳ね上がりを引き起こしたのかも特定できません。
BeautifulSoup の帯に入っています。 公開ベンチは同じ 10 MB フィクスチャでさらに 4 つのパーサーを測定しており、cheerio の 2,927.89 ms を並べるとこの比較の意味が最もはっきりします。
| パーサー (10 MB) | p50 |
|---|---|
| selectolax (lexbor) | 159.93 ms |
| lxml | 172.93 ms |
| parsel | 231.85 ms |
| selectolax (modest) | 247.95 ms |
| BeautifulSoup + lxml | 2,261.56 ms |
| BeautifulSoup + html.parser | 2,788.75 ms |
| cheerio | 2,927.89 ms |
上の 4 行の Python ベンチマーク値は bench_parse.json の公開値で、cheerio の値は今回の実行結果です。基準パーサーは 2 回の実行で 0.989×〜1.079× に収まって再現されたため、約 8% 未満の差は不確かさの範囲内と見なしてください。たとえば cheerio と BeautifulSoup の html.parser バックエンドの差(5%)はこの範囲内ですが、cheerio と selectolax の差(18 倍)は範囲外です。
BeautifulSoup は、利便性を重視し、その代わりに遅いことを承知で使うライブラリです。Python のパフォーマンス議論では、必ず「別のものに置き換えろ」と言われる存在です。10 MB の文書では、cheerio は C バックエンドのパーサー群の帯ではなく、その同じ帯のいちばん下に位置しています。
Node 側の置き換え先という問いは、この文章では未解決のままです。より新しい Node の代替手段はテストしていないため、この結果だけで「ライブラリの置き換えは不可能」とも、「新しいものは成熟していない」とも言えません。ここで示しているのは、測定した cheerio の経路と、列挙した Python スタックとの比較だけです。
これはライブラリ比較であると同時に、ランタイム比較でもあります。 cheerio のミリ秒は Node の JIT とガベージコレクタが生み、他の結果は CPython が C バックエンドのパーサーを呼び出した結果です。コンテンツハッシュは同じ仕事が行われたことを示しており、どちらの数字も実際にスタックを選ぶ開発者が体験するものです。ただし、これを「cheerio のアルゴリズムが selectolax より 18 倍悪い」と読むべきではありません。これは、このマシン上で、各ライブラリのネイティブランタイム内で実際に起きたことです。
セットアップの現実
| ライブラリ | パッケージ数 | ディスク使用量 | ライセンス | スター数 | 最終 push |
|---|---|---|---|---|---|
| cheerio | 22 (npm) | 9.0 MiB | MIT | 30,449 | 2026-08-11 |
| PyQuery | 3 (pip) | 20.1 MiB | BSD | 2,380 | 2026-07-27 |
公式リファレンス: Cheerio の設定ドキュメント。
metadata-snapshot.json、執筆当日に取得。
npm install cheerio は 2 秒未満で完了し、9.0 MiB を取得しました。コールドインポートは、同じマシン上の別の変換実行で 0.056 秒でした。
直接依存が 11 個というのはパーサーとしては多く、依存ツリーを監査するなら把握しておく価値があります。内訳は htmlparser2、parse5、parse5-htmlparser2-tree-adapter、parse5-parser-stream、domhandler、domutils、dom-serializer、cheerio-select、encoding-sniffer、undici、whatwg-mimetype です。これだけで 2 つの完全なパーサー実装が同梱されているのは、cheerio が求めに応じてどちらも使えるためです。
3 万超のスターと、テスト前日に push がある状態は、このカテゴリではかなり健全な保守性の संकेत です。
メモリと、壊れた HTML が与える影響
メモリ使用量と不正 HTML への挙動は、導入や障害対応に直結するため、ここでは別々に測定しています。
より広い負荷テストの文脈は、10 ライブラリのメモリと不正 HTML の比較 にあります。
ピーク常駐メモリ は /usr/bin/time -l で測定し、各セルごとに新しいプロセスを立ち上げました。import 時の最低値はライブラリを読み込んだまま待機している状態のコスト、ピーク値にはドキュメント本体が含まれます。
| ライブラリ | ランタイム | import 時の下限 | 226 KB のピーク | 10 MB のピーク |
|---|---|---|---|---|
| html2text | python3.14 | 18.7 | 19.9 | 71.2 |
| pyquery | python3.14 | 30.3 | 33.9 | 172.5 |
| resiliparse | python3.14 | 20.5 | 25.1 | 225.1 |
| markdownify | python3.14 | 23.9 | 28.9 | 278.5 |
| goose3 | python3.14 | 44.1 | 52.4 | 398.5 |
| cheerio | node22 | 66.8 | 76.5 | 398.5 |
| justext | python3.14 | 30.3 | 36.6 | 431.2 |
| newspaper4k | python3.14 | 52.6 | 61.8 | 668.5 |
| trafilatura | python3.14 | 52.5 | 64.8 | 927.1 |
| turndown | node22 | 47.8 | 68.4 | 2947.1 |
memory-results.json。Python と Node の下限値は互いに直接比較できません。どちらにもインタプリタが含まれているためです。
この混在表では、import 時の下限が 66.8 MiB と cheerio が最も高く、Node ランタイムと依存関係を含んでいます。10 MB フィクスチャではプロセスのピークが 398.5 MiB でした。他の行には、パーサー、変換器、記事抽出器など、主目的の異なるものが含まれているため、性能順位ではなくプロセス使用量の文脈として見るのが適切です。同じランタイムである turndown の行はさらに高いピークを示しますが、cheerio の「タイトルと href を抽出する」という契約ではなく、変換処理を行っています。
壊れた HTML。 それぞれ 1 つだけ壊し方の違う 12 個の文書、つまり未閉鎖タグ、入れ子が崩れたインライン要素、スペースを含む引用符なし属性、余計な閉じタグ、<html> がまったくないケース、重複属性、タグ途中で切れた文書、壊れたエンティティ、閉じていない <script>、偽の charset 宣言、マークアップを含むコメント、そして 600 層のネスト。さらに、サイズを合わせた整形式の制御文書を 2 つ用意しました。「何も返さなかった」という結果は、同じサイズのきれいな文書でも黙ったままなら、不正さについて何も言えないからです。
cheerio は 14 件中 0 件で例外を投げず、0 件で空を返さず、不正フィクスチャ全体で 22 個中 11 個のスコア対象サンプルを復元しました (malformed-results.json)。パーサーの採点では、11 個の採点可能な不正文書に対して heading と link のサンプルを確認します。paragraph のサンプルは採点せず、閉じていない <script> のフィクスチャは除外されます。この節には同じ契約の基準値がないため、11/22 を品質順位として読むべきではありません。言えるのは、cheerio が 14 個の不正+制御入力すべてに対して例外なく非空出力を返し、採点対象マーカーの半分を復元したということです。
長所と短所
良い点。 jQuery に慣れた構文。MIT ライセンス。3 万 449 のスターと、計測前日のリポジトリ活動という、保守の健全さを示すタイムスタンプ付きのシグナル。2 つのパーサーバックエンドとエンコーディング関連パッケージを備えていますが、バックエンドごとの差やエンコーディングの正確さはここでは切り分けていません。ソート済みタイトル+href のハッシュは、すべてのフィクスチャサイズで基準値と一致しました。
悪い点。 10 MB 文書では selectolax より 18.5 倍遅く、1 MB でも 4 倍。2 つの完全なパーサー実装を含む 11 個の直接依存。Node 専用。そして、どのサイズであれば明らかな選択肢でなくなるのか、ドキュメントには何も書かれていません。
どんな人が使うべきで、どんな人は避けるべきか
cheerio を使うべき人。 Node 上で動かしていて、jQuery 風 API の価値が高く、対象ページが今回テストした 1 MB までのサイズ帯に近い場合です。1 MB は、10 MB で急激に跳ね上がる直前の最大テスト点です。この間の閾値がどこか、また Web 全体のどれだけがその下にあるかは、この文章では断定しません。
使う前にベンチマークすべき人。 レポート生成、カタログのダンプ、長大な一覧ページなど、非常に大きな HTML 文書を扱う場合です。XML sitemap の挙動はテストしていません。10 MB の HTML フィクスチャでは、1 文書あたり 2.9 秒というコストは無視できず、積み上がります。
Python を使っている人。 この比較が示すのは別のことです。selectolax、lxml、PyQuery は 10 KB 以上では事実上横並びで、差は 0.5%〜5.4% 程度に収まり、実行範囲も重なっています。だから速度ではなく API で選べばよいのです。3 つの差よりも、cheerio とのギャップのほうが重要です。
マネージド API の位置づけ
cheerio は、すでに持っている HTML を解析します。ページの取得、JavaScript のレンダリング、あるいは anti-bot 層の処理はしません。そして多くの実サイトでは、そちらのほうが仕事の難しい半分です。
取得・レンダリング・抽出をまとめて任せるマネージドサービス、たとえば私たちの Thunderbit のようなものは、責任の境界が違います。Thunderbit はここではベンチマークしていません。重要なのは、渡された HTML をセレクタで解析することと、取得・レンダリング・抽出そのものを外部委託することの違いです。この文章では、それらを同じ指標で品質・遅延・コスト比較していません。
公平に言えば、HTML を自分で持っていてセレクタも分かるなら、cheerio は無料で使いやすいです。ページを大規模に取得するなら、あるいは DOM ではなくデータそのものを定義したいなら、話は別です。
より広い選択肢については、Web スクレイピング API の比較 でホスト型の選択肢を、オープンソーススクレイパーの総覧 でセルフホスト型の選択肢を紹介しています。解析後の出力をモデルに渡すなら、Python で HTML を Markdown に変換する方法 が、どこで忠実度が失われるかを説明しています。
cheerio を使うべきか?
はい、Node 上で、API の適合度が重要で、対象文書が今回テストした小〜1 MB の範囲に収まるなら使う価値があります。
API の親和性と現在の保守状況は、正当な選定材料です。ただし、このベンチマークは、特定のサポート対応が存在することや、テストしたページサイズ分布が本番コーパスに一致することまでは証明しません。
覚えておくべき数字は 10 MB のほうです。1 MB から 10 MB のどこかで、cheerio のコストは他と連動しなくなり、増幅し始めます。4 倍が 18.5 倍になります。もしあなたのコーパスにそのサイズの文書が含まれるなら、導入前に必ずベンチマークしてください。ライブラリ側は何も警告してくれません。
Web データ抽出に Thunderbit を試す Get Started Free
FAQ
cheerio を Python のパーサーと比較するのは公平ですか? これはアルゴリズム比較ではなく、スタック比較です。5/5 のページサイズで、4 つすべてがハッシュ規則の下で同一のソート済みタイトルテキストと href を返しました。ただし、それでパーサー全体が同等だと証明されるわけではありません。cheerio の計測には Node のランタイム挙動が含まれ、他の結果には CPython が C バックエンドのパーサーを呼ぶ処理が含まれます。この比較は、それらのエンドツーエンドの選択を表しています。
なぜ 1 KB の行は順位付けされていないのですか? 28 マイクロ秒では、測定結果がノイズに支配されるからです。3 回の実行で Python パーサーの差は 77.6% あり、個々の実行結果も互いに重なっています。そのサイズでの順序付けは、単なるアーティファクトにすぎません。10 KB 以上は十分安定して読めます。
10 MB で何が跳ね上がりの原因なのですか? このテストでは分かりません。ここで分かるのは、その跳ね上がりが本物でノイズではないということです。cheerio の 3 回の実行は 2,839 ms、2,928 ms、2,954 ms に収まり、他の結果と明確に分離されていました。一方、1 MB では差は 4 倍でした。原因の切り分けには、cheerio のパーサーバックエンドを個別にプロファイルする必要がありますが、それは今回の範囲外です。
実際には依存関係はいくつありますか?
直接依存は 11 個、解決後のトップレベルは 22 個、ディスク上は 9.0 MiB です。そのうち 2 つは完全なパーサー実装である htmlparser2 と parse5 です。cheerio はどちらも使えるためです。寛容な解析と仕様準拠の解析の両方を扱うための代償であり、依存ツリーを監査するなら知っておく価値があります。
ここでテストしていないことは何ですか?
226 KB と 10 MB の 2 つの文書に対するプロセスのピークメモリ、そして 14 入力の不正+制御セットで、cheerio が例外を投げず、すべてで非空出力を返し、採点対象マーカー 22 個中 11 個を復元したことはテストしました。parse5-parser-stream 経由のストリーミング、エンコーディングの正しさ、バックエンド固有のエラー回復、1〜10 MB の間の性能ジャンプの位置、XML 解析、より新しい Node 代替手段はテストしていません。


