Na jednej maszynie cheerio w Node przeanalizowało i wyciągnęło tytuły oraz hrefy z syntetycznego dokumentu HTML o rozmiarze 10 MB w 2 927,89 ms. selectolax, uruchomiony przez CPython z parserem opartym na C, wykonał to samo wydobycie pól w 158 ms. Posortowany tekst tytułów i hashe hrefów były zgodne. To porównanie całego stosu end-to-end między runtime’ami, a nie odosobniony werdykt na temat samego algorytmu parsera.
Na stronie o rozmiarze 10 KB różnica wynosi 2× i nikt by tego nie zauważył. Kluczowe pytanie brzmi: gdzie Twoje strony znajdują się na tej krzywej.
Czym jest cheerio
cheerio to parser HTML z interfejsem w stylu jQuery dla Node i nie bez powodu jest domyślną odpowiedzią w tym ekosystemie: 30 449 gwiazdek na GitHubie, licencja MIT i commit w repozytorium dzień przed moim testem. Testowana wersja: 1.2.0.
Oficjalne źródło: oficjalne wprowadzenie do 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();
Jeśli kiedykolwiek pisałeś w jQuery, API od razu będzie Ci znajome. Ta znajomość to w dużej mierze powód, dla którego cheerio wygrało w swoim ekosystemie.
Pod spodem nie jest to jeden parser, lecz cały stos: htmlparser2 i parse5 do parsowania, domhandler i domutils do drzewa DOM, cheerio-select do selektorów, a do tego undici, encoding-sniffer i inne — 11 bezpośrednich zależności, które po rozwiązaniu dają 22 pakiety najwyższego poziomu i 9,0 MiB na dysku. Te pakiety zapewniają możliwości parsowania i obsługi kodowania, ale jednocześnie zwiększają ślad zależności. Ta analiza testowała wynik dla błędnego wejścia, ale nie sprawdzała poprawności kodowania ani nie izolowała żadnego z backendów parsera.
Jak mierzono i dlaczego temu ufać
W bazie tego badania już wcześniej istniał benchmark parserów: pięć rozmiarów stron od 1 KB do 10 MB, 50 iteracji, trzy niezależne uruchomienia oraz — i to jest najważniejsze — bramka zgodności, która hashuje wyodrębnioną treść: posortowane tytuły plus posortowane hrefy, względem parsera referencyjnego. Parser, który po cichu pomija część pracy, nie może pochwalić się szybkim wynikiem.
Dodanie cheerio wymagało dwóch kontroli, zanim jakikolwiek wynik został uznany.
Czy referencja zachowała się tak jak wcześniej? selectolax uruchomiono ponownie w tej samej sesji na tych samych fixture’ach. Jego hash treści odtworzył się na 5 z 5 rozmiarów, a p50 mieścił się w zakresie 0,989× do 1,079× opublikowanej wartości. To potwierdza, że jest to ta sama maszyna, która wygenerowała oryginalną tabelę.
Czy cheerio wygenerowało te same oceniane pola? Jego hash treści — liczony w Node według identycznej reguły, SHA-256 nad posortowanym tekstem tytułów i posortowanymi hrefami — zgadzał się z referencją na 5 z 5 rozmiarów. To dowód zgodności dla tych posortowanych pól na tych fixture’ach, ale nie dla kształtu DOM, kolejności dokumentu, atrybutów, normalizacji tekstu ani odzyskiwania po błędach.
Dopiero wtedy te czasy mają sens.
| Rozmiar strony | 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 w milisekundach, mediana z trzech uruchomień. parser-bench.json. Trzy parsery Python działały w jednym procesie; cheerio uruchomiono w Node 22, co jest granicą runtime’u, a także biblioteką — patrz niżej.
Jak czytać tę tabelę uczciwie

Wiersz 1 KB to szum. Wśród trzech parserów Python rozrzut dla tego rozmiaru wynosi 77,6%, a pojedyncze uruchomienia mocno się nakładają — selectolax mieścił się w zakresie od 0,0267 do 0,0404 ms w trzech próbach. Przy 28 mikrosekundach dominują rozdzielczość timera i planowanie zadań. Nie klasyfikowałbym niczego przy 1 KB, w tym cheerio.
Środek tabeli nie robi wrażenia. 2× do 4× dla stron od 10 KB do 1 MB. Dla scrapera przetwarzającego kilkaset stron oznacza to 45 ms na stronę zamiast 15 ms i po prostu tego nie zauważysz.
Wiersz 10 MB to nie szum. Trzy uruchomienia cheerio dały 2 839, 2 928 i 2 954 ms — wyniki są spójne i wyraźnie oddzielone od pozostałych wierszy. Wynik end-to-end dla 10 MB wyraźnie odbiega od wzorca widocznego dla mniejszych rozmiarów. Pięć punktów pomiarowych nie pozwala ustalić asymptotycznej złożoności ani wskazać, czy skok powoduje warstwa runtime’u, parsera, selektorów, alokacji czy garbage collectora.
Wpisuje się w zakres BeautifulSoup. Opublikowany benchmark mierzył jeszcze cztery parsery na tym samym fixture 10 MB, a zestawienie wyniku cheerio 2 927,89 ms z nimi jest najciekawszą częścią tego artykułu:
| Parser (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 |
Cztery wiersze Python pochodzą z opublikowanych wartości z bench_parse.json; wynik cheerio pochodzi z tego uruchomienia. Parser referencyjny odtworzył się w zakresie 0,989×–1,079× pomiędzy dwoma przebiegami, więc różnice mniejsze niż około 8% traktuj jako mieszczące się w tej niepewności — cheerio względem backendu html.parser w BeautifulSoup (różnica 5%) mieści się w niej, cheerio względem selectolax (18×) już nie.
BeautifulSoup to biblioteka, po którą ludzie sięgają, gdy chcą wygody i świadomie akceptują jej niską szybkość — to ta, którą w wątkach o wydajności Python wszyscy radzą zastąpić. Na dokumencie 10 MB cheerio plasuje się na dole tego samego przedziału, a nie w klasie parserów opartych na C, z którymi zwykle bywa zestawiane.
W tym artykule pytanie o zamiennik po stronie Node pozostaje otwarte. Nowsze alternatywy dla Node nie były testowane, więc ten wynik nie mówi, że zmiana biblioteki jest niemożliwa ani nie skreśla innych rozwiązań jako mniej dojrzałych. Pokazuje jedynie zmierzoną ścieżkę cheerio na tle wymienionych stosów Python.
To porównanie runtime’ów, a nie tylko bibliotek. Milisekundy cheerio pochodzą z JIT-a i garbage collectora Node; pozostałe z CPython wywołującego parsery oparte na C. Hash treści potwierdza, że wykonano tę samą pracę, a oba wyniki odzwierciedlają rzeczywisty wybór stosu dokonany przez programistę — ale nikt nie powinien tego czytać jako „algorytm cheerio jest 18× gorszy niż selectolax”. Tak wyglądało to na tej maszynie, w natywnym runtime każdej z bibliotek.
Realia konfiguracji
| Biblioteka | Pakiety | Dysk | Licencja | Gwiazdki | Ostatni 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 |
Oficjalne źródło: dokumentacja konfiguracji Cheerio.
metadata-snapshot.json, pobrane w dniu pisania.
npm install cheerio trwało mniej niż dwie sekundy i pobrało 9,0 MiB. Zimny import w osobnym uruchomieniu konwertera na tej samej maszynie zajął 0,056 s.
Jedenaście bezpośrednich zależności to sporo jak na parser i warto to znać, jeśli audytujesz drzewo zależności: htmlparser2, parse5, parse5-htmlparser2-tree-adapter, parse5-parser-stream, domhandler, domutils, dom-serializer, cheerio-select, encoding-sniffer, undici oraz whatwg-mimetype. W środku znajdują się dwie pełne implementacje parsera, ponieważ cheerio może korzystać z jednej albo drugiej, zależnie od tego, o co go poprosisz.
Trzydzieści tysięcy gwiazdek i commit dzień przed testem to w tej kategorii bardzo dobry sygnał utrzymania.
Pamięć i co robi z nią uszkodzony HTML
Zużycie pamięci i zachowanie przy błędnym HTML wpływają na wdrożenie oraz obsługę awarii, dlatego tutaj mierzymy je osobno.
Szerszy kontekst testów obciążeniowych znajduje się w porównaniu pamięci i błędnego HTML dla dziesięciu bibliotek.
Szczytowe zużycie pamięci procesu, mierzone przez /usr/bin/time -l, po jednym świeżym procesie na komórkę — „podłoga” importu pokazuje koszt załadowanej i bezczynnej biblioteki, a wartości szczytowe uwzględniają dokument.
| Biblioteka | Runtime | Podłoga importu | Szczyt 226 KB | Szczyt 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. Basenowe wyniki dla Python i Node nie są ze sobą bezpośrednio porównywalne; interpreter jest częścią obu.
cheerio ma najwyższą podłogę importu w tej mieszanej tabeli — 66,8 MiB, wraz z runtime’em Node i zależnościami. Proces osiągnął 398,5 MiB przy fixture 10 MB. Pozostałe wiersze obejmują parsery, konwertery i ekstraktory artykułów wykonujące inną główną pracę, więc traktuj je jako kontekst zużycia procesu, a nie ranking wydajności między sobą. Wiersz turndown w tym samym runtime miał znacznie wyższy szczyt, ale wykonuje konwersję, a nie kontrakt selekcji tytułów i hrefów mierzony dla cheerio.
Uszkodzony HTML. Dwanaście dokumentów, z których każdy psuje dokładnie jedną rzecz — niezamknięte tagi, źle zagnieżdżone elementy inline, atrybuty bez cudzysłowów zawierające spacje, samotne tagi zamykające, brak <html> w ogóle, zduplikowane atrybuty, dokument urwany w połowie tagu, błędne encje, niezamknięty <script>, fałszywa deklaracja charsetu, komentarz zawierający markup i 600 poziomów zagnieżdżenia — plus dwa poprawne kontrole o dopasowanych rozmiarach, bo „nic nie zwróciło” mówi coś o błędnym HTML tylko wtedy, gdy biblioteka milczy również na czystym dokumencie o tym samym rozmiarze.
cheerio zgłosiło wyjątek w 0 z 14 przypadków i w 0 nie zwróciło nic, odzyskując 11/22 oznaczonych punktów kontrolnych wśród błędnych fixture’ów (malformed-results.json). Dla parserów scorer sprawdza znaczniki nagłówków i linków w jedenastu możliwych do ocenienia błędnych dokumentach; nie ocenia znacznika akapitu, a fixture z niezamkniętym <script> jest wykluczony. Bez baseline’u o tym samym kontrakcie w tej sekcji wynik 11/22 nie jest rankingiem jakości. Uprawniony wniosek jest taki, że cheerio zwróciło niepusty wynik bez wyjątku dla wszystkich czternastu wejść z błędnym HTML-em i kontrolami, odzyskując przy tym połowę ocenianych znaczników.
Zalety i wady
Na plus. Znajoma składnia jQuery. MIT. Aktualny sygnał utrzymania: 30 449 gwiazdek i aktywność repozytorium dzień przed testem. Obecne są dwa backendy parsera i pakiety związane z kodowaniem, choć odzyskiwania po błędach backendu i dokładności kodowania nie izolowano w tym teście. Hash z posortowanych tytułów i hrefów zgadzał się z referencją przy każdym rozmiarze fixture.
Na minus. 18,5× wolniejsze od selectolax na dokumencie 10 MB i 4× na 1 MB. Jedenaście bezpośrednich zależności, w tym dwie pełne implementacje parsera. Tylko Node. I nic w dokumentacji nie sugeruje rozmiaru, przy którym przestaje być oczywistym wyborem.
Kto powinien używać, a kto nie
Użyj cheerio, jeśli pracujesz w Node, cenisz API w stylu jQuery, a przykładowe strony przypominają testowane rozmiary do 1 MB. Jeden megabajt to największy przetestowany punkt przed ostrym skokiem przy 10 MB; ten artykuł nie wyznacza granicy między nimi ani nie twierdzi, jaka część internetu znajduje się poniżej niej.
Najpierw wykonaj benchmark, jeśli masz do czynienia z bardzo dużymi dokumentami HTML, takimi jak generowane raporty, zrzuty katalogów czy długie strony list. Zachowanie XML sitemap nie było testowane. W przypadku fixture HTML 10 MB koszt 2,9 sekundy na dokument to realny wydatek, który się kumuluje.
Jeśli pracujesz w Pythonie, z tego porównania wynika coś innego: selectolax, lxml i PyQuery są praktycznie remisowe od 10 KB wzwyż (w granicach 0,5% do 5,4%, z nakładającymi się zakresami przebiegów), więc wybieraj po API, a nie po szybkości. Najciekawsza liczba to odstęp cheerio od nich wszystkich, a nie różnice między samymi nimi.
Gdzie pasuje zarządzane API
cheerio parsuje HTML, który już masz. Nie pobiera stron, nie renderuje JavaScriptu i nie radzi sobie z warstwą anti-bot — a w wielu realnych zastosowaniach to właśnie ta trudniejsza połowa zadania.
Zarządzana usługa do pobierania/renderowania/ekstrakcji, w tym nasz własny Thunderbit, działa na innej granicy odpowiedzialności. Thunderbit nie był tutaj benchmarkowany. Istotne rozróżnienie polega na tym, że mamy tu parsowanie dostarczonego HTML-a za pomocą selektorów kontra outsourcing pozyskania, renderowania i ekstrakcji; ten artykuł nie daje porównania jakości, opóźnienia ani kosztu przy tych samych metrykach.
Sprawiedliwe ujęcie jest takie: jeśli masz HTML i znasz swoje selektory, cheerio jest darmowe i przyjemne w użyciu. Jeśli pobierasz strony na dużą skalę albo wolisz opisać dane zamiast DOM-u, to już zupełnie inny zakup.
Szerszy przegląd rynku znajdziesz w naszym zestawieniu API do web scrapingu oraz w przeglądzie open-source’owych scraperów dla rozwiązań self-hosted. Jeśli wynik parsowania ma trafić do modelu, konwersja HTML do Markdown w Pythonie pokazuje, gdzie zaczynają się straty w wierności.
Wypróbuj Thunderbit do ekstrakcji danych z sieci
Czy warto używać cheerio?
Tak, w Node — jeśli liczy się dopasowanie API, a reprezentatywne dokumenty pozostają w zakresie od małych do 1 MB.
Znajomość API i aktualne sygnały utrzymania to sensowne kryteria wyboru. Benchmark nie dowodzi, że istnieje konkretna ścieżka wsparcia ani że testowany rozkład rozmiarów stron odpowiada produkcyjnemu korpusowi.
Liczba, którą warto zapamiętać, to ta dla 10 MB. Gdzieś między 1 MB a 10 MB koszt cheerio przestaje podążać za innymi i zaczyna rosnąć wielokrotnie — 4× zmienia się w 18,5×. Jeśli w Twoim korpusie są tak duże dokumenty, zrób benchmark przed wdrożeniem, bo biblioteka sama Cię przed tym nie ostrzeże.
Wypróbuj Thunderbit do ekstrakcji danych z sieci Get Started Free
FAQ
Czy porównywanie cheerio z parserami Python jest uczciwe? To porównanie stosów, a nie algorytmów. Wszystkie cztery rozwiązania zwróciły identyczny posortowany tekst tytułów i hrefy zgodnie z regułą haszowania na 5 z 5 rozmiarów stron. Nie dowodzi to pełnej równoważności parserów. Czas cheerio obejmuje zachowanie runtime’u Node, a pozostałe obejmują CPython wywołujący parsery oparte na C; porównanie opisuje te decyzje end-to-end.
Dlaczego wiersz 1 KB nie jest klasyfikowany? Bo przy 28 mikrosekundach pomiar dominuje szum. W trzech uruchomieniach parsery Python miały rozrzut 77,6%, a ich pojedyncze przebiegi nakładały się na siebie. Każda kolejność w tym rozmiarze byłaby artefaktem. Od 10 KB wzwyż wyniki są już wystarczająco stabilne do interpretacji.
Co powoduje skok przy 10 MB? Ten test tego nie mówi. Ustala natomiast, że skok jest rzeczywisty, a nie losowy: trzy uruchomienia cheerio dały 2 839, 2 928 i 2 954 ms, wyraźnie oddzielone od wszystkiego innego, podczas gdy różnica przy 1 MB wynosiła 4×. Wyizolowanie przyczyny wymagałoby profilowania backendów parsera cheerio osobno, a to wykraczało poza zakres tego testu.
Ile to naprawdę ma zależności?
Jedenaście bezpośrednich, 22 pakiety najwyższego poziomu po rozwiązaniu, 9,0 MiB na dysku. Dwie z nich to pełne implementacje parsera — htmlparser2 i parse5 — ponieważ cheerio może używać jednej albo drugiej. To cena obsługi zarówno parsowania pobłażliwego, jak i zgodnego ze specyfikacją, i warto ją znać przy audycie drzew zależności.
Czego tu nie testowano?
Testy objęły szczytowe zużycie pamięci procesu dla jednego dokumentu 226 KB i jednego 10 MB, a także zestaw 14 wejść z błędnym HTML-em i kontrolami, w którym cheerio nie zgłosiło wyjątku, zwróciło niepusty wynik dla wszystkich i odzyskało 11/22 ocenianych znaczników. Nie testowano streamingu przez parse5-parser-stream, poprawności kodowania, odzyskiwania specyficznego dla backendu, miejsca skoku wydajności między 1 a 10 MB, parsowania XML ani nowszych alternatyw dla Node.


