Volver al Centro de conocimiento
Plataforma

Chrome headless nuevo y antiguo: shell, modo y diferencias

El headless unificado es Chrome sin ventana; el shell antiguo es otro binario. Una ejecución medida muestra qué cambia y qué sigue distinto del modo con ventana.

BotBrowser Team

Documentación

Quieres la documentación estructurada de Plataforma?

Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.

Chrome distribuye dos implementaciones headless bajo un mismo nombre, y la diferencia es arquitectónica, no cosmética. BotBrowser proporciona un perfil declarado que mantiene coherentes entre sí las superficies que una página puede leer en cada lanzamiento, y no cambia qué binario lanzó Chrome; la pregunta sobre el modo debe responderse antes de que el perfil se aplique. La división es fácil de enunciar y fácil de equivocar en un script: un modo es el navegador completo sin ventana visible, el otro es un ejecutable aparte que no comparte ni el renderizador ni el sistema de extensiones.

Resumen

  • Desde Chrome 112, --headless ejecuta el navegador completo sin ventana; ese es el significado que el término tiene hoy.
  • La implementación antigua es un binario aparte, chrome-headless-shell, que desde Chrome 132 solo se distribuye a través de Chrome for Testing.
  • Los dos difieren en aspectos que una página puede observar: en la ejecución registrada aquí, el shell informó cero plugins y ningún objeto window.chrome, mientras que el headless unificado informó ambos.
  • Ninguno es el modo con ventana, y ninguno coincide exactamente con él; webdriver valía true en los tres casos porque lo fijó el cliente, no por el modo.
  • Puppeteer elige entre ambos con headless: true y headless: 'shell', y Playwright usa un executablePath o su propio canal.
  • Comprueba cuál lanzaste leyendo el nombre del binario y el token del agente de usuario, no suponiendo el flag.

Contenido

Tres columnas. El headless unificado es el binario completo de Chrome sin ventana e informa plugins y un objeto chrome. El headless shell es un binario aparte que informó cero plugins y ningún objeto chrome. El modo con ventana es la tercera columna, con una ventana real. Un pie señala que el token del agente de usuario, y no el flag, registra cuál se ejecutó.

Dos implementaciones, un mismo flag

La historia importa porque la mayoría de los scripts escritos antes de 2023 codifican la suposición antigua. El headless original era una implementación separada que se distribuía dentro del mismo binario de Chrome; la documentación de Chrome afirma que no compartía el código del navegador //chrome. En Chrome 112 el navegador incorporó un modo headless unificado, en el que Chrome crea las ventanas de plataforma que crearía en modo con ventana y simplemente no las muestra, y la documentación describe todas las demás funciones como disponibles sin limitaciones.

old headless   -> separate implementation, own binary since Chrome 132
new headless   -> full Chrome, no visible window, since Chrome 112

Desde Chrome 132 la implementación antigua solo existe como chrome-headless-shell, un binario independiente publicado en el panel de Chrome for Testing, y la documentación de Puppeteer añade el detalle operativo de que el shell no reproduce del todo el comportamiento de Chrome normal, aunque rinde más en tareas que no necesitan el conjunto completo de funciones.

Esa sola frase explica buena parte de la confusión que hay en los resultados de búsqueda sobre el tema. chrome headless new y chrome headless new vs old no preguntan por un flag que cambió de significado dos veces; preguntan cuál de dos ejecutables está arrancando un script, y la respuesta decide si la carga de extensiones, los códecs multimedia y la enumeración de plugins se comportan como el autor esperaba.

La prueba práctica no es la cadena del flag sino el artefacto. Un directorio de compilación que contiene chrome-headless-shell es la implementación antigua independientemente de los argumentos, y un lanzamiento que apunta al navegador completo con --headless es el unificado aunque la misma palabra aparezca en la línea de comandos.

Hay una razón por la que los dos conviven bajo un mismo nombre durante tanto tiempo. Ambos responden a la misma pregunta, si un navegador puede ejecutarse donde nadie está mirando, y ambos la satisfacen. Solo divergen cuando quien llama depende de un componente que el shell deja fuera: el sistema de extensiones, el registro de plugins, el conjunto de códecs y las partes de la interfaz del navegador que una página todavía puede observar a través del objeto window. Un pipeline que solo toma capturas de páginas estáticas no notará la diferencia, y un pipeline que carga una extensión la notará de inmediato.

Por eso también migrar un pipeline es un cambio de dos partes y no una edición de flag. Pasar del shell al headless unificado cambia lo que informa el navegador, así que las aserciones escritas contra los valores del shell empiezan a fallar por una razón correcta; hacer el movimiento inverso las vuelve a hacer pasar, lo cual es peor, porque la prueba pasa mientras mide un navegador más pequeño que el que ejecuta un usuario. Una versión fijada de Puppeteer hace esto concreto: antes de v22 el valor por defecto era el shell, y la documentación registra que ahora el valor por defecto es el modo unificado, de modo que subir una dependencia por sí solo cambia qué implementación se ejecuta.

Qué informaron los tres modos

Una ejecución local registró los mismos campos en tres sesiones del mismo host, usando una compilación de Chromium sin modificaciones y un perfil desechable por sesión. Los valores de abajo son lo que imprimió esa ejecución, no una afirmación sobre todas las compilaciones.

unified headless  HeadlessChrome/131.0.0.0      plugins=5  chrome=object   1280x720  outerW=1280
headless shell    HeadlessChrome/131.0.6778.33  plugins=0  chrome=undefined 1280x720 outerW=1280
headed            Chrome/131.0.0.0              plugins=5  chrome=object   1280x720  outerW=1282

webdriver=true in all three (the client set it, not the mode)
devicePixelRatio=1 in all three

Dos campos separan al shell de los otros dos, y ambos son el tipo de valor que una página puede leer en una sola expresión. El shell informó navigator.plugins.length === 0 mientras que el headless unificado y el modo con ventana informaron cinco, lo que se deduce de que el shell excluye los componentes del navegador que registran esas entradas. El shell también informó typeof window.chrome === 'undefined' donde los otros dos informaron un objeto.

navigator.plugins.length         shell 0   unified 5   headed 5
typeof window.chrome             shell undefined  unified object  headed object
user agent token                 HeadlessChrome in both headless modes, Chrome in headed

Las medidas de la ventana son el resultado más interesante, porque son iguales en los tres casos: innerWidth 1280, innerHeight 720, screen.width 1280, devicePixelRatio 1. Solo outerWidth difiere, y solo por dos píxeles en la sesión con ventana, lo que es un artefacto de la decoración de la ventana y no una propiedad del modo. Esa es la corrección útil a una suposición común: estos tres modos no difirieron en el viewport, así que una comparación de capturas entre ellos no mide lo que el autor cree.

El uso práctico de ese hallazgo es un control. Antes de atribuir una diferencia de renderizado al modo, ejecuta el mismo fixture dos veces en el mismo modo y confirma que es estable, después ejecútalo una vez en el otro modo y compara los dos resultados. Si el par en el mismo modo difiere, la diferencia es ruido de medición por animación, carga diferida o un temporizador, y la comparación entre modos no se puede leer hasta corregirlo. Si el par en el mismo modo es idéntico y el par entre modos difiere, el modo o el binario son una causa plausible, y los cuatro campos registrados dicen cuál se movió.

Tres campos que se dejaron fuera de la tabla a propósito merecen nombrarse, porque son los primeros por los que pregunta un lector. navigator.webdriver valía true en las tres sesiones, pero eso es obra del cliente: todas las bibliotecas de automatización de este caso lo fijan en la página antes de que se ejecute cualquier script, así que no dice nada sobre el modo y compararlo entre modos produciría un hallazgo falso. Notification.permission leía denied en las tres y navigator.hardwareConcurrency leía 16 en las tres, que es el número de núcleos del host y no algo que el estado de la ventana cambie.

field                       unified   shell   headed
navigator.webdriver         true      true    true    (client-set, not mode)
Notification.permission     denied    denied  denied
navigator.hardwareConcurrency  16      16      16
navigator.language          en-US     en-US   en-US

Ese bloque es el resultado negativo que necesita la comparación de modos, porque elimina tres explicaciones candidatas antes de atribuir cualquier diferencia. Un lector que encuentra una diferencia en uno de esos campos entre dos ejecuciones está viendo una diferencia de host o de cliente, no de headless frente a con ventana.

El token del agente de usuario es el campo que conviene registrar y no inferir. Los dos modos headless informaron HeadlessChrome y el modo con ventana informó Chrome, y las colas de versión diferían porque las dos compilaciones están en niveles de parche distintos, lo que es una segunda razón para registrar la compilación junto con el modo.

Hay una advertencia sobre ese token que importa a cualquiera que compare versiones a lo largo del tiempo. El token es tanto una anulación del lado del cliente como una propiedad del binario: un lanzamiento que fija un agente de usuario personalizado reemplaza la cadena que lee la página mientras el binario de debajo no cambia. Así que el token dice lo que vio la página, que es lo que lee un sitio, y la ruta del binario dice lo que se ejecutó de verdad. Registrar ambos es lo que permite a un lector posterior distinguir una actualización de una anulación.

Cómo elige cada herramienta un modo

Los tres clientes de uso común expresan la elección de forma distinta, y solo uno de ellos tiene una opción dedicada para ello.

puppeteer      headless: true      -> unified headless
               headless: 'shell'   -> chrome-headless-shell
               headless: false     -> headed
playwright     executablePath: ... -> whichever binary the path names
               channel: 'chromium' -> Playwright's bundled build, unified headless by default
selenium       options.addArguments('--headless')            -> unified in current Chrome
               options: binary at chrome-headless-shell       -> old implementation

Puppeteer es el único que nombra el shell en su API, y la documentación registra la versión en la que cambió el valor por defecto: antes de v22, Puppeteer lanzaba el headless antiguo de forma predeterminada, y el valor actual por defecto es headless: true, que es el modo unificado. Un script fijado a un Puppeteer más antiguo ejecuta por tanto una implementación distinta que uno escrito hoy, con la misma línea de código.

El archivo de dependencias forma parte del registro por esa razón. Dos repositorios en el mismo commit de una suite de pruebas pueden ejecutar navegadores distintos porque sus lockfiles resuelven Puppeteer de forma diferente, y la línea de lanzamiento que comparten no dice nada sobre cuál ganó. Registrar la versión resuelta de Puppeteer junto a la ruta resuelta del binario convierte eso de un misterio en un diff.

Playwright no tiene opción de shell, porque lanza el binario que nombra la ruta. Apuntar la ruta a chrome-headless-shell es posible y produce el comportamiento del shell, incluida la lista de plugins ausente, lo que importa si la prueba verifica recuentos de plugins. Apuntarla al navegador completo con el modo headless por defecto da el modo unificado.

Selenium expresa la elección enteramente a través del binario y los argumentos. En Chrome actual, --headless significa modo unificado, y la implementación antigua se alcanza configurando la ruta del binario del navegador hacia el shell. Ese es todo el mecanismo: el flag describe una ventana y el binario decide la implementación.

Selenium choice = (options.addArguments) + (browser binary path)
                 flag picks a window; the path picks the implementation

Los tutoriales más antiguos a veces muestran una variante del flag en lugar de una ruta al shell. Las páginas actuales de la documentación de Chrome describen --headless y el binario chrome-headless-shell, y no documentan esa variante, así que la comprobación fiable es la misma que usa el resto de la página: resolver la ruta del ejecutable que se ejecutó de verdad y leer después el token del agente de usuario.

Chromedriver añade un requisito aparte que está documentado y es fácil de incumplir: la versión del driver tiene que coincidir con la versión del navegador que controla. La página de selección de versión de ChromeDriver enuncia la regla, incluida la división anterior a la 115 en la que tenían que coincidir los números major, minor y build. Un desajuste es un problema del driver, no del modo headless, y se manifiesta como una sesión que no arranca, no como una sesión que informa el modo equivocado.

--headless              current Chrome: unified
chrome-headless-shell   separate download from Chrome for Testing
driver version          must match the browser it drives

El desajuste que hay que vigilar es un chromedriver cuya versión no coincide con la del navegador que controla, porque un cambio de modo suele hacerse al mismo tiempo que una actualización del navegador y los dos fallos se parecen. Fijar la versión de Chrome for Testing cubre la regla de los tres artefactos que mantiene el navegador, el driver y el shell en una misma versión.

Qué sigue cambiando frente al modo con ventana

El modo unificado elimina la implementación separada, no la diferencia entre un navegador sin supervisión y uno con ventana. Tres diferencias sobrevivieron a la ejecución anterior, y son las que una prueba que compara headless con modo con ventana tiene que tener en cuenta.

field                 unified vs headed
user agent token      HeadlessChrome vs Chrome
outerWidth            1280 vs 1282 (window decoration)
window focus/visibility   assumptions about focus do not hold in an unattended session

El soporte de extensiones es la diferencia que cambió con el salto y que todavía sorprende a la gente. El shell antiguo no implementa el sistema de extensiones, mientras que el modo unificado ejecuta el navegador completo y puede cargar una extensión desempaquetada bajo las condiciones que necesita un lanzamiento con ventana; el requisito de contexto persistente y los flags emparejados están registrados en cargar extensiones en Puppeteer y Playwright.

Esa diferencia también explica una clase de prueba que pasa por la razón equivocada. Una suite que espera que una extensión inyecte un ayudante y se ejecuta sobre el shell no fallará al cargar; fallará más tarde, o no fallará, porque la página nunca recibe el ayudante y la aserción que depende de él se omite. Verificar el conjunto instalado al arrancar, como describe ese artículo, convierte el caso silencioso en uno con nombre.

La ruta de renderizado es una segunda diferencia fácil de exagerar. En este host, con esta compilación y una sesión con ventana acelerada por hardware, la salida compuesta coincidió, pero eso es una medición y no una garantía: una máquina sin GPU utilizable llega a un rasterizador por software, y las fuentes que trae un contenedor deciden si el texto se dibuja. Son propiedades del host, no del modo, y cambian con independencia del flag.

mode decides:   whether a window exists, which binary runs, which extensions load
host decides:   GPU path, font set, system libraries, clock and locale
record field:   navigator.plugins.length (0 or 5), window.outerWidth, --load-extension present

Esa separación es la razón por la que una comparación de headless y con ventana que solo registra el modo no puede explicar una diferencia. Comparar señales entre headless y modo con ventana es el método para registrar ambos lados; preparar un servidor headless es donde se configura la mitad del host en esa lista. Ninguno de los dos sustituye a saber qué binario se está ejecutando, que es el único hecho que esta familia de flags controla.

Qué descarta realmente una diferencia

Una diferencia entre dos ejecuciones solo informa cuando ya se descartaron las explicaciones baratas, y las mediciones de arriba hacen explícito ese orden. Cuando una captura cambia tras cambiar de modo, hay cuatro candidatos en la lista, y tres de ellos se pueden excluir antes de tocar el navegador.

1. same-mode rerun        -> identical? animation and lazy loading are excluded
2. viewport fields        -> innerWidth 1280 and dpr 1 in all three here
3. plugin count           -> 0 for the shell, 5 for unified and headed
4. binary path and build  -> the only candidate left when 1 to 3 match

La misma disciplina se aplica a una decisión de migración, que es la razón por la que la mayoría de los lectores llega a este tema. Mover una suite del shell al headless unificado cuesta una reconstrucción de las suposiciones que se escribieron contra el navegador más pequeño: un recuento de plugins de cero, un objeto chrome ausente, sin soporte de extensiones. Volver del unificado al shell parece una mejora de rendimiento y reinstala en silencio las tres cosas, y la única forma de notarlo es mantener los cuatro campos en el informe y compararlos a través del cambio.

Sobre este tema en concreto, la afirmación que conviene evitar es que el modo unificado no deja nada que observar. Sí lo deja. Es el navegador completo sin ventana, lo que significa que presenta los mismos componentes que el modo con ventana y sigue informando el modo en el token del agente de usuario, en el comportamiento de foco de la ventana y en lo que el despliegue haga con la GPU y las fuentes. Un lector que quiere que el modo deje de aparecer en cada campo pide algo que ningún flag de lanzamiento puede entregar, y un lector que quiere medir si dejó de aparecer debería registrar los campos de arriba en lugar de confiar en el flag.

Parte de ese ruido merece cuantificarse antes de atribuirlo al modo. En las tres sesiones anteriores el viewport era idéntico, así que cualquier diferencia de maquetación entre ellas venía de la página y no del tamaño de la ventana; la única medida que se movió fue outerWidth, y se movió dos píxeles en la única sesión que tenía un marco de ventana real. Cuando una comparación de capturas entre dos sesiones headless difiere, esa es la primera alternativa que hay que revisar, porque dos sesiones de la misma implementación en el mismo host no deberían diferir en maquetación en absoluto.

La segunda alternativa es la sustitución de fuentes. Un contenedor que no trae fuentes CJK dibuja esos glifos como cajas o notdef, y eso ocurre igual en los dos modos headless; no es una propiedad del flag y cambiar de modo no lo arreglará. La forma de distinguir ambos casos es renderizar una cadena fija con un carácter latino y uno CJK y comparar el recuento de píxeles de la región de texto, que cambia cuando falta una fuente y no cambia cuando solo cambió el modo.

Conclusión práctica

Decide el modo nombrando el binario y luego confírmalo leyendo el registro. En Puppeteer, headless: true y headless: 'shell' son implementaciones distintas, no dos ajustes de una; en Playwright y Selenium la elección vive en la ruta del ejecutable, lo que significa que una configuración copiada puede cambiar la implementación sin cambiar ninguna opción visible. Quien usa Selenium tiene la misma lista resuelta, con las ChromeOptions que fijan el modo y las cuatro propiedades del host que cambian en CI: Selenium con navegador headless. Quien usa Selenium tiene la misma lista resuelta, con las ChromeOptions que fijan el modo y las cuatro propiedades del host que cambian en CI: Selenium con navegador headless.

Registra cuatro campos en cada ejecución: la ruta resuelta del binario, el token del agente de usuario, el recuento de plugins y la versión de la compilación. Esos cuatro separan el shell del headless unificado y cualquiera de los dos del modo con ventana, y lo hacen desde el lado de la página, donde una prueba puede verificar sobre ellos sin leer el código de lanzamiento. Cuando aparece una diferencia entre dos ejecuciones, revisa el binario primero, porque esa es la variable de la que trata este tema.

El resumen honesto de la comparación de tres vías es corto. El headless unificado se comporta como el navegador completo sin ventana; el shell se comporta como un navegador más pequeño que omite componentes; el modo con ventana se comporta como un navegador completo con una ventana. Todo lo demás que cambia entre dos ejecuciones en máquinas distintas pertenece al host, no al flag.

Fuentes

#Chrome Headless#Headless Shell#Puppeteer#Chromedriver#Renderizado

Este artículo forma parte de la guía: Superficies de la plataforma web: mapa de revisión

Ejecuta la misma comprobación en BotBrowser

Carga un perfil, mantén dentro del navegador las señales que acabas de leer y confirma el resultado en tu propia máquina antes del despliegue.