窗口尺寸与页面视口
本文说明浏览器窗口开多大、页面按多大排版、截出来的图是什么尺寸。「窗口尺寸」和「页面视口」在早期实现里是同一个值,现在分开管理 —— 混在一起会得到一个页面比窗口还大的怪状态。
一、两件事必须分开看
| 由什么决定 | 默认行为 | |
|---|---|---|
| 窗口尺寸 | 屏幕的可用工作区 | 宽取工作区的 28/32、高取满 |
| 页面视口 | 配置项 browser.viewport | 跟随真实窗口(有头时) |
把它们当成一件事时会出现这种结果:页面按钉死的尺寸排版,窗口按自己的尺寸显示,于是「页面上的视口高度」和「窗口里能看见的高度」对不上。
二、窗口尺寸按「可用工作区」算,不是整块屏幕
窗口外框尺寸 = 可用工作区宽度的 28/32、高度取满。
关键是「可用工作区」而不是「整块屏幕」:任务栏不算进可用高度。用整块屏幕的高度开窗口,窗口底边会被任务栏压住,而压住的那几十像素正好是页面底部。
实测(逻辑分辨率 1707×1067、任务栏占 48 像素的机器):
| 量 | 值 |
|---|---|
| 屏幕分辨率 | 1707 × 1067 |
| 可用工作区 | 1707 × 1019 |
| 开出来的窗口外框 | 1484 × 1019 |
宽度取 28/32 是留出右侧空间,方便人工在同一块屏幕上看到浏览器的实时操作。Chromium 与 Firefox 共用同一套算法,免得两个引擎的窗口大小不一致、排查时看到的页面布局也不一样。
量不到屏幕的机器(容器里没有显示设备)会退回一个保守的默认尺寸,而不是让
start失败在这一步。
三、browser.viewport:页面视口三档取值
| 取值 | 行为 | 什么时候用 |
|---|---|---|
window(默认) | 视口交给真实窗口:页面按窗口的实际内容区排版、拖动窗口页面跟着回流,截图尺寸等于窗口里真实可见的页面区 | 默认。所见即所得 |
fixed | 一直用按屏幕算出来的固定尺寸 | 需要固定尺寸截图做前后对比,或者站点只认固定视口 |
宽x高(如 1440x900) | 钉死这个尺寸 | 想让所有截图尺寸完全一致 |
分隔符 x / * / × 都认,大小写不限,前后空格会忽略。写了不认识的值会直接失败并列出可选值。
无头模式没有真实窗口可跟随,所以配 window 时会自动退回固定视口,并把原因写进 start 回执的 data.browser.viewport.note —— 不会静默失效。
start 回执里的 data.browser.viewport 说明这次用的是哪种:
{"data":{"browser":{"viewport":{"mode":"window","size":"window","note":null}}}}
fixed 或显式尺寸时是 {"mode":"fixed","size":"fixed 1484x1019","note":null}。
只影响托管 profile 那条路(Playwright 的持久化上下文)。CDP 那条路(用户自己的 Chrome profile、Edge)本来就不套视口模拟,页面尺寸一直跟着窗口走(见 05)。
四、为什么默认从 fixed 改成 window
视口钉死时,页面按钉死的尺寸排版,而窗口按自己的尺寸显示。实测(旧默认,屏幕 1707×1067):
| 量 | 实测值 |
|---|---|
页面自称视口(window.innerHeight) | 1067 |
| 窗口外框 | 1019 |
| 真实可见的页面区 | 约 931(1019 减标题栏、标签栏、地址栏约 88 像素) |
screenshot 的像素 | 1484 × 1067 |
innerHeight > outerHeight 对真实浏览器窗口是物理上不可能的 —— 这就是「视口被模拟、跟窗口脱钩」的铁证。后果有两条:
- 每页底部约 13% 渲染在窗口外面,人看不见;
- 而截图把这部分也拍了进去,于是喂给视觉模型的画面和用户眼睛看到的不是一回事;
get_browser_state的viewport_height以及pixels_above/pixels_below也按这个数算,元素「在不在视口里」的判断同样受影响。
改成 window 之后同一台机器上的实测:
| 量 | window(默认) | fixed |
|---|---|---|
| 页面视口(CSS 像素) | 1470 × 925 | 1484 × 1019 |
| 窗口外框 | 1484 × 1020 | 1484 × 1019 |
innerHeight 与 outerHeight 的关系 | 正常(小于) | 视口被模拟,两者不再对应 |
devicePixelRatio | 真实系统 DPI 比(这台机器 1.5) | 固定 1.0 |
| 截图与 DOM 坐标 | 需要除以 devicePixelRatio 换算 | 1:1 |
五、截图的像素尺寸跟着视口策略走
Playwright 默认按设备像素出图(scale: device),所以出图尺寸 = CSS 视口 × devicePixelRatio:
browser.viewport | devicePixelRatio | 出图(同一台机器实测) |
|---|---|---|
window | 真实系统 DPI 比(1.5) | 1470×925 的视口 → 2205×1388 |
fixed / 宽x高 | 固定 1.0 | 就是 CSS 视口那么大 |
window 下出的是屏幕上真实的物理像素 —— 也就是用户实际看到的那一屏,和「所见即所得」是自洽的,只是文件大一些。要把图上的像素位置换算成页面坐标(结构化文本里的坐标、clip 参数、set_viewport 用的都是 CSS 像素),就除以 devicePixelRatio。
想让它回到 CSS 像素是做不到的,不是没试:Playwright 的截图实现里那一步是
clip.scale /= this._browserContext._options.deviceScaleFactor,而那个选项恰好被viewport为空禁止设置(见下一节),于是它除以 1、退化成空操作。所以「视口跟随窗口」与「CSS 像素截图」只能二选一 —— 要后者就把browser.viewport配成fixed。
六、实现要点:为什么必须显式传空视口
跟随窗口时用的是 Playwright 协议里的 noDefaultViewport(Java API 上是传一个空的 ViewportSize)。
不能靠「什么都不设」来实现跟随窗口:不设的话 Playwright 会套上它自己的默认视口(宽 1280、高 720),于是又变回「页面尺寸和窗口对不上」,只是数字换了一个。
跟随窗口时有两类参数不能一起给:
deviceScaleFactor、isMobile、hasTouch:Playwright 校验上下文参数时会直接拒绝(报"deviceScaleFactor" option is not supported with null "viewport"),启动失败。它们只在视口钉死时才设。跟随窗口时设备像素比由真实窗口与系统决定,反而更接近用户所见。- 窗口尺寸:视口为空时 Playwright 不会替我们定窗口大小,所以要自己给一个窗口尺寸参数,否则窗口会用 profile 里上次留下的尺寸或浏览器自己的默认值,那就不可预期了。
Firefox 那条路不额外给窗口尺寸参数:Playwright 给 Firefox 传的是 -width / -height,自己再补一组会多出一份互相冲突的参数,而这属于引擎差异、不便于在本机实测 —— 结果是 Firefox 的窗口用它自己的默认尺寸,视口跟随这个窗口。
七、运行时改视口:set_viewport
{"id":"1001","method":"set_viewport","params":{"width":1440,"height":900}}
它是钉死一个尺寸:钉死之后没有 API 再交还给窗口(Page.setViewportSize 只有整数重载),要回到跟随窗口请重新 start 一个任务。常见用途是在截图前临时放大视口,一次拿到更多内容。
八、元素在视口外时拿不到索引
快照默认只给视口内的元素索引。pixels_above / pixels_below 非 0 就说明页面还有内容在视口外,而 viewport_height 就是上面那个视口高度。要一次拿到首屏之外的元素,按优先级:
get_browser_state传viewportExpansion(例如1500),把快照范围向外扩;- 改用
click_element_by_selector/input_text_by_selector(不依赖索引); - 滚动到目标位置后重新取快照。
不可见元素不适用这三条:隐藏元素既不在快照里,选择器类接口也会因为可操作性检查失败而拿不到它(等满动作超时后返回「没匹配到可操作的元素」),只能用 execute_js 直接设值并派发事件。
