部署

Headless 浏览器可靠截图工作流

从 profile viewport、页面就绪信号、原生整页截图到长图底部复核,建立稳定的 headless 浏览器截图流程,并为容器预留共享内存。

文档中心

想直接看维护中的产品文档?

这篇文章对应的主题已经有文档中心页面。需要规范流程、当前参数和长期参考时,优先看 docs。

先固定 profile viewport

截图只有在呈现了浏览器实际要呈现的页面时才有意义。对于使用 profile 的 BotBrowser 会话,viewport 是渲染契约的起点。它会影响换行、响应式断点、 固定导航、图片尺寸,以及首屏能看到的内容范围。它不是截图脚本可以随手 替换的临时参数。

常见问题是让自动化库选择默认 viewport。默认 context 可能比选定 profile 更 窄,页面会在截图前就发生重新排版。另一个问题是为了做整页图而把 viewport 改成文档高度。这样会改变响应式布局,也会让输出不再代表这次浏览器会话。

如果 profile 应该提供尺寸,就不要在 context 中设置 viewport。Puppeteer 使用 defaultViewport: null,Playwright 创建 context 时省略 viewport 覆盖。如果 业务确实需要不同尺寸,应把它写入截图规格并与 profile 一起固定,在同类任务 中始终使用同一选择。

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 设置。这个省略本身很重要。不要为了匹配文档高度而 调用 viewport resize。长文档应当在 profile 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 });
            })
      )
    );
  });
}

图片分支把失败的可选缩略图当作已经进入备用状态,避免一张无关图片让整 个任务一直等待。如果页面要求每一张图片都必须存在,就让页面标记只在所需 资源成功后出现,并对缺失资源记录清楚的失败。

网络空闲可以作为辅助信号,但不应是唯一信号。统计脚本、实时更新、长连接 和轮询会让页面在可见内容完成后仍然保持忙碌。相反,客户端渲染也可能在没有 新的网络请求时完成。页面契约应表达读者真正需要看到的状态。

不要在导航后加入固定 sleep。快速页面会白白等待,慢页面仍可能过早截图, 同时隐藏页面没有就绪的真实原因。稳定标记能在路由变化时提供可复核的依据。

不拉伸 viewport,使用原生整页截图

需要首屏图时,使用普通截图选项捕获当前 viewport。需要文档图时,使用自动 化库提供的原生整页选项。这个选项能保留 profile viewport,同时包含折叠线 以下的内容,是整页请求应使用的边界。

await waitForCaptureReady(page);

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

不要在调用前把 viewport 高度设成文档高度。这个捷径会改变响应式布局,可能 破坏依赖正常滚动的懒加载,也会让图片不再代表选定的 profile。它还可能创建 一个超出显示后端或容器内存能力的巨大渲染面。

如果页面有懒加载内容,可以先用正常滚动让目标范围进入加载流程,然后再调用 原生整页选项。滚动只是请求页面加载指定范围,不是整页截图的替代品。回到 需要的顶部位置,再次确认就绪契约,然后让原生截图选项收集整份文档。

无限滚动必须有明确边界。应事先决定截图在当前已加载内容、指定章节或路由 完成状态处结束。不要让任务在页面持续追加项目时无限滚动。边界固定的报告 更容易复核、存储和比较,图片高度也不会被时机左右。

固定元素和响应式布局

吸顶标题、同意面板、聊天按钮和浮动工具栏可能覆盖整页截图中的内容。如果 这些元素不属于报告,截图路由应提供打印或归档呈现方式。与其在图片生成后 删除元素,不如使用小型的截图专用样式。这个呈现选择应留在页面或路由配置 中,同时保持 profile viewport 不变。

同类任务使用同一个 viewport。宽度变化会改变换行和卡片位置,即使页面数据 完全相同。device pixel ratio 变化还会改变输出尺寸和图片组装期间所需的内存。 把这些值作为截图元数据记录,而不是让脚本临时决定。

单独检查长图下部

长图下半部分需要独立复核。图片顶部可能看起来正确,但页脚、最后一行、边 框或背景色可能被截断,底部也可能仍显示加载占位。只从左上角打开图片,很 容易漏掉这些问题。

给页面添加稳定的结束标记,并把它放进复核用的页面样本。标记可以是页脚标题、 报告最后一个章节,或读者能理解的完成区域。它应当出现在最终图片中,而不是 一个隐藏的浏览器实现标志。

长图可以按下面的顺序检查:

  1. 打开缩小预览,确认文档确实到达预期结尾。
  2. 以可读比例打开最后一段,检查最后一行、页脚、边框和背景填充。
  3. 将图片尺寸和文件大小与截图记录对照。突然变化可能表示布局移动、章节 缺失或资源加载失败。

自动质量检查应关注页面契约和生成的文件。确认结束标记在截图前可见,输出文 件存在,并且图片可以正常解码。保留少量有代表性的长页面样本用于视觉复核。 在受控回归套件中,逐像素比较可以提供帮助,但仍应结合人类可读的检查,因 为字体、平台渲染和合理的内容变化都可能影响单个像素。

不要只把文档高度当成成功条件。高度可能在内容绘制前就已经存在,也可能在图 片生成后继续增长。结束标记和产物复核回答了更有用的问题:预期内容是否真正 进入了最终图片。

为容器中的高图预留共享内存

长截图消耗的资源不只是磁盘上的 PNG 或 JPEG 大小。浏览器在组装页面时需要 工作空间,自动化层接收结果时也需要空间,编码器写文件时还需要临时空间。 高 DPI profile 会同时放大像素面。共享内存挂载过小的容器,往往只在最长页 面上失败,因此看起来像偶发的截图问题。

共享内存要按实际工作负载预留。计算 profile viewport、device pixel ratio、 批准范围内的最长文档、图片格式,以及同一容器可能同时存在的截图数量,并留 出浏览器启动和普通页面渲染的余量。通过容器运行时显式设置 /dev/shm,或 使用 Compose 的等效设置,不要依赖运行时很小的默认值。

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

共享内存值应放在部署配置中,因为它取决于批准的页面集合。不要把短页面测试 使用的数值直接复制给长文档工作负载。页面集合改变后,应重新捕获最有代表性 的长页面,并同时查看输出和容器资源状态。

容器的其他资源也要一起规划。输出目录要能容纳完整图片和写入期间的临时文 件,挂载目录的所有权要与 worker 一致,浏览器 profile 存储要和最终产物分开。 失败写入不能用半张图片替换上一次已经批准的图片。可以先在同一个输出卷内 写入临时名称,关闭文件后再使用文件系统支持的原子操作完成替换。

如果批准的 Linux 配置使用 Xvfb,其显示面应覆盖 profile viewport,并使用已经 验证过的颜色配置。原生 headless 模式可能不需要 Xvfb。不要因为图片很高就 自动加入虚拟显示。选择一个支持的显示路径,在目标页面上验证,然后让开发和 生产保持一致。

让输出格式服务于用途

PNG 更适合文字、图表、界面和视觉回归,因为小标签和细边缘不会出现压缩伪影。 JPEG 更适合以照片为主、文件大小比边缘精度更重要的预览。把格式写入截图规格, 避免后续任务悄悄改变复核标准。

高 DPI 输出适合需要放大报告或保存归档记录的场景,但会增加内存、传输和存 储消耗。device pixel ratio 应由选定 profile 提供,不要为了让图片看起来更 锐利而在 context 中覆盖它。若消费者只需要小预览,应在批准的原图之后生成 派生图,而不是改变浏览器的渲染契约。

文件名应稳定记录路由、profile family、截图模式、格式和创建日期,同时不要 把页面私密数据放进文件名。生成派生图时保留原始产物,后续复核才能区分渲染 变化和缩略图处理变化。

一段清晰的生产流程

截图函数应让执行顺序一眼可见:创建使用 profile 的页面,导航,等待页面契约, 需要时调用原生整页选项,并在失败时关闭页面。

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',
  });
}

如果多个路由使用同一份批准的会话策略,可以把浏览器创建放在函数外。页面 所有权要清楚,产物完成后关闭页面,错误要连同路由和截图模式一起交给任务 运行器。失败的截图不能静默地沿用旧文件。

批量任务中的每个页面都应走同一套就绪和产物检查。不要为后续页面增加跳过 结束标记的快速路径。第一张图片通常看起来健康,慢页面才会暴露共享内存、 晚到字体或不断增长的文档问题。统一契约能让这些差异更容易解释。

常见现象和处理顺序

图片空白或大部分空白。 先确认导航进入预期路由,页面就绪标记出现, 并确认选定的显示路径可用。查看浏览器输出,找出页面在标记出现前是否已经 停止,而不是先加更长的 sleep。

图片在最后章节前结束。 确认结束标记出现,懒加载范围已经通过正常滚动 准备好,并确认增量页面有明确边界。随后检查产物下部。

多次截图中的文字位置变化。 确认使用了同一个 profile viewport、device pixel ratio、字体、配色和页面数据。等待字体完成,并在截图前检查布局移动。

只有长页面让容器失败。 同时检查共享内存挂载、临时空间、输出卷和显示 面。短页面不能证明最长图片一定放得下。调整资源后重新捕获代表性长页面,并 把部署设置与工作负载记录放在一起。

浮动控件覆盖报告。 使用路由的归档呈现,或者截取有意义的内容元素,不 要在图片生成后修改像素。确认该呈现仍在 profile viewport 中渲染。

图片中有加载占位。 让页面就绪标记等待所需资源,或明确声明备用状态是 有意的。通用网络状态无法判断占位是否可以被读者接受。

建立发布前复核样本

保留一小组有代表性的路由用于发布复核,包括短页面、长报告、使用网页字体的 页面、带懒加载媒体的页面,以及使用选定 profile 的响应式页面。重点不是收 集所有页面,而是覆盖会改变图片边界或资源需求的状态。

每条路由记录 profile 引用、viewport 来源、截图模式、格式、显示路径、容器内 存策略、就绪标记和结束标记。检查首屏和下部,确认产物可解码、方向正确,并 写入预定位置。合理变化发生时,也要像维护页面一样更新截图记录。

可靠截图是渲染工作流,而不是最后一次按钮点击。profile 提供稳定 viewport, 页面提供有意义的就绪状态,原生整页选项在不改变 viewport 的前提下抵达折叠 线以下,复核则专门检查最容易丢失的长图下部。容器共享内存和输出空间补齐 长页面与高 DPI 文档的资源契约,让截图可以在不同环境中用于视觉复核、归档 和经过授权的质量检查。

#Screenshot#无头模式#部署#Reliability#Playwright

让 BotBrowser 从研究走向生产

先用这些指南理解模型,再进入跨平台验证、隔离上下文和面向规模化的浏览器部署。