Chrome headless новый и старый: shell, режим и различия
Унифицированный headless это Chrome без окна, старый shell это отдельный бинарник. Один измеренный запуск показывает, что меняется и что остаётся иным от режима с окном.
BotBrowser Team
Нужна структурированная документация по теме Платформа?
Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.
Chrome поставляет две реализации headless под одним именем, и разница здесь архитектурная, а не косметическая. BotBrowser предоставляет объявленный профиль, который сохраняет согласованность поверхностей, доступных странице для чтения, между запусками, и он не меняет то, какой бинарник запустил Chrome; вопрос о режиме нужно решить до того, как профиль вступит в силу. Это разделение легко сформулировать и легко испортить в скрипте: один режим это полный браузер без видимого окна, другой это отдельный исполняемый файл, который не разделяет ни рендерер, ни систему расширений.
Кратко
- Начиная с Chrome 112,
--headlessзапускает полный браузер без окна; именно это сегодня означает данный термин. - Старая реализация это отдельный бинарник
chrome-headless-shell, который с Chrome 132 распространяется только через Chrome for Testing. - Они различаются в том, что страница может наблюдать: в записанном здесь запуске shell сообщил ноль плагинов и отсутствие объекта
window.chrome, а унифицированный headless сообщил и то, и другое. - Ни один из них не является режимом с окном, и ни один не совпадает с ним точно;
webdriverбыл равенtrueво всех трёх случаях потому, что его установил клиент, а не режим. - Puppeteer выбирает между ними через
headless: trueиheadless: 'shell', а Playwright используетexecutablePathили собственный канал. - Проверяйте, что именно вы запустили, по имени бинарника и токену пользовательского агента, а не по предположению о флаге.
Содержание
- Две реализации под одним флагом
- Что показали три режима
- Как каждая библиотека выбирает режим
- Что по-прежнему отличается от режима с окном
- Что на самом деле исключает различие
- Практический итог
Две реализации под одним флагом
История важна потому, что большинство скриптов, написанных до 2023 года, кодируют старое допущение. Исходный headless был отдельной реализацией, которая поставлялась внутри того же бинарника Chrome; документация Chrome указывает, что он не разделял код браузера //chrome. В Chrome 112 у браузера появился унифицированный режим headless: Chrome создаёт те же оконные объекты платформы, что и в режиме с окном, и просто не показывает их, а документация описывает все остальные функции как доступные без ограничений.
old headless -> separate implementation, own binary since Chrome 132
new headless -> full Chrome, no visible window, since Chrome 112
С Chrome 132 старая реализация доступна только как chrome-headless-shell, отдельный бинарник, публикуемый через панель Chrome for Testing, а документация Puppeteer добавляет операционную деталь: shell не воспроизводит поведение обычного Chrome полностью, хотя работает быстрее в задачах, которым не нужен полный набор возможностей.
Одно это предложение объясняет большую часть путаницы в результатах поиска по теме. chrome headless new и chrome headless new vs old спрашивают не о флаге, который дважды сменил смысл; они спрашивают, какой из двух исполняемых файлов стартует скрипт, и ответ решает, будут ли загрузка расширений, медиакодеки и перечисление плагинов вести себя так, как ожидал автор.
Практическая проверка это не строка флага, а артефакт. Каталог сборки, в котором лежит chrome-headless-shell, это старая реализация независимо от аргументов, а запуск, указывающий на полный браузер с --headless, это унифицированный режим, хотя на командной строке стоит то же слово.
Есть причина, по которой они так долго живут под одним именем. Оба отвечают на один вопрос, может ли браузер работать там, где никто не смотрит, и оба на него отвечают. Они расходятся только тогда, когда вызывающий код зависит от компонента, который shell опускает: системы расширений, реестра плагинов, набора кодеков и тех частей интерфейса браузера, которые страница всё ещё может наблюдать через объект window. Конвейер, который только снимает скриншоты статических страниц, разницы не заметит, а конвейер, который загружает расширение, заметит её сразу.
Поэтому миграция конвейера это изменение из двух частей, а не правка флага. Переход от shell к унифицированному headless меняет то, что сообщает браузер, поэтому проверки, написанные под значения shell, начинают падать по правильной причине; обратный переход снова заставляет их проходить, что хуже, потому что тест проходит, измеряя браузер меньше того, в котором работает пользователь. Зафиксированная версия Puppeteer делает это наглядным: до v22 значением по умолчанию был shell, а документация фиксирует, что теперь по умолчанию используется унифицированный режим, так что одно обновление зависимости меняет выполняемую реализацию.
Что показали три режима
Один локальный запуск записал одни и те же поля в трёх сессиях на одном хосте, используя стандартную сборку Chromium и одноразовый профиль на сессию. Значения ниже это то, что напечатал тот запуск, а не утверждение о любой сборке.
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
Два поля отделяют shell от двух других, и оба относятся к значениям, которые страница читает одним выражением. Shell сообщил navigator.plugins.length === 0, тогда как унифицированный headless и режим с окном сообщили пять, что следует из того, что shell исключает компоненты браузера, регистрирующие эти записи. Shell также сообщил typeof window.chrome === 'undefined' там, где два других сообщили объект.
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
Измерения окна это более интересный результат, потому что они одинаковы во всех трёх случаях: innerWidth 1280, innerHeight 720, screen.width 1280, devicePixelRatio 1. Отличается только outerWidth, и всего на два пикселя в сессии с окном, что является артефактом оформления окна, а не свойством режима. Это полезная поправка к распространённому допущению: эти три режима не различались по вьюпорту, поэтому сравнение скриншотов между ними измеряет не то, что думает автор.
Практическое применение этой находки это контроль. Прежде чем приписывать разницу в рендеринге режиму, запустите ту же фикстуру дважды в одном режиме и убедитесь, что она стабильна, затем запустите её один раз в другом режиме и сравните два результата. Если пара в одном режиме различается, разница это шум измерения от анимации, ленивой загрузки или таймера, и сравнение между режимами нельзя читать, пока это не исправлено. Если пара в одном режиме идентична, а пара между режимами различается, режим или бинарник являются правдоподобной причиной, и четыре записанных поля говорят, что именно сдвинулось.
Три поля, намеренно оставленные вне таблицы, стоит назвать, потому что о них читатели спрашивают первыми. navigator.webdriver был равен true во всех трёх сессиях, но это дело клиента: каждая библиотека автоматизации здесь устанавливает его на странице до запуска любого скрипта, так что он ничего не говорит о режиме, и сравнивать его между режимами означало бы получить ложную находку. Notification.permission читался как denied во всех трёх, а navigator.hardwareConcurrency читался как 16 во всех трёх, что является числом ядер хоста, а не чем-то, что меняет состояние окна.
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
Этот блок и есть отрицательный результат, нужный сравнению режимов, потому что он снимает три кандидата в объяснения до того, как разнице что-то приписывают. Читатель, который находит разницу в одном из этих полей между двумя запусками, смотрит на разницу хоста или клиента, а не на headless против режима с окном.
Токен пользовательского агента это поле, которое стоит записывать, а не выводить. Оба режима headless сообщили HeadlessChrome, а режим с окном сообщил Chrome, и хвосты версий различались, потому что две сборки находятся на разных уровнях патча, что является второй причиной записывать сборку вместе с режимом.
Есть оговорка об этом токене, важная для всех, кто сравнивает версии во времени. Токен это и клиентская подмена, и свойство бинарника: запуск с пользовательским агентом заменяет строку, которую читает страница, хотя бинарник под ней не меняется. Так что токен говорит, что увидела страница, а это то, что читает сайт, а путь к бинарнику говорит, что действительно работало. Записывать оба это то, что позволяет более позднему читателю отличить обновление от подмены.
Как каждая библиотека выбирает режим
Три распространённых клиента выражают выбор по-разному, и только у одного есть отдельная опция для этого.
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 единственный, кто называет shell в своём API, и документация фиксирует версию, где изменилось значение по умолчанию: до v22 Puppeteer по умолчанию запускал старый headless, а текущее значение по умолчанию это headless: true, то есть унифицированный режим. Скрипт, зафиксированный на более старом Puppeteer, поэтому выполняет другую реализацию, чем написанный сегодня, при той же строке кода.
Файл зависимостей поэтому является частью записи. Два репозитория на одном коммите набора тестов могут запускать разные браузеры, потому что их lockfiles разрешают Puppeteer по-разному, и общая строка запуска ничего не говорит о том, кто победил. Запись разрешённой версии Puppeteer рядом с разрешённым путём к бинарнику превращает это из загадки в diff.
У Playwright нет опции shell, потому что он запускает бинарник, указанный путём. Направить путь на chrome-headless-shell возможно, и это даёт поведение shell, включая отсутствующий список плагинов, что важно, если тест проверяет количество плагинов. Направление на полный браузер с headless по умолчанию даёт унифицированный режим.
Selenium выражает выбор целиком через бинарник и аргументы. В текущем Chrome --headless означает унифицированный режим, а старая реализация достигается настройкой пути к бинарнику браузера на shell. В этом весь механизм: флаг описывает окно, а бинарник решает реализацию.
Selenium choice = (options.addArguments) + (browser binary path)
flag picks a window; the path picks the implementation
Старые руководства иногда показывают вариант флага вместо пути к shell. Текущие страницы документации Chrome описывают --headless и бинарник chrome-headless-shell и не документируют этот вариант, поэтому надёжная проверка та же, что и в остальной части материала: определить путь к исполняемому файлу, который действительно работал, а затем прочитать токен пользовательского агента.
Chromedriver добавляет отдельное требование, которое задокументировано и которое легко нарушить: версия драйвера должна совпадать с версией браузера, которым он управляет. Страница выбора версии ChromeDriver формулирует это правило, включая разделение до версии 115, где должны были совпадать номера major, minor и build. Несовпадение это проблема драйвера, а не режима headless, и проявляется оно как сессия, которая не стартует, а не как сессия, сообщающая неверный режим.
--headless current Chrome: unified
chrome-headless-shell separate download from Chrome for Testing
driver version must match the browser it drives
Несоответствие, за которым стоит следить, это chromedriver, версия которого не совпадает с версией браузера, которым он управляет, потому что смена режима часто происходит одновременно с обновлением браузера и оба отказа выглядят похоже. Фиксация версии Chrome for Testing описывает правило трёх артефактов, которое держит браузер, драйвер и shell на одной версии.
Что по-прежнему отличается от режима с окном
Унифицированный режим убирает отдельную реализацию, а не разницу между браузером без присмотра и браузером с окном. Три различия пережили запуск выше, и именно их должен учитывать тест, сравнивающий headless с режимом с окном.
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
Поддержка расширений это различие, которое изменилось с переключением и до сих пор застаёт людей врасплох. Старый shell не реализует систему расширений, тогда как унифицированный режим исполняет полный браузер и может загрузить распакованное расширение при условиях, которые нужны запуску с окном; требование постоянного контекста и парные флаги записаны в загрузке расширений в Puppeteer и Playwright.
Это различие также объясняет класс тестов, которые проходят по неверной причине. Набор, который ожидает, что расширение внедрит помощник, и работает на shell, не упадёт при загрузке; он упадёт позже или не упадёт вовсе, потому что страница никогда не получает помощник, а проверка, которая от него зависит, пропускается. Проверка установленного набора при старте, как описано в той статье, превращает молчаливый случай в названный.
Путь рендеринга это второе различие, которое легко переоценить. На этом хосте, с этой сборкой и сессией с окном с аппаратным ускорением, итоговый вывод совпал, но это одно измерение, а не гарантия: машина без пригодного GPU приходит к программному растеризатору, а шрифты, которые поставляет контейнер, решают, будет ли текст отрисован вообще. Это свойства хоста, а не режима, и они меняются независимо от флага.
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
Это разделение и есть причина, по которой сравнение headless и режима с окном, записывающее только режим, не может объяснить разницу. Сравнение сигналов headless и режима с окном это метод записи обеих сторон; настройка headless-сервера это место, где настраивается половина списка, относящаяся к хосту. Ни то, ни другое не заменяет знание о том, какой бинарник работает, а это единственный факт, которым управляет это семейство флагов.
Что на самом деле исключает различие
Различие между двумя запусками информативно только после того, как дешёвые объяснения сняты, и измерения выше делают этот порядок явным. Когда скриншот меняется после смены режима, в списке четыре кандидата, и три из них можно исключить до того, как трогать браузер.
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
Та же дисциплина применима к решению о миграции, и именно поэтому большинство читателей приходит к этой теме. Перемещение набора от shell к унифицированному headless стоит перестройки допущений, написанных под меньший браузер: количество плагинов ноль, отсутствующий объект chrome, нет поддержки расширений. Возврат от унифицированного режима к shell выглядит как выигрыш в производительности и тихо восстанавливает все три, и единственный способ это заметить это держать четыре поля в отчёте и сравнивать их через изменение.
Именно по этой теме утверждение, которого стоит избегать, состоит в том, что унифицированный режим не оставляет ничего для наблюдения. Оставляет. Это полный браузер без окна, а значит, он предоставляет те же компоненты, что и режим с окном, и по-прежнему сообщает режим в токене пользовательского агента, в поведении фокуса окна и в том, что развёртывание делает с GPU и шрифтами. Читатель, который хочет, чтобы режим перестал появляться в каждом поле, просит то, что не может дать ни один флаг запуска, а читатель, который хочет измерить, исчез ли он, должен записывать поля выше, а не доверять флагу.
Часть этого шума стоит измерить до того, как приписывать его режиму. В трёх сессиях выше вьюпорт был идентичен, поэтому любая разница вёрстки между ними шла от страницы, а не от размера окна; единственное измерение, которое сдвинулось, это outerWidth, и он сдвинулся на два пикселя в единственной сессии с настоящей рамкой окна. Когда сравнение скриншотов между двумя сессиями headless различается, это первая альтернатива для проверки, потому что две сессии одной реализации на одном хосте не должны различаться по вёрстке вообще.
Вторая альтернатива это подмена шрифтов. Контейнер, который не поставляет шрифты CJK, рисует эти глифы как квадраты или notdef, и это происходит одинаково в обоих режимах headless; это не свойство флага, и смена режима это не исправит. Отличить два случая можно, отрисовав фиксированную строку с одним латинским и одним CJK символом и сравнив количество пикселей в области текста, которое меняется при отсутствии шрифта и не меняется, когда сменился только режим.
Практический итог
Решайте режим, называя бинарник, а затем подтверждайте его, читая запись. В Puppeteer headless: true и headless: 'shell' это разные реализации, а не две настройки одной; в Playwright и Selenium выбор живёт в пути к исполняемому файлу, а значит, скопированная конфигурация может сменить реализацию, не меняя ни одной видимой опции. У пользователей Selenium есть тот же список с решением: Selenium с headless-браузером собирает ChromeOptions для режима и четыре свойства хоста, которые меняются в CI.
Записывайте четыре поля при каждом запуске: разрешённый путь к бинарнику, токен пользовательского агента, количество плагинов и версию сборки. Эти четыре отделяют shell от унифицированного headless и любой из них от режима с окном, и делают это со стороны страницы, где тест может их проверять, не читая код запуска. Когда между двумя запусками появляется разница, проверяйте бинарник первым, потому что это та переменная, о которой идёт речь в этой теме.
Честное резюме сравнения трёх режимов короткое. Унифицированный headless ведёт себя как полный браузер без окна; shell ведёт себя как меньший браузер, который опускает компоненты; режим с окном ведёт себя как полный браузер с окном. Всё остальное, что меняется между двумя запусками на разных машинах, принадлежит хосту, а не флагу.
Источники
Эта статья входит в руководство: Поверхности веб-платформы: карта проверки
Похожие статьи
Headless-скрейпинг: что действительно меняется
Начало работыSelenium без окна: ChromeOptions и различия
РазвертываниеСигналы безголового и обычного браузера: метод сравнения
ПлатформаСогласованность профиля в headless и headed браузере
ОтпечаткиChrome for Testing: фиксируем build, а не отпечаток
ПлатформаЗагрузка расширений в Puppeteer и Playwright
Выполните ту же проверку в BotBrowser
Загрузите профиль, сохраните внутри браузера те сигналы, о которых только что прочитали, и подтвердите результат на своей машине до раскатки.