ある 환경에서는、Node 上の cheerio が 10 MB の合成 HTML ドキュメントから title と href を抽出するまでに 2,927.89 ms かかりました。一方、C 製のパーサーを使う CPython 経由の selectolax は、同じ項目抽出を 158 ms で完了しました。ソート済みの title テキストと href のハッシュは一致しています。これはパーサー単体の優劣ではなく、実行環境まで含めたエンドツーエンドの比較です。
10 KB のページなら差は 2 倍程度で、誰も気づかないでしょう。大事なのは、あなたのページがその曲線のどこにあるかです。
cheerio とは
cheerio は、Node 向けの jQuery 風 HTML パーサーです。このエコシステムでデフォルト解として選ばれてきたのには理由があります。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 がそのエコシステムで支持されてきた大きな理由です。
内部は 1 つのパーサーではなくスタック構成です。解析には htmlparser2 と parse5、ツリーには domhandler と domutils、セレクタには cheerio-select、さらに undici、encoding-sniffer などを含む 11 個の直接依存 があり、解決後のトップレベルパッケージは 22、ディスク使用量は 9.0 MiB でした。これらのパッケージは解析や文字コード検出の機能を提供する一方で、依存関係のボリュームにもつながっています。このレビューでは壊れた入力に対する出力は試しましたが、文字コードの正しさや、どちらか一方のパーサー実装だけを切り出した評価はしていません。
計測方法と、その信頼性
今回のベースラインにはすでにパーサーのベンチマークがありました。1 KB から 10 MB までの 5 つのサイズ、50 回の反復、3 回の独立実行、そして重要な点として、抽出結果のハッシュ一致を確認するパリティゲートが入っています。対象はソート済みタイトルとソート済み href で、基準パーサーと照合します。裏でこっそり処理を飛ばすようなパーサーは、速い数字を出せません。
cheerio を追加する前に、数字として成立するかを確かめるため、2 つの確認が必要でした。
基準値は前回と同じ位置に出たか? selectolax を同じセッション・同じフィクスチャで再実行しました。内容ハッシュは 5/5 サイズ で再現し、p50 は公開値の 0.989 倍〜1.079 倍 に収まりました。つまり、このマシンは元の表を出したあの環境です。
cheerio は同じ項目を正しく出せたか? Node で同じルール、つまりソート済み title テキストとソート済み href に対する SHA-256 を使って内容ハッシュを計算したところ、5/5 サイズで基準と一致しました。これは、このフィクスチャにおけるソート済みフィールドのパリティを示すものであって、DOM の形、文書順、属性、テキスト正規化、エラー回復まで同等だと示すものではありません。
そこまで確認して、初めてタイミングの意味が出ます。
| ページサイズ | selectolax | lxml | PyQuery | cheerio (Node) | cheerio vs 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 マイクロ秒では、タイマー精度とスケジューリングの影響が支配的です。cheerio を含め、1 KB で順位をつけるべきではありません。
表の中央は特筆すべき点が少ないです。 10 KB から 1 MB の範囲では差は 2 倍〜4 倍程度。数百ページを処理するスクレイパーなら、1 ページ 15 ms が 45 ms になったところで、体感ではまず分かりません。
10 MB 行はノイズではありません。 cheerio の 3 回の実行は 2,839、2,928、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 の代替は試していないため、この結果だけで「置き換えは不可能」とは言えませんし、未成熟だとも言えません。ここで示せるのは、提示した Python スタックに対して測定した cheerio の結果だけです。
これはライブラリ比較であると同時に、ランタイム比較でもあります。 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 configuration documentation。
metadata-snapshot.json は執筆当日に取得したものです。
npm install cheerio は 2 秒未満で完了し、9.0 MiB を取得しました。コールドインポートは、同じマシンで別コンバータを使った実行で 0.056 s でした。
11 個の直接依存はパーサーとしては多めで、ツリー監査をするなら把握しておく価値があります。内訳は htmlparser2、parse5、parse5-htmlparser2-tree-adapter、parse5-parser-stream、domhandler、domutils、dom-serializer、cheerio-select、encoding-sniffer、undici、whatwg-mimetype です。cheerio は用途に応じてどちらかを使えるため、完全なパーサー実装が 2 系統含まれています。
スター数が 3 万、しかもテスト前日に push がある。こうした指標としては、かなり健全なメンテ状況です。
メモリと、壊れた HTML がそれに与える影響
メモリ使用量と壊れた入力への挙動は、運用面と障害時の扱いに効くため、ここでは別々に測定しています。
より広いストレステストの文脈は、10 ライブラリのメモリおよび壊れた HTML 比較 にあります。
ピーク常駐メモリ は /usr/bin/time -l で測定し、各セルごとに新規プロセスを立ち上げました。import floor はライブラリを読み込んで待機しただけのコスト、ピーク値にはドキュメント本体が含まれます。
| ライブラリ | ランタイム | import floor | 226 KB peak | 10 MB peak |
|---|---|---|---|---|
| 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 のベースライン同士は直接比較できません。両方にインタプリタが含まれているからです。
cheerio は、この混在表の中で import floor が最も高く、Node ランタイムと依存関係込みで 66.8 MiB でした。10 MB フィクスチャではプロセスのピークは 398.5 MiB です。他の行には、パーサー、コンバータ、記事抽出器など、主目的の異なるものが含まれているので、同列の性能比較ではなく、あくまでプロセスフットプリントの参考として見てください。同じランタイムの turndown はもっと高いピークでしたが、こちらは cheerio が測っている title/href 抽出ではなく、変換処理を行っています。
壊れた HTML。 未閉じタグ、入れ子が崩れたインライン要素、空白を含む引用なし属性、余計な閉じタグ、<html> そのものがない文書、重複属性、途中で切れたタグ、壊れたエンティティ、未閉じの <script>, 嘘の charset 宣言、マークアップを含むコメント、600 レベルのネスト――この 12 件に加えて、サイズを揃えた 2 件の正常系コントロール を用意しました。「何も返さなかった」という結果は、同じサイズのきれいな文書でも沈黙するライブラリでない限り、壊れ方の評価にはなりません。
cheerio は 14 件中 0 件で例外を投げず、0 件で空結果 を返しませんでした。壊れたフィクスチャ群では、スコア対象のシグナルを 22 個中 11 個 回復しています (malformed-results.json)。パーサー用の採点では、11 の採点可能な壊れた文書に対して見出しとリンクのシグナルを確認します。段落シグナルは採点せず、未閉じ <script> のフィクスチャは除外しています。この節には同じ契約のベースラインがないため、11/22 を品質順位とはみなせません。支持できる結論は、cheerio が 14 個すべての壊れた入力+コントロール入力で例外なく非空の出力を返し、かつスコア対象マーカーの半分を回復した、という点です。
長所と短所
良い点。 見慣れた jQuery 構文。MIT。テスト前日の repository activity を含む、スター 30,449 というメンテの健康 संकेत。2 つのパーサーバックエンドと文字コード関連パッケージを持つこと。とはいえ、このレビューではバックエンドごとの復元力や文字コード精度は切り分けていません。ソート済み title+href のハッシュは、すべてのフィクスチャサイズで基準と一致しました。
悪い点。 10 MB 文書では selectolax より 18.5 倍遅く、1 MB でも 4 倍。2 つの完全なパーサー実装を含む 11 個の直接依存。Node 専用。そして、どのサイズから明確に最有力候補ではなくなるのか、ドキュメントからは読み取れません。
どんな人に向いていて、どんな人には向かないか
cheerio を使うべき人 は、Node 環境にいて、jQuery 風 API の使いやすさを重視し、対象ページがおおむね 1 MB までのテストサイズに近い場合です。1 MB は、10 MB で急に跳ね上がる直前の最大テスト点です。この記事では、その間のどこが境目か、あるいはウェブ全体の何割がそこより下にあるかまでは示していません。
特に大きな HTML を扱うなら事前ベンチマークを。生成レポート、カタログダンプ、長い一覧ページなどです。XML sitemap の挙動は試していません。10 MB の HTML フィクスチャでは、1 ドキュメントあたり 2.9 秒は、積み上がると無視できないコストです。
Python を使っているなら、別の結論になります。10 KB 以上では selectolax、lxml、PyQuery は実質的に横並びで、差は 0.5%〜5.4% 程度、しかも実行範囲が重なっています。なので、速度ではなく API で選べばよいでしょう。cheerio がこの 3 つに対してどれだけ離れているかのほうが、面白い数字です。
マネージド API はどこに入るか
cheerio は、すでに持っている HTML を解析します。ページ取得も、JavaScript レンダリングも、ボット対策層の処理も担当しません。現実の対象では、その後半のほうが難しいことも多いです。
取得・レンダリング・抽出までを代行するマネージドサービス、たとえば私たちの Thunderbit は、別の責任境界にあります。Thunderbit は今回のベンチ対象ではありません。重要なのは、手元の HTML をセレクタで解析するのか、それとも取得・レンダリング・抽出を外部に任せるのか、という違いです。この文章では、同じ指標での品質・遅延・コスト比較はしていません。
公平に言えば、HTML を手元に持っていてセレクタも分かっているなら、cheerio は無料で快適に使えます。一方で、大量ページを取得したい、あるいは DOM ではなくデータそのものを定義したいなら、それは別の選択です。
より広い視点では、web scraping API roundup でホスト型の選択肢を、open-source scraper pillar でセルフホスト型の選択肢を紹介しています。抽出結果をモデルに渡すなら、Python で HTML を Markdown に変換する方法 で、どこで忠実度が失われるかも確認できます。
cheerio を使うべきか?
はい、Node で使うなら、API の相性が重要で、対象文書がテストした小〜1 MB の範囲に収まる場合です。
API の親和性と現在のメンテナンス状況は、正当な選定要素です。ただし、このベンチマークは特定のサポート対応があることも、テストしたページ分布が本番コーパスと一致することも証明しません。
覚えておくべき数字は 10 MB のほうです。1 MB と 10 MB の間のどこかで、cheerio のコストは他と連動しなくなり、倍率が膨らみ始めます。4 倍が 18.5 倍になるのです。コーパスにそのサイズの文書が含まれるなら、導入前にベンチマークしてください。ライブラリ側から警告は出ません。
Thunderbit で Web データ抽出を試す Get Started Free
よくある質問
cheerio と Python パーサーを比べるのは公平ですか? これはアルゴリズム比較ではなく、スタック比較です。5/5 のページサイズで、4 つすべてが同じソート済み title テキストと href を、ハッシュルールの下で返しました。とはいえ、パーサー全体が同等だと証明したわけではありません。cheerio のタイミングには Node のランタイム挙動が含まれ、他方には CPython が C バックエンドのパーサーを呼び出す挙動が含まれます。比較しているのは、こうしたエンドツーエンドの選択です。
なぜ 1 KB 行は順位付けしないのですか? 28 マイクロ秒では、測定がノイズに支配されるからです。3 回の実行で Python パーサー間の差は 77.6% に達し、個々の結果も互いに重なっていました。このサイズでの順序づけは偶然の産物です。10 KB 以上なら十分安定して読めます。
10 MB で何が起きているのですか? このテストだけでは断定できません。ここで言えるのは、その跳ね上がりが本物で、ノイズではないということです。cheerio の 3 回の実行は 2,839、2,928、2,954 ms で、他の結果群から明確に離れていました。一方、1 MB での差は 4 倍でした。原因を切り分けるには、cheerio のパーサーバックエンドを個別にプロファイルする必要がありますが、それは今回の範囲外です。
実際には何個の依存関係がありますか?
直接依存は 11、解決後のトップレベルは 22、ディスク上では 9.0 MiB です。そのうち 2 つは完全なパーサー実装、htmlparser2 と parse5 で、cheerio は状況に応じてどちらかを使えます。寛容な解析と仕様準拠の解析の両方に対応する代償として、依存ツリーの監査時には把握しておくべきです。
ここで試していないものは何ですか?
今回の草案では、226 KB と 10 MB の 1 文書ずつでのプロセスピークメモリ、そして 14 入力の壊れた HTML+コントロールセットを試し、cheerio は例外を投げず、すべてで非空出力を返し、スコア対象シグナルを 11/22 回復しました。試していないのは、parse5-parser-stream によるストリーミング、文字コードの正しさ、バックエンド固有の復元力、1〜10 MB の間での性能ジャンプの位置、XML パース、そして新しい Node 代替です。なお、元の相対アーティファクトリンクは公開時点で同じ公開ディレクトリ構造が必要で、なければ永続的な公開 URL かリポジトリのコミット参照が必要です。


