Chrome headless nouveau et ancien : shell, mode et écarts
Le headless unifié est Chrome sans fenêtre, le shell historique est un binaire séparé. Une exécution mesurée montre ce qui change et ce qui reste différent du mode avec fenêtre.
BotBrowser Team
Vous voulez la documentation structurée pour Plateforme ?
Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.
Chrome livre deux implémentations headless sous un seul nom, et l'écart est architectural plutôt que cosmétique. BotBrowser fournit un profil déclaré qui maintient cohérentes entre elles les surfaces qu'une page peut lire d'un lancement à l'autre, et il ne change pas le binaire que Chrome a lancé ; la question du mode doit être tranchée avant que le profil s'applique. La séparation est facile à énoncer et facile à rater dans un script : un mode est le navigateur complet sans fenêtre visible, l'autre est un exécutable distinct qui ne partage ni le moteur de rendu ni le système d'extensions.
Résumé
- Depuis Chrome 112,
--headlesslance le navigateur complet sans fenêtre ; c'est le sens que le terme porte aujourd'hui. - L'ancienne implémentation est un binaire distinct,
chrome-headless-shell, distribué depuis Chrome 132 uniquement via Chrome for Testing. - Les deux diffèrent sur des points qu'une page peut observer : dans l'exécution consignée ici, le shell a rapporté zéro plugin et aucun objet
window.chrome, alors que le headless unifié a rapporté les deux. - Aucun des deux n'est le mode avec fenêtre, et aucun ne correspond exactement à lui ;
webdrivervalaittruedans les trois cas parce que le client l'a posé, pas à cause du mode. - Puppeteer choisit entre eux avec
headless: trueetheadless: 'shell', et Playwright utilise unexecutablePathou son propre canal. - Vérifiez celui que vous avez lancé en lisant le nom du binaire et le jeton d'agent utilisateur, sans supposer le flag.
Sommaire
- Deux implémentations pour une seule option
- Ce que les trois modes ont rapporté
- Comment chaque outil choisit un mode
- Ce qui diffère encore du mode avec fenêtre
- Ce qui peut être écarté après une différence
- Conclusion pratique
Deux implémentations pour une seule option
L'historique compte parce que la plupart des scripts écrits avant 2023 encodent l'ancienne supposition. Le headless d'origine était une implémentation séparée qui se trouvait livrée dans le même binaire Chrome ; la documentation Chrome indique qu'elle ne partageait pas le code navigateur //chrome. Dans Chrome 112, le navigateur a gagné un mode headless unifié : Chrome crée les fenêtres de plateforme qu'il créerait en mode avec fenêtre et ne les affiche simplement pas, et la documentation décrit toutes les autres fonctions comme disponibles sans limitation.
old headless -> separate implementation, own binary since Chrome 132
new headless -> full Chrome, no visible window, since Chrome 112
Depuis Chrome 132, l'ancienne implémentation n'existe plus que sous la forme de chrome-headless-shell, un binaire autonome publié via le tableau de bord Chrome for Testing, et la documentation Puppeteer ajoute le détail opérationnel que le shell ne reproduit pas entièrement le comportement de Chrome ordinaire tout en étant plus performant pour les tâches qui n'ont pas besoin de l'ensemble complet des fonctions.
Cette seule phrase explique l'essentiel de la confusion dans les résultats de recherche sur le sujet. chrome headless new et chrome headless new vs old ne portent pas sur un flag qui aurait changé de sens deux fois ; ils demandent lequel de deux exécutables un script démarre, et la réponse décide si le chargement d'extensions, les codecs média et l'énumération des plugins se comportent comme l'auteur l'attendait.
Le test pratique n'est pas la chaîne du flag mais l'artefact. Un répertoire de compilation qui contient chrome-headless-shell est l'ancienne implémentation quels que soient les arguments, et un lancement qui pointe vers le navigateur complet avec --headless est l'unifié même si le même mot apparaît sur la ligne de commande.
Il y a une raison pour laquelle les deux vivent sous un seul nom si longtemps. Tous deux répondent à la même question, si un navigateur peut tourner là où personne ne regarde, et tous deux y répondent. Ils divergent seulement quand l'appelant dépend d'un composant que le shell laisse de côté : le système d'extensions, le registre de plugins, le jeu de codecs et les parties de l'interface du navigateur qu'une page peut encore observer via l'objet window. Un pipeline qui ne prend que des captures de pages statiques ne verra pas la différence, et un pipeline qui charge une extension la verra immédiatement.
C'est aussi pourquoi migrer un pipeline est un changement en deux parties plutôt qu'une édition de flag. Passer du shell au headless unifié change ce que le navigateur rapporte, donc les assertions écrites contre les valeurs du shell se mettent à échouer pour une bonne raison ; faire le mouvement inverse les fait repasser, ce qui est pire, parce que le test passe tout en mesurant un navigateur plus petit que celui qu'un utilisateur exécute. Une version figée de Puppeteer rend cela concret : avant v22, la valeur par défaut était le shell, et la documentation consigne que la valeur par défaut est désormais le mode unifié, si bien qu'une simple montée de dépendance change l'implémentation exécutée.
Ce que les trois modes ont rapporté
Une exécution locale a consigné les mêmes champs dans trois sessions sur le même hôte, avec une compilation Chromium standard et un profil jetable par session. Les valeurs ci-dessous sont ce que cette exécution a affiché, pas une affirmation sur toutes les compilations.
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
Deux champs séparent le shell des deux autres, et tous deux sont du type de valeur qu'une page lit en une seule expression. Le shell a rapporté navigator.plugins.length === 0 alors que le headless unifié et le mode avec fenêtre rapportaient cinq, ce qui découle du fait que le shell exclut les composants du navigateur qui enregistrent ces entrées. Le shell a aussi rapporté typeof window.chrome === 'undefined' là où les deux autres rapportaient un objet.
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
Les mesures de fenêtre sont le résultat le plus intéressant, parce qu'elles sont identiques dans les trois cas : innerWidth 1280, innerHeight 720, screen.width 1280, devicePixelRatio 1. Seul outerWidth diffère, et seulement de deux pixels dans la session avec fenêtre, ce qui est un artefact de décoration de fenêtre et non une propriété du mode. C'est la correction utile à une supposition courante : ces trois modes n'ont pas différé par le viewport, donc une comparaison de captures entre eux ne mesure pas ce que l'auteur croit.
L'usage pratique de ce constat est un témoin. Avant d'attribuer une différence de rendu au mode, exécutez la même fixture deux fois dans le même mode et confirmez qu'elle est stable, puis exécutez-la une fois dans l'autre mode et comparez les deux résultats. Si la paire en même mode diffère, la différence est du bruit de mesure venu d'une animation, d'un chargement différé ou d'un minuteur, et la comparaison entre modes ne se lit pas tant que ce point n'est pas réglé. Si la paire en même mode est identique et que la paire entre modes diffère, le mode ou le binaire est une cause plausible, et les quatre champs consignés disent lequel a bougé.
Trois champs volontairement laissés hors du tableau méritent d'être nommés, parce que ce sont ceux que les lecteurs demandent en premier. navigator.webdriver valait true dans les trois sessions, mais c'est le fait du client : chaque bibliothèque d'automatisation ici le pose sur la page avant que le moindre script tourne, donc il ne dit rien du mode et le comparer entre modes produirait un faux constat. Notification.permission valait denied dans les trois cas et navigator.hardwareConcurrency valait 16 dans les trois, ce qui est le nombre de cœurs de l'hôte et non quelque chose que l'état de la fenêtre change.
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
Ce bloc est le résultat négatif dont la comparaison de modes a besoin, parce qu'il retire trois explications candidates avant qu'une différence soit attribuée. Un lecteur qui trouve une différence sur l'un de ces champs entre deux exécutions regarde une différence d'hôte ou de client, pas du headless contre le mode avec fenêtre.
Le jeton d'agent utilisateur est le champ à consigner plutôt qu'à déduire. Les deux modes headless rapportaient HeadlessChrome et le mode avec fenêtre rapportait Chrome, et les fins de version différaient parce que les deux compilations sont à des niveaux de correctif différents, ce qui est une seconde raison de consigner la compilation avec le mode.
Une réserve sur ce jeton compte pour quiconque compare des versions dans le temps. Le jeton est à la fois une substitution côté client et une propriété du binaire : un lancement qui fixe un agent utilisateur personnalisé remplace la chaîne que lit la page alors que le binaire en dessous ne change pas. Le jeton dit donc ce que la page a vu, ce qui est ce qu'un site lit, et le chemin du binaire dit ce qui a réellement tourné. Consigner les deux est ce qui permet à un lecteur ultérieur de distinguer une montée de version d'une substitution.
Comment chaque outil choisit un mode
Les trois clients d'usage courant expriment le choix différemment, et un seul dispose d'une option dédiée.
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 est le seul à nommer le shell dans son API, et la documentation consigne la version où la valeur par défaut a changé : avant v22, Puppeteer lançait l'ancien headless par défaut, et la valeur par défaut actuelle est headless: true, qui est le mode unifié. Un script figé sur un Puppeteer plus ancien exécute donc une implémentation différente de celui écrit aujourd'hui, avec la même ligne de code.
Le fichier de dépendances fait donc partie du dossier. Deux dépôts sur le même commit d'une suite de tests peuvent exécuter des navigateurs différents parce que leurs lockfiles résolvent Puppeteer autrement, et la ligne de lancement qu'ils partagent ne dit rien de celui qui l'a emporté. Consigner la version résolue de Puppeteer à côté du chemin résolu du binaire transforme cela de mystère en diff.
Playwright n'a pas d'option de shell, parce qu'il lance le binaire que nomme le chemin. Pointer le chemin vers chrome-headless-shell est possible et produit le comportement du shell, y compris la liste de plugins absente, ce qui compte si le test vérifie des nombres de plugins. Le pointer vers le navigateur complet avec le headless par défaut donne le mode unifié.
Selenium exprime le choix entièrement par le binaire et les arguments. Dans Chrome actuel, --headless signifie unifié, et l'ancienne implémentation s'atteint en configurant le chemin du binaire du navigateur vers le shell. C'est tout le mécanisme : le flag décrit une fenêtre, le binaire décide de l'implémentation.
Selenium choice = (options.addArguments) + (browser binary path)
flag picks a window; the path picks the implementation
Les tutoriels plus anciens montrent parfois une variante du flag plutôt qu'un chemin vers le shell. Les pages actuelles de la documentation Chrome décrivent --headless et le binaire chrome-headless-shell, et ne documentent pas cette variante ; la vérification fiable est donc celle qu'utilise le reste de la page : résoudre le chemin de l'exécutable qui a réellement tourné, puis lire le jeton d'agent utilisateur.
Chromedriver ajoute une exigence distincte, documentée et facile à enfreindre : la version du driver doit correspondre à celle du navigateur qu'il pilote. La page de sélection de version de ChromeDriver énonce la règle, y compris la séparation antérieure à la 115 où les numéros majeur, mineur et build devaient correspondre. Un décalage est un problème de driver, pas de mode headless, et il se manifeste par une session qui ne démarre pas plutôt que par une session qui rapporte le mauvais mode.
--headless current Chrome: unified
chrome-headless-shell separate download from Chrome for Testing
driver version must match the browser it drives
Le décalage à surveiller est un chromedriver dont la version ne correspond pas au navigateur qu'il pilote, parce qu'un changement de mode se fait souvent en même temps qu'une mise à niveau du navigateur et que les deux échecs se ressemblent. Figer la version de Chrome for Testing couvre la règle des trois artefacts qui garde le navigateur, le driver et le shell sur une même version.
Ce qui diffère encore du mode avec fenêtre
Le mode unifié supprime l'implémentation séparée, pas la différence entre un navigateur sans surveillance et un navigateur avec fenêtre. Trois différences ont survécu à l'exécution ci-dessus, et ce sont celles qu'un test comparant headless et mode avec fenêtre doit prendre en compte.
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
Le support des extensions est la différence qui a changé avec la bascule et qui surprend encore. L'ancien shell n'implémente pas le système d'extensions, alors que le mode unifié exécute le navigateur complet et peut charger une extension non empaquetée dans les conditions qu'exige un lancement avec fenêtre ; l'exigence de contexte persistant et les flags appariés sont consignés dans charger des extensions dans Puppeteer et Playwright.
Cette différence explique aussi une classe de tests qui passent pour la mauvaise raison. Une suite qui attend d'une extension qu'elle injecte un assistant et qui tourne sur le shell n'échouera pas au chargement ; elle échouera plus tard, ou pas du tout, parce que la page ne reçoit jamais l'assistant et que l'assertion qui en dépend est sautée. Vérifier l'ensemble installé au démarrage, comme le décrit le guide sur les extensions, transforme le cas silencieux en cas nommé.
Le chemin de rendu est une seconde différence facile à surestimer. Sur cet hôte, avec cette compilation et une session avec fenêtre accélérée par le matériel, la sortie composée correspondait, mais c'est une mesure et non une garantie : une machine sans GPU utilisable atteint un rasteriseur logiciel, et les polices qu'un conteneur livre décident si le texte se dessine. Ce sont des propriétés de l'hôte, pas du mode, et elles changent indépendamment du 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
Cette séparation est pourquoi une comparaison de headless et de mode avec fenêtre qui ne consigne que le mode ne peut pas expliquer une différence. Comparer les signaux entre headless et mode avec fenêtre est la méthode pour consigner les deux côtés ; préparer un serveur headless est l'endroit où se configure la moitié hôte de cette liste. Aucun des deux ne remplace le fait de savoir quel binaire tourne, qui est le seul fait que cette famille de flags contrôle.
Ce qui peut être écarté après une différence
Une différence entre deux exécutions n'informe qu'une fois les explications bon marché écartées, et les mesures ci-dessus rendent cet ordre explicite. Quand une capture change après un changement de mode, quatre candidats sont sur la liste, et trois peuvent être exclus avant de toucher au navigateur.
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 même discipline vaut pour une décision de migration, qui est la raison pour laquelle la plupart des lecteurs arrivent sur ce sujet. Déplacer une suite du shell vers le headless unifié coûte une reconstruction des suppositions écrites contre le navigateur plus petit : un nombre de plugins de zéro, un objet chrome absent, aucun support d'extensions. Revenir de l'unifié vers le shell ressemble à un gain de performance et réinstalle discrètement les trois, et le seul moyen de le voir est de garder les quatre champs dans le rapport et de les comparer à travers le changement.
Sur ce sujet précis, l'affirmation à éviter est que le mode unifié ne laisse rien à observer. Il en laisse. C'est le navigateur complet sans fenêtre, ce qui veut dire qu'il présente les mêmes composants que le mode avec fenêtre et rapporte encore le mode dans le jeton d'agent utilisateur, dans le comportement de focus de la fenêtre et dans ce que le déploiement fait du GPU et des polices. Un lecteur qui veut que le mode cesse d'apparaître dans chaque champ demande quelque chose qu'aucun flag de lancement ne peut livrer, et un lecteur qui veut mesurer s'il a cessé d'apparaître devrait consigner les champs ci-dessus plutôt que faire confiance au flag.
Une partie de ce bruit mérite d'être quantifiée avant d'être attribuée au mode. Dans les trois sessions ci-dessus, le viewport était identique, donc toute différence de mise en page entre elles venait de la page et non de la taille de fenêtre ; la seule mesure qui a bougé était outerWidth, et elle a bougé de deux pixels dans la seule session qui avait un cadre de fenêtre réel. Quand une comparaison de captures entre deux sessions headless diffère, c'est la première alternative à vérifier, parce que deux sessions de la même implémentation sur le même hôte ne devraient pas différer en mise en page du tout.
La seconde alternative est la substitution de polices. Un conteneur qui ne livre pas de polices CJK dessine ces glyphes en carrés ou en notdef, et cela se produit à l'identique dans les deux modes headless ; ce n'est pas une propriété du flag et changer de mode n'y remédiera pas. Le moyen de distinguer les deux cas est de rendre une chaîne fixe contenant un caractère latin et un caractère CJK puis de comparer le nombre de pixels de la région de texte, qui change quand une police manque et ne change pas quand seul le mode a changé.
Conclusion pratique
Décidez le mode en nommant le binaire puis confirmez-le en lisant le relevé. Dans Puppeteer, headless: true et headless: 'shell' sont des implémentations différentes, pas deux réglages d'une même ; dans Playwright et Selenium le choix vit dans le chemin de l'exécutable, ce qui veut dire qu'une configuration copiée peut changer l'implémentation sans changer aucune option visible. Les utilisateurs de Selenium disposent de la même liste résolue, avec les ChromeOptions qui fixent le mode et les quatre propriétés d’hôte qui changent en CI : Selenium en navigateur headless. Les utilisateurs de Selenium disposent de la même liste résolue, avec les ChromeOptions qui fixent le mode et les quatre propriétés d’hôte qui changent en CI : Selenium en navigateur headless.
Consignez quatre champs à chaque exécution : le chemin résolu du binaire, le jeton d'agent utilisateur, le nombre de plugins et la version de compilation. Ces quatre séparent le shell du headless unifié et l'un ou l'autre du mode avec fenêtre, et ils le font du côté de la page, où un test peut les vérifier sans lire le code de lancement. Quand une différence apparaît entre deux exécutions, vérifiez le binaire d'abord, parce que c'est la variable dont traite ce sujet.
Le résumé honnête de la comparaison à trois est court. Le headless unifié se comporte comme le navigateur complet sans fenêtre ; le shell se comporte comme un navigateur plus petit qui omet des composants ; le mode avec fenêtre se comporte comme un navigateur complet avec une fenêtre. Tout le reste qui change entre deux exécutions sur des machines différentes appartient à l'hôte, pas au flag.
Sources
Cet article fait partie du guide : Surfaces de la plateforme web : carte de revue
Articles Connexes
Scraping headless : ce qui change vraiment
DémarrageSelenium sans fenêtre : ChromeOptions et écarts
DéploiementSignaux headless et avec interface : méthode de comparaison
PlateformeCohérence des profils entre navigateurs headless et headed
EmpreinteChrome for Testing : épingler un build, pas une empreinte
PlateformeCharger une extension dans Puppeteer et Playwright
Exécutez la même vérification dans BotBrowser
Chargez un profil, gardez dans le navigateur les signaux que vous venez de lire, puis confirmez le résultat sur votre propre machine avant le déploiement.