En una máquina, cheerio en Node analizó y extrajo los títulos y los href de un documento HTML sintético de 10 MB en 2,927.89 ms. selectolax, ejecutándose a través de CPython con un parser apoyado en C, completó la misma extracción de campos en 158 ms. Los títulos ordenados y los hashes de href coincidieron. Se trata de una comparación integral de pila entre entornos de ejecución, no de un veredicto aislado sobre el algoritmo del parser.
En una página de 10 KB la diferencia es de 2×, y nadie se daría cuenta. La cuestión real es dónde se sitúan tus páginas dentro de esa curva.
Qué es cheerio
cheerio es el parser HTML para Node con sintaxis tipo jQuery, y por una buena razón suele ser la respuesta por defecto en ese ecosistema: 30,449 estrellas en GitHub, licencia MIT y un commit en el repositorio el día antes de la 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 trabajado con jQuery, ya conoces la API. Esa familiaridad es una de las principales razones por las que ganó terreno en su ecosistema.
Por dentro, no es 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 codificación, pero también aumentan la huella de dependencias. Esta revisión probó la salida ante entradas malformadas, pero no evaluó la corrección de la codificación ni aisló ninguno de los dos motores de parsing.
La medición y por qué es confiable
Esta base de investigación ya contaba con un benchmark de parsers: cinco tamaños de página de 1 KB a 10 MB, 50 iteraciones, tres ejecuciones independientes y —lo importante— una validación de paridad que calcula hash del contenido extraído, títulos ordenados más href ordenados, frente a un parser de referencia. Un parser que omite trabajo en silencio no puede aparentar un tiempo rápido.
Añadir cheerio a esa base exigía dos comprobaciones antes de considerar cualquier cifra.
¿El referente seguía dando el mismo resultado? selectolax se volvió a ejecutar en la misma sesión y sobre los mismos fixtures. 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 el texto de títulos ordenado y los href ordenados— coincidió con el referente en 5 de 5 tamaños. Eso demuestra paridad para esos campos ordenados en estos fixtures, no para 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, lo que también supone una frontera de runtime, además de la de librería —ver más abajo.
Cómo leer esa tabla con honestidad

La fila de 1 KB es ruido. Entre los tres parsers de Python, la dispersión en ese tamaño fue del 77.6%, y las ejecuciones individuales se solapan muchísimo —selectolax osciló entre 0.0267 y 0.0404 ms en sus tres corridas. Con 28 microsegundos, dominan la resolución del temporizador y la planificación del sistema. Yo no clasificaría nada en 1 KB, incluido cheerio.
La zona media de la tabla no tiene nada de especial. Entre 10 KB y 1 MB, las diferencias están entre 2× y 4×. Para un scraper que procese unos cientos de páginas, eso significa 45 milisegundos por página en lugar de 15, y no lo notarás.
La fila de 10 MB no es ruido. Las tres corridas de cheerio dieron 2,839, 2,928 y 2,954 ms —muy consistentes y claramente separadas de las demás filas. El resultado extremo de 10 MB se aparta de forma marcada del patrón de los tamaños más pequeños. Cinco puntos de tamaño no bastan para establecer complejidad asintótica ni para identificar si el salto viene del runtime, el parser, los selectores, la asignación de memoria o la recolección de basura.
Se sitúa en la franja de BeautifulSoup. El benchmark publicado midió otros cuatro parsers sobre el mismo fixture de 10 MB, y colocar 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 alrededor del 8% como parte de esa incertidumbre —cheerio frente al backend html.parser de BeautifulSoup (separados por un 5%) entra en ese margen; cheerio frente a selectolax (18×) no.
BeautifulSoup es la librería a la que la gente recurre por comodidad sabiendo que es lenta; es la que en todos los hilos de rendimiento de Python te dicen que reemplaces. En un documento de 10 MB, cheerio queda en la parte baja de ese mismo grupo, no en el grupo de los parsers apoyados en C con los que suele compararse.
La pregunta sobre una alternativa nativa en Node sigue abierta en este artículo. No se probaron alternativas más nuevas en Node, así que este resultado no permite afirmar que cambiar de librería sea imposible ni descartarlas por menos maduras. 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 librería. Los milisegundos de cheerio vienen del JIT y del recolector de basura de Node; los demás provienen de CPython llamando a parsers apoyados en C. El hash de contenido demuestra que se hizo el mismo trabajo, y ambas cifras reflejan lo que experimenta un desarrollador al elegir una pila —pero nadie debería leer esto como “el algoritmo de cheerio es 18× peor que el de selectolax”. Es lo que ocurrió, en esta máquina, dentro del runtime nativo de cada librería.
Realidad de la configuración
| Librería | 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 día en que se escribió.
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 aparte del convertidor sobre la misma máquina.
Once dependencias directas son muchas para un parser, y conviene tenerlas presentes 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í se incluyen dos implementaciones completas de parser, porque cheerio puede usar cualquiera de las dos según lo que le pidas.
Treinta mil estrellas y un push el día anterior a la prueba son señales de mantenimiento tan sólidas como cabría esperar en esta categoría.
Memoria y qué hace el HTML roto con ella
La huella de memoria y el comportamiento ante HTML mal formado influyen en el despliegue y en la gestión de fallos, así que aquí se miden por separado.
El contexto ampliado de la prueba de estrés está en la comparación de memoria y HTML malformado de diez librerías.
Memoria residente pico, mediante /usr/bin/time -l, un proceso nuevo por celda —el piso de importación es lo que cuesta la librería cargada e inactiva; los picos incluyen el documento.
| Librería | Runtime | Piso 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 el piso de importación más alto en esta tabla mixta, con 66.8 MiB, incluyendo el runtime de Node y sus dependencias. Su proceso alcanzó un máximo de 398.5 MiB con el fixture de 10 MB. Las demás filas incluyen parsers, conversores y extractores de artículos con trabajos principales distintos, así que úsalas como contexto de huella de proceso, no como ranking de rendimiento entre iguales. La fila de turndown en el mismo runtime alcanzó un pico mucho mayor, pero realiza conversión y no el contrato de selección de títulos y href medido para cheerio.
HTML roto. Doce documentos que rompen 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 inválidas, un <script> sin cerrar, una declaración de charset engañosa, un comentario que contiene marcado y 600 niveles de anidación— más dos controles bien formados de tamaño equivalente, porque “no devolvió nada” solo habla de malformación si la librería tampoco permanece en silencio ante un documento limpio del mismo tamaño.
cheerio lanzó error en 0 de 14 y no devolvió nada en 0, recuperando 11/22 marcadores puntuados en los fixtures malformados (malformed-results.json). En parsers, el sistema de puntuación comprueba los marcadores de encabezado y enlaces en once documentos malformados evaluables; no puntúa el marcador de párrafo, y el fixture con <script> sin cerrar queda excluido. Sin una referencia con el mismo contrato en esta sección, 11/22 no es un ranking de calidad. La conclusión que sí está respaldada es que cheerio devolvió salida no vacía sin lanzar errores en los catorce inputs malformados y de control, mientras recuperaba la mitad de los marcadores puntuados.
Pros y contras
A favor. Sintaxis jQuery familiar. MIT. Una señal de mantenimiento con fecha: 30,449 estrellas y actividad en el repositorio el día anterior a la prueba. Hay dos motores de parsing y paquetes relacionados con la codificación, aunque aquí no se aisló la recuperación del backend ni la precisión de codificación. El hash de títulos y href ordenados coincidió con el referente en todos los tamaños de fixture.
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 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 con forma de jQuery te resulta valiosa y tus páginas representativas se parecen a los tamaños probados hasta 1 MB. Un megabyte es el mayor punto probado 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 sitemaps XML. En el fixture HTML de 10 MB, 2.9 segundos por documento es un coste material que se multiplica.
Si estás en Python, esta comparación dice otra cosa: selectolax, lxml y PyQuery prácticamente empatan a partir de 10 KB (dentro de un 0.5% a 5.4%, con rangos de ejecución solapados), así que elige por API, no por velocidad. La diferencia de cheerio frente a los tres es el número interesante, no las pequeñas diferencias entre ellos.
Dónde encaja una API gestionada
cheerio analiza HTML que ya tienes. No descarga, no renderiza JavaScript ni se ocupa de una capa antibots —y en muchos objetivos reales esa es la mitad más difícil del trabajo.
Un servicio gestionado de obtención/renderizado/extracción, incluido nuestro Thunderbit, ocupa una frontera de responsabilidad distinta. Thunderbit no se evaluó aquí. La distinción relevante es entre analizar HTML proporcionado con selectores y externalizar 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 formulación justa: si tú controlas el HTML y conoces tus selectores, cheerio es gratis y agradable de usar. Si vas a capturar páginas a escala, o prefieres describir los datos en lugar del DOM, eso ya es otra compra.
Para una visión más amplia, nuestro resumen de APIs de web scraping cubre opciones alojadas y el pilar de scrapers open source las autogestionadas. Si la salida analizada va a alimentar 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 la compatibilidad de la API importa y los documentos representativos se mantienen cerca del rango pequeño a 1 MB probado.
La familiaridad de la API y las señales actuales de mantenimiento son factores de selección legítimos. El benchmark no demuestra que exista una respuesta concreta de soporte ni que la distribución de tamaños de página probada coincida con un corpus de producción.
Quédate con la cifra de 10 MB. En algún punto entre 1 MB y 10 MB, el coste de cheerio deja de seguir al de las demás opciones y empieza a multiplicarse —4× pasa a 18.5×. Si tu corpus incluye documentos tan grandes, haz benchmark antes de comprometerte, porque nada en la librería 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 pila, no de algoritmo. Los cuatro produjeron el mismo texto de títulos y los mismos href ordenados bajo la regla de hash en 5 de 5 tamaños de página. Eso no prueba equivalencia total de parsers. Los tiempos de cheerio incluyen el comportamiento del runtime de Node, mientras que los demás incluyen CPython llamando a parsers apoyados en C; la comparación describe esas decisiones de extremo a extremo.
¿Por qué la fila de 1 KB no se clasifica? Porque a 28 microsegundos la medición está dominada por ruido. En tres ejecuciones, los parsers de Python tuvieron una dispersión del 77.6%, con solapamiento entre corridas. Cualquier orden en ese tamaño sería un artefacto. A partir de 10 KB la lectura ya 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 corridas de cheerio terminaron en 2,839, 2,928 y 2,954 ms, claramente separadas del resto, mientras que la diferencia en 1 MB era de 4×. Aislar la causa implicaría perfilar por separado los motores de parsing de cheerio, lo cual quedaba fuera del alcance.
¿Cuántas dependencias tiene realmente?
Once directas, 22 de primer nivel tras la resolución, 9.0 MiB en disco. Dos de ellas son implementaciones completas de parser —htmlparser2 y parse5— porque cheerio puede usar cualquiera de las dos. Ese es el coste de soportar tanto parsing flexible como conforme a la especificación, y conviene saberlo si auditas árboles de dependencias.
¿Qué no se probó aquí?
El borrador sí probó el pico de memoria del proceso con un documento de 226 KB y otro de 10 MB, y un conjunto de catorce entradas entre malformadas y de control en el que cheerio no lanzó errores, devolvió salida no vacía en todas y recuperó 11/22 marcadores puntuados. No se probó el streaming mediante parse5-parser-stream, la corrección de la codificación, la recuperación específica del backend, la ubicación exacta del salto de rendimiento entre 1 y 10 MB, el parsing XML ni alternativas más nuevas en Node. Los enlaces crudos a artefactos relativos también requieren la misma estructura pública de directorios al publicar; de lo contrario, hacen falta URLs públicas duraderas o una referencia a un commit del repositorio.


