Развертывание

Надёжные скриншоты в headless-браузере

Практический процесс надёжных headless-скриншотов с viewport профиля, явным состоянием готовности, нативным снимком всей страницы, проверкой нижней части и памятью контейнера.

Документация

Нужна поддерживаемая продуктовая документация?

У этой статьи есть соответствующая страница в центре документации. Используйте docs для каноничного сценария настройки, актуальных флагов и долгосрочной справки.

Начните с viewport профиля

Скриншот полезен только тогда, когда он представляет страницу, которую должен был отрисовать браузер. В сессии с профилем viewport является началом контракта рендеринга. Он влияет на переносы строк, responsive-границы, закрепленную навигацию, размеры изображений и объем контента до прокрутки. Это не временная настройка, которую можно менять для удобства снимка.

Распространенная причина различий заключается в том, что библиотека автоматизации выбирает удобный viewport сама. Контекст может оказаться уже выбранного профиля, и страница перестроится еще до создания изображения. Другая причина, попытка задать высоту viewport равной высоте документа для получения длинного снимка. Такой прием меняет responsive-верстку и результат уже не представляет текущую сессию браузера.

Если размеры должен задавать профиль, не указывайте viewport в контексте. В Puppeteer используйте defaultViewport: null. В Playwright создавайте контекст без переопределения viewport. Если продукту нужен другой размер, зафиксируйте его в профиле и спецификации снимка, а затем используйте один и тот же выбор для сопоставимых запусков.

const browser = await chromium.launch({
  executablePath: process.env.BROWSER_BINARY,
  args: [`--bot-profile=${process.env.BROWSER_PROFILE}`],
});

const context = await browser.newContext();
const page = await context.newPage();

В примере намеренно нет настройки viewport. Именно это отсутствие важно. Не вызывайте resize, чтобы подогнать viewport под высоту документа. Длинный документ должен оставаться длинным внутри viewport профиля.

Browser workflow review

Определите готовность сигналами самой страницы

Завершение навигации не означает, что изображение готово. Начальная навигация может закончиться, пока приложение еще размещает карточки, загружает график, выбирает язык или заменяет резервные шрифты. Снимок между этими этапами является корректным файлом, но показывает состояние, которое читатель не должен видеть.

Для каждого маршрута задайте небольшой контракт готовности. Маркер, созданный самой страницей, обычно надежнее фиксированной паузы: страница может создать его только после появления нужного контента. Это может быть элемент со стабильным атрибутом, видимое состояние или компонент маршрута, который появляется после последнего изменения верстки. Контракт должен описывать видимое содержимое и не зависеть от закрытого события браузера.

Полезный контракт обычно включает такие условия:

  • Основной контейнер существует и виден.
  • Данные требуемого представления отрисованы.
  • Изменения верстки после прихода данных завершены.
  • Шрифты заголовков, подписей и текста готовы.
  • Изображения в нужном диапазоне загружены или перешли в намеренно выбранное резервное состояние.
  • Для снимка всей страницы есть ясный маркер окончания.

Маркер окончания особенно важен для страниц, которые собирают содержимое частями. Появление первой карточки ничего не говорит о нижней половине отчета. Ожидание маркера окончания дает стабильную границу. Если у маршрута нет естественного конца, задайте явную границу снимка, вместо того чтобы считать растущий документ завершенным.

Практический порядок ожидания

В этом варианте используются маркер маршрута, готовность шрифтов и состояние изображений. Произвольная пауза отсутствует. Замените селекторы на значения для конкретной страницы и сделайте маркеры частью ее контракта, а не универсальным селектором для всех маршрутов.

async function waitForCaptureReady(page) {
  await page.waitForLoadState('domcontentloaded');
  await page.locator('[data-capture-ready]').waitFor({ state: 'visible' });
  await page.locator('[data-capture-end]').waitFor({ state: 'visible' });

  await page.evaluate(async () => {
    await document.fonts.ready;
    await Promise.all(
      [...document.images].map(image =>
        image.complete
          ? undefined
          : new Promise(resolve => {
              image.addEventListener('load', resolve, { once: true });
              image.addEventListener('error', resolve, { once: true });
            })
      )
    );
  });
}

Ветка изображений считает необязательную миниатюру, которая не загрузилась, завершенным резервным состоянием. Один второстепенный ресурс не удерживает всю задачу. Если требуется каждое изображение, пусть маркер страницы появляется только после обязательных ресурсов, а отсутствие ресурса записывается явно.

Отсутствие сетевой активности может быть вспомогательным сигналом, но не единственным. Обновления в реальном времени, постоянные соединения и периодические запросы могут оставлять страницу занятой после готовности видимого контента. Наоборот, клиентская отрисовка может закончиться без нового запроса. Контракт страницы должен выражать состояние, которое должен увидеть читатель.

Не добавляйте фиксированную паузу после навигации. Быстрая страница просто будет ждать, а медленная все равно может быть захвачена слишком рано. Пауза также скрывает причину незавершенной страницы. Стабильный маркер остается понятной точкой проверки при изменении маршрута.

Снимайте всю страницу без растягивания viewport

Для изображения viewport используйте обычный режим скриншота. Для изображения документа используйте нативную опцию полной страницы в библиотеке автоматизации. Она сохраняет viewport профиля и включает содержимое ниже видимой области.

await waitForCaptureReady(page);

await page.screenshot({
  path: outputPath,
  fullPage: true,
  type: 'png',
});

Не задавайте высоту viewport равной высоте документа перед этим вызовом. Такой обходной прием меняет responsive-верстку, может нарушить загрузку содержимого, зависящую от обычной прокрутки, и создает изображение, которое больше не представляет выбранный профиль. Кроме того, поверхность рендеринга может оказаться больше возможностей графического пути или памяти контейнера.

Если на странице есть отложенное содержимое, перед нативным снимком подготовьте его обычной прокруткой. Прокрутка просит страницу загрузить нужный диапазон. Она не заменяет снимок всей страницы. Вернитесь к верхней позиции, снова подтвердите контракт готовности и только затем используйте нативную опцию.

Для бесконечной прокрутки нужна явная граница. Заранее решите, заканчивается ли изображение на текущем загруженном наборе, на именованном разделе или в состоянии завершения маршрута. Не позволяйте задаче прокручивать страницу бесконечно, пока она добавляет элементы. Отчет с фиксированной границей проще проверять, хранить и сравнивать, а его высота не зависит от момента снимка.

Закрепленные элементы и responsive-верстка

Закрепленные заголовки, панели согласия, кнопки чата и плавающие панели могут закрыть содержимое длинного изображения. Если они не относятся к отчету, маршрут снимка должен предоставлять представление для печати или архива. Небольшой стиль, предназначенный для этого представления, обычно понятнее, чем удаление элементов после создания изображения. Viewport профиля при этом остается прежним.

Используйте один viewport для сопоставимых запусков. Изменение ширины меняет переносы строк и положение карточек даже при одинаковых данных. Изменение device pixel ratio также меняет размеры результата и нужную память во время его создания. Записывайте эти значения в метаданных снимка, а не оставляйте их случайным настройкам скрипта.

Отдельно проверяйте нижнюю часть длинного изображения

Низ длинного изображения требует отдельной проверки. Верх может выглядеть хорошо, пока подвал, последняя строка, граница или фон обрезаны. Внизу также может остаться индикатор загрузки. Если открывать изображение только с верхнего левого угла, эти проблемы легко пропустить.

Добавьте странице стабильный маркер окончания и включите его в образец проверки. Это может быть заголовок подвала, последний раздел отчета или понятная читателю зона завершения. Маркер должен быть виден в финальном изображении, а не быть скрытой отметкой запуска браузера.

Проверяйте нижний край тремя способами:

  1. Откройте уменьшенный предварительный просмотр и убедитесь, что документ дошел до ожидаемого конца.
  2. Откройте последнюю часть в читаемом масштабе и проверьте последнюю строку, подвал, границы и заливку фона.
  3. Сравните размеры и объем файла с записью о снимке. Резкое изменение может указывать на сдвиг верстки, пропущенный раздел или недоступный ресурс.

Автоматические проверки должны относиться к контракту страницы и созданному файлу. Убедитесь, что маркер окончания был виден до захвата, файл существует и изображение декодируется. Оставьте небольшой набор длинных страниц для визуальной проверки. Сравнение пикселей полезно в контролируемом регрессионном наборе, но его следует дополнять читаемой проверкой: шрифты, рендеринг платформы и допустимые изменения контента могут менять отдельные пиксели.

Не используйте исходную высоту документа как единственное условие успеха. Высота может быть известна до отрисовки содержимого и продолжить расти после создания изображения. Маркер окончания и проверка результата отвечают на более полезный вопрос: попало ли требуемое содержимое в финальный снимок?

Предусмотрите общую память для высоких изображений в контейнерах

Длинный скриншот потребляет больше, чем размер готового PNG или JPEG на диске. Браузеру нужно рабочее пространство для сборки страницы, слою автоматизации нужно место для получения результата, а кодировщику требуется место во время записи. Профиль с высокой плотностью пикселей увеличивает поверхность в обоих направлениях. Слишком маленькая общая память контейнера может проявить проблему только на самой длинной странице, поэтому она выглядит случайной.

Заранее резервируйте общую память под реальную нагрузку. Учитывайте viewport профиля, device pixel ratio, самый длинный разрешенный документ, формат изображения и снимки, которые могут существовать одновременно в контейнере. Оставляйте запас для запуска браузера и обычной отрисовки. Явно задайте размер /dev/shm через runtime контейнера или эквивалентную настройку Compose, а не принимайте маленькое значение по умолчанию.

docker run --rm \
  --shm-size="${BROWSER_SHM_SIZE}" \
  -e BROWSER_PROFILE=/run/profiles/capture.enc \
  screenshot-worker

Значение должно находиться в конфигурации развертывания, потому что оно зависит от набора разрешенных страниц. Не переносите настройку короткого теста на работу с длинными документами. После изменения набора повторите снимок самой показательной длинной страницы и проверьте результат вместе с ресурсами контейнера.

Важны и другие ресурсы. Каталог вывода должен вмещать готовое изображение и временную запись, том должен иметь права пользователя worker, а хранилище профиля нужно отделить от финальных артефактов. Неудачная запись не должна заменять одобренный файл частичным. Сначала пишите во временное имя на том же томе, закройте файл, затем используйте обычную атомарную операцию файловой системы.

Если утвержденная конфигурация Linux использует Xvfb, его поверхность должна покрывать viewport профиля и использовать проверенную настройку цвета. Нативный headless-режим может не требовать Xvfb. Не добавляйте виртуальный дисплей только из-за высоты изображения. Выберите поддерживаемый путь, проверьте его на целевых страницах и сохраняйте одинаковый выбор в разработке и производстве.

Подберите формат под задачу

PNG является надежным выбором для текста, схем, интерфейсов и визуальных регрессий. Он сохраняет четкие границы и не добавляет артефакты сжатия возле мелких подписей. JPEG может подойти для фото-ориентированных предварительных просмотров, когда размер файла важнее точности границ. Запишите формат в спецификации, чтобы другой запуск не изменил критерий проверки без явного решения.

Результат с высокой плотностью пикселей удобен, если читатель будет увеличивать отчет или изображение станет архивной записью. Он также увеличивает потребление памяти, передачу и хранение. Device pixel ratio должен задаваться выбранным профилем. Не переопределяйте его в контексте только ради большей резкости. Если нужен маленький предварительный просмотр, создайте производный файл после сохранения утвержденного оригинала.

Используйте стабильные имена, в которых указаны маршрут, семейство профиля, режим, формат и дата, но нет частных данных страницы. При создании производного файла сохраняйте исходный артефакт. Так последующая проверка сможет отличить изменение рендеринга от изменения миниатюры.

Небольшой производственный шаблон

Функция снимка должна явно показывать порядок действий: открыть страницу с профилем, перейти по адресу, дождаться контракта страницы, при необходимости использовать нативный режим полной страницы и закрыть страницу после ошибки.

async function capture(page, url, outputPath, fullPage = false) {
  await page.goto(url, { waitUntil: 'domcontentloaded' });
  await waitForCaptureReady(page);
  await page.screenshot({
    path: outputPath,
    fullPage,
    type: 'png',
  });
}

Если несколько маршрутов используют одну утвержденную политику сессии, создавайте браузер вне этой функции. Явно владейте каждой страницей, закрывайте ее после завершения артефакта и передавайте ошибку вместе с маршрутом и режимом снимка. Неудачный снимок должен быть виден исполнителю, а не молча заменяться старым файлом.

В пакетной работе каждая страница должна проходить одинаковые проверки готовности и результата. Не добавляйте быстрый путь, который пропускает маркер окончания для последующих страниц. Первая страница часто выглядит здоровой, а медленная выявляет нехватку памяти, поздние шрифты или растущий документ. Единый контракт облегчает объяснение таких различий.

Симптомы и порядок проверки

Изображение пустое или почти пустое. Убедитесь, что переход достиг нужного маршрута, появился маркер готовности и доступен выбранный графический путь. Посмотрите вывод браузера, чтобы понять, остановилась ли страница до маркера, вместо того чтобы сразу добавлять паузу.

Изображение заканчивается до последнего раздела. Проверьте маркер окончания, подготовьте отложенное содержимое обычной прокруткой и убедитесь, что бесконечная прокрутка имеет границу. Затем проверьте нижнюю часть файла.

Положение текста меняется между снимками. Убедитесь, что использовались один viewport профиля, device pixel ratio, набор шрифтов, цветовая схема и данные страницы. Дождитесь готовности шрифтов и проверьте сдвиги верстки до захвата.

Контейнер падает только на длинных страницах. Проверьте вместе общую память, временное пространство, том вывода и поверхность дисплея. Короткая страница не доказывает, что самое длинное изображение поместится. Измените ресурс, повторите показательный снимок и сохраните настройку рядом с записью нагрузки.

Плавающий элемент закрывает отчет. Используйте архивное представление маршрута или снимайте содержательный элемент. Не исправляйте пиксели после создания и убедитесь, что представление сохраняет viewport профиля.

В изображении осталась заглушка загрузки. Пусть маркер готовности ждет нужный ресурс или явно объявите резервное состояние намеренным. Общая сетевая активность не показывает, приемлема ли заглушка для читателя.

Проверьте образец до создания эталона

Сохраните небольшой набор маршрутов для проверки развертывания. Включите короткую страницу, длинный отчет, страницу с веб-шрифтами, страницу с отложенными медиа и responsive-макет с выбранным профилем. Смысл не в сборе всех страниц, а в покрытии состояний, которые меняют границу изображения или потребление ресурсов.

Для каждого маршрута запишите профиль, источник viewport, режим, формат, графический путь, политику памяти контейнера, маркер готовности и маркер окончания. Проверьте первый экран и нижнюю часть. Убедитесь, что артефакт декодируется, имеет ожидаемую ориентацию и записан в нужное место. Если изменение намеренное, обновите и запись о снимке.

Надёжный скриншот является процессом рендеринга, а не последним нажатием кнопки. Профиль дает стабильный viewport, страница сообщает осмысленную готовность, нативная опция полной страницы достигает нижнего содержимого без изменения viewport, а проверка уделяет внимание части изображения, которая чаще всего теряется. Общая память и хранилище завершают контракт для длинных документов и профилей с высокой плотностью пикселей. Такой процесс сохраняет пользу снимков для визуальной проверки, архива и разрешенных проверок качества в разных средах.

#Screenshot#headless#развертывание#Reliability#Playwright

Переведите BotBrowser из исследований в продакшн

Используйте эти руководства, чтобы понять модель, а затем перейти к кроссплатформенной валидации, изолированным контекстам и масштабируемому браузерному развертыванию.