En una máquina, cheerio en Node analizó y extrajo títulos y hrefs de un documento HTML sintético de 10 MB en 2.927,89 ms. selectolax, ejecutándose en CPython con un parser respaldado por C, completó la misma extracción de campos en 158 ms. Los títulos ordenados y los hashes de hrefs coincidieron. Esta es una comparación de pila completa entre runtimes, no un veredicto aislado sobre el algoritmo del parser.
En una página de 10 KB la diferencia es de 2×, y nadie la notaría. La verdadera pregunta es en qué punto de esa curva caen tus páginas.
Qué es cheerio
cheerio es el parser HTML con sintaxis de jQuery para Node, y por una buena razón suele ser la respuesta por defecto en ese ecosistema: 30.449 estrellas en GitHub, licencia MIT y un push al repositorio el día antes de que yo hiciera esta prueba. Versión evaluada: 1.2.0.
Referencia oficial: Introducción oficial de 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();
Si has usado jQuery, ya conoces la API. Esa familiaridad es, en gran parte, la razón por la que ganó en su ecosistema.
Por debajo no hay un único parser, sino una pila: htmlparser2 y parse5 para el análisis, domhandler y domutils para el árbol, cheerio-select para los selectores, además de undici, encoding-sniffer y otros — once dependencias directas, que resuelven en 22 paquetes de primer nivel y 9,0 MiB en disco. Esos paquetes aportan capacidades de análisis y de codificación, y también suman a la huella de dependencias. Esta revisión probó la salida con entradas malformadas, pero no la corrección de la codificación ni aisló ninguno de los dos motores de parser.
La medición y por qué es fiable
Esta base de investigación ya contaba con un banco de pruebas de parsers: cinco tamaños de página de 1 KB a 10 MB, 50 iteraciones, tres ejecuciones independientes y —lo importante— una compuerta de paridad que hashea el contenido extraído, títulos ordenados más hrefs ordenados, contra un parser de referencia. Un parser que silenciosamente se salta trabajo no puede presumir de tiempo rápido.
Agregar cheerio requirió dos comprobaciones antes de dar por válido cualquier número.
¿El referente seguía cayendo donde debía? selectolax se volvió a ejecutar en la misma sesión y con los mismos archivos de prueba. Su hash de contenido se reprodujo en 5 de 5 tamaños, y su p50 quedó entre 0,989× y 1,079× de la cifra publicada. Así que esta es la misma máquina que generó la tabla original.
¿Cheerio produjo los mismos campos puntuados? Su hash de contenido —calculado en Node con la misma regla, SHA-256 sobre títulos ordenados y hrefs ordenados— coincidió con el referente en 5 de 5 tamaños. Eso demuestra paridad para esos campos ordenados en estos archivos de prueba, no la forma del DOM, el orden del documento, los atributos, la normalización del texto ni la recuperación ante errores.
Solo entonces los tiempos significan algo.
| Tamaño de página | 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 en milisegundos, mediana de tres ejecuciones. parser-bench.json. Los tres parsers de Python se ejecutaron en un solo proceso; cheerio corrió en Node 22, que también marca una frontera de runtime, no solo de biblioteca —ver más abajo.
Leer esa tabla con honestidad

La fila de 1 KB es ruido. Entre los tres parsers de Python, la variación en ese tamaño fue del 77,6%, y las ejecuciones individuales se solapan por completo —selectolax osciló entre 0,0267 y 0,0404 ms a lo largo de sus tres corridas. Con 28 microsegundos, la resolución del temporizador y la planificación del sistema mandan. Yo no clasificaría nada en 1 KB, incluido cheerio.
La parte media de la tabla no tiene nada de especial. Entre 2× y 4× en páginas de 10 KB a 1 MB. Para un scraper que procesa unos pocos cientos de páginas, eso es 45 milisegundos por página en lugar de 15, y no lo vas a percibir.
La fila de 10 MB no es ruido. Las tres ejecuciones de cheerio quedaron en 2.839, 2.928 y 2.954 ms —muy juntas y claramente separadas de las demás filas. El resultado extremo de 10 MB se aparta con fuerza del patrón de tamaños más pequeños. Cinco puntos de tamaño no bastan para establecer complejidad asintótica ni para identificar qué capa del runtime, del parser, del selector, de la asignación de memoria o del recolector de basura provoca el salto.
Se ubica en la franja de BeautifulSoup. El banco publicado midió cuatro parsers más en el mismo archivo de 10 MB, y poner los 2.927,89 ms de cheerio junto a ellos es lo más útil de este artículo:
| 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 |
Las cuatro filas de Python son las cifras publicadas en bench_parse.json; la de cheerio proviene de esta ejecución. El parser de referencia se reprodujo entre 0,989× y 1,079× entre ambas corridas, así que trata diferencias inferiores a aproximadamente 8% como parte de esa incertidumbre —cheerio frente al backend html.parser de BeautifulSoup (5% de diferencia) entra ahí; cheerio frente a selectolax (18×) no.
BeautifulSoup es la biblioteca a la que la gente recurre cuando quiere comodidad y acepta conscientemente que es lenta —es la que en cualquier hilo de rendimiento de Python te dirán que reemplaces. En un documento de 10 MB, cheerio queda en el extremo inferior de esa misma franja, no en la franja de los parsers respaldados por C con los que suele compararse.
La pregunta sobre una sustitución del lado de Node sigue abierta en este artículo. No se probaron alternativas más nuevas de Node, así que este resultado no afirma que cambiar de biblioteca sea imposible ni las descarta por menos consolidadas. Solo muestra la ruta medida de cheerio frente a las pilas de Python enumeradas.
También es una comparación de runtime, no solo de biblioteca. Los milisegundos de cheerio provienen del JIT y del recolector de basura de Node; los demás provienen de CPython llamando a parsers respaldados por C. El hash de contenido demuestra que se hizo el mismo trabajo, y ambas cifras son las que experimenta realmente un desarrollador al elegir una pila —pero nadie debería leer esto como “el algoritmo de cheerio es 18× peor que el de selectolax”. Eso es lo que ocurrió, en esta máquina, dentro del runtime nativo de cada biblioteca.
La realidad de la configuración
| Biblioteca | Paquetes | Disco | Licencia | Estrellas | Último 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 |
Referencia oficial: Documentación de configuración de Cheerio.
metadata-snapshot.json, obtenido el mismo día de redacción.
npm install cheerio tardó menos de dos segundos y descargó 9,0 MiB. La importación en frío se midió en 0,056 s en una ejecución de conversión aparte en la misma máquina.
Once dependencias directas son muchas para un parser, y conviene saberlo si auditas tu árbol: htmlparser2, parse5, parse5-htmlparser2-tree-adapter, parse5-parser-stream, domhandler, domutils, dom-serializer, cheerio-select, encoding-sniffer, undici y whatwg-mimetype. Ahí vienen dos implementaciones completas de parser, porque cheerio puede usar cualquiera según lo que le pidas.
Tener más de treinta mil estrellas y un push el día antes de la prueba es, dentro de esta categoría, una señal de mantenimiento tan sana como puede haber.
Memoria y qué hace el HTML roto con ella
La huella de memoria y el comportamiento ante HTML malformado afectan el despliegue y el manejo de fallos, así que aquí se miden por separado.
El contexto más amplio de la prueba de estrés está en la comparativa de memoria y HTML malformado entre diez bibliotecas.
Memoria residente máxima, vía /usr/bin/time -l, un proceso nuevo por celda —el valor base de importación es lo que cuesta la biblioteca cargada e inactiva; los picos incluyen el documento.
| Biblioteca | Runtime | Base de importación | Pico 226 KB | Pico 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. Las referencias base de Python y Node no son comparables entre sí; el intérprete está dentro de ambas.
cheerio tiene la base de importación más alta en esta tabla mixta de contexto con 66,8 MiB, incluyendo el runtime de Node y sus dependencias. Su proceso alcanzó un pico de 398,5 MiB con el archivo de 10 MB. Las demás filas incluyen parsers, convertidores y extractores de artículos que hacen trabajos principales distintos, así que úsalas como contexto de huella del proceso y no como ranking de rendimiento entre pares. La fila de turndown en el mismo runtime llegó mucho más alto, pero realiza conversión en lugar del contrato de selectores de títulos y hrefs medido para cheerio.
HTML roto. Doce documentos, cada uno rompiendo exactamente una cosa —etiquetas sin cerrar, elementos inline mal anidados, atributos sin comillas con espacios, cierres sobrantes, ausencia total de <html>, atributos duplicados, un documento truncado a mitad de etiqueta, entidades incorrectas, un <script> sin cerrar, una declaración de charset engañosa, un comentario que contiene marcado y 600 niveles de anidamiento— más dos controles bien formados a tamaños equivalentes, porque “no devolvió nada” solo dice algo sobre la malformación si la biblioteca tampoco responde en silencio ante un documento limpio del mismo tamaño.
cheerio lanzó una excepción en 0 de 14 y no devolvió nada en 0, recuperando 11/22 centinelas puntuados en los archivos malformados (malformed-results.json). En parsers, el evaluador comprueba centinelas de encabezados y enlaces en once documentos malformados puntuables; no puntúa el centinela de párrafo, y el archivo con <script> sin cerrar se excluye. Sin una línea base con el mismo contrato en esta sección, 11/22 no es un ranking de calidad. La conclusión que sí puede sostenerse es que cheerio devolvió salida no vacía sin lanzar excepción en las catorce entradas malformadas más control, mientras recuperaba la mitad de los marcadores puntuados.
Pros y contras
A favor. Sintaxis familiar de jQuery. MIT. Una señal de mantenimiento fechada: 30.449 estrellas y actividad en el repositorio el día anterior a la prueba. Hay dos motores de parser y paquetes relacionados con codificación, aunque la recuperación del backend y la precisión de la codificación no se aislaron aquí. El hash ordenado de título más href coincidió con el referente en todos los tamaños de archivo.
En contra. 18,5× más lento que selectolax en un documento de 10 MB, y 4× en 1 MB. Once dependencias directas, incluidas dos implementaciones completas de parser. Solo para Node. Y nada en su documentación sugiere un tamaño a partir del cual deje de ser la opción obvia.
Quién debería usarlo y quién no
Usa cheerio si trabajas en Node, la API tipo jQuery te resulta valiosa y tus páginas representativas se parecen a los tamaños probados hasta 1 MB. Un megabyte es el punto más grande evaluado antes del salto brusco de 10 MB; este artículo no establece el umbral entre ambos ni afirma qué parte de la web queda por debajo.
Haz benchmark antes de usarlo para documentos HTML muy grandes, como informes generados, volcados de catálogos o páginas largas de listados. No se probó el comportamiento con XML sitemap. En el archivo HTML de 10 MB, 2,9 segundos por documento es un coste material que se multiplica.
Si trabajas en Python, esta comparación dice otra cosa: selectolax, lxml y PyQuery están prácticamente empatados desde 10 KB en adelante (dentro de 0,5% a 5,4%, con rangos de ejecución superpuestos), así que elige por API y no por velocidad. La diferencia de cheerio frente a los tres es el dato interesante, no las diferencias entre ellos.
Dónde encaja una API gestionada
cheerio analiza HTML que ya tienes. No descarga páginas, no renderiza JavaScript ni gestiona una capa antibots —y en muchos objetivos reales, esa es la mitad más difícil del trabajo.
Un servicio gestionado de fetch/render/extraction, incluido nuestro propio Thunderbit, vive en otra frontera de responsabilidad. Thunderbit no se midió aquí. La distinción relevante es entre analizar HTML proporcionado con selectores y delegar la adquisición, el renderizado y la extracción; este artículo no ofrece una comparación de calidad, latencia o coste con la misma métrica.
La forma justa de verlo: si ya tienes el HTML y conoces tus selectores, cheerio es gratuito y agradable de usar. Si vas a obtener páginas a escala, o prefieres describir los datos en lugar del DOM, eso es otra compra.
Para un panorama más amplio, nuestra comparativa de APIs de scraping web cubre opciones alojadas y el pilar de scrapers open source las autogestionadas. Si el resultado parseado va a un modelo, convertir HTML a Markdown en Python explica dónde se pierde fidelidad.
Prueba Thunderbit para extraer datos web
¿Deberías usar cheerio?
Sí, en Node, cuando importa que la API encaje y los documentos representativos se mantengan cerca del rango pequeño a 1 MB probado.
La familiaridad con la API y las señales de mantenimiento actuales son criterios de selección totalmente válidos. El benchmark no demuestra que exista una respuesta de soporte concreta ni que la distribución de tamaños probada coincida con tu corpus de producción.
El número que conviene recordar es el de 10 MB. En algún punto entre 1 MB y 10 MB, el coste de cheerio deja de seguir al resto y empieza a multiplicarse —4× pasa a 18,5×. Si tu corpus contiene documentos tan grandes, haz benchmark antes de comprometerte, porque la biblioteca no te avisará.
Prueba Thunderbit para extraer datos web Get Started Free
Preguntas frecuentes
¿Es justo comparar cheerio con parsers de Python? Es una comparación de pilas, no de algoritmos. Los cuatro produjeron exactamente los mismos títulos ordenados y hrefs bajo la regla del hash en 5 de 5 tamaños de página. Eso no prueba una equivalencia total entre parsers. Los tiempos de cheerio incluyen el comportamiento del runtime de Node, mientras que los demás incluyen CPython llamando a parsers respaldados por C; la comparación describe esas decisiones de extremo a extremo.
¿Por qué la fila de 1 KB no se clasifica? Porque con 28 microsegundos la medición está dominada por el ruido. En tres ejecuciones, los parsers de Python se separaron un 77,6%, con corridas individuales superponiéndose entre sí. Cualquier ordenación en ese tamaño sería un artefacto. Desde 10 KB en adelante la lectura es estable.
¿Qué provoca el salto en 10 MB? Esta prueba no lo dice. Lo que sí establece es que el salto es real y no ruido: las tres ejecuciones de cheerio quedaron en 2.839, 2.928 y 2.954 ms, muy separadas del resto, mientras que la brecha en 1 MB era de 4×. Aislar la causa implicaría perfilar por separado los motores de parser de cheerio, algo que quedaba fuera del alcance aquí.
¿Cuántas dependencias tiene realmente?
Once directas, 22 de primer nivel tras resolver, 9,0 MiB en disco. Dos de ellas son implementaciones completas de parser —htmlparser2 y parse5— porque cheerio puede usar cualquiera. Ese es el precio de manejar tanto parsing tolerante como compatible con la especificación, y conviene saberlo si auditas árboles de dependencias.
¿Qué no se probó aquí?
Las pruebas cubrieron el pico de memoria del proceso en un documento de 226 KB y otro de 10 MB, además de un conjunto de catorce entradas malformadas más control en el que cheerio no lanzó errores, devolvió salida no vacía en todas y recuperó 11/22 centinelas puntuados. No se probó el streaming vía parse5-parser-stream, la corrección de codificación, la recuperación específica de cada backend, la ubicación exacta del salto de rendimiento entre 1 y 10 MB, el parsing XML ni alternativas más nuevas de Node.


