- date
- entry
- 027
- topic
- tooling
- rev
- —
headless Chrome 截 375px 寬的圖,版面其實排在 500px
--window-size=375,300 截出來的 PNG 確實是 375×300。但頁面讀到的 innerWidth 是 500,右邊的字被裁掉了。
我用 headless Chrome 產 OG 封面,指令裡關鍵的就是 --window-size=2400,1260 --screenshot。同樣的方法拿來截手機版面,把寬度改成 375,出來的 PNG 也確實是 375 像素寬。
但那張圖裡的版面是用 500px 寬排的,右邊被裁掉一截。這篇要談的是 --window-size 在寬度低於 500 時發生的事,以及高度上另一個 87px 的落差。依序回答三件事:實測數字、為什麼截圖尺寸對了內容卻是錯的、要截窄版面該改用什麼。
實測:低於 500 的寬度都變成 500
測試頁在載入過程中用一段行內腳本,把 innerWidth、innerHeight 等數值寫進頁面。每個寬度各用一個新的 profile 目錄、啟動一次 headless Chrome(Google Chrome 154.0.8037.98,macOS):
"$CHROME" --headless=new --disable-gpu --user-data-dir="$PWD/p$W" \
--window-size=$W,800 --virtual-time-budget=2000 --dump-dom "file://$PWD/w.html"
--window-size 寬 | innerWidth |
|---|---|
| 320 | 500 |
| 375 | 500 |
| 499 | 500 |
| 500 | 500 |
| 501 | 501 |
| 640 | 640 |
在這個版本與設定下,500 以上照設定走,500 以下一律變 500。我也試了 --headless=old,同樣是 500;不過 Chrome 的 headless 說明寫明,從 132.0.6793.0 起舊版 headless 只以獨立的 chrome-headless-shell 提供,所以這次大概根本沒有跑到舊實作。
截圖尺寸是對的,內容是錯的
麻煩在於截圖本身不會露餡。換成 --screenshot,--window-size=375,300 產出的 PNG 是 375×300,尺寸完全符合。測試頁頂端是一條 flex 橫條,左邊放 L,右邊放 RIGHT-EDGE:
L RIG
innerWidth=500
innerHeight=213
右邊的字只剩 RIG。頁面是照 500px 排版,截圖再從左邊裁出 375px。對 RWD 來說,這代表寫給 max-width: 480px 的 media query 一條都沒有生效,截到的是一個較寬版面左側的 375px,看起來仍然像一張正常的窄截圖。
為什麼:500 是寫死的下限,截圖前視窗又被調整過一次
500 這個數字在 Chromium 原始碼裡(154.0.8037.98 的 chrome/browser/ui/views/frame/layout/browser_view_layout.h):
// The minimum width for the normal (tabbed or web app) browser window's
// contents area. ...
static constexpr int kMainBrowserContentsMinimumWidth = 500;
註解說這是一般瀏覽器視窗內容區的最小寬度(popup 不適用)。new headless 跑的是完整瀏覽器,所以也套用這個下限。
截圖那一段在同版本的 components/headless/command_handler/headless_command.js:--screenshot 會把擷取範圍 clip 成你要的寬高,並在擷取前呼叫 Browser.setContentsSize,把內容區調成你要的尺寸。寬度 375 調不下去,停在 500,clip 卻照 375 裁,於是右邊被切掉。
高度則是相反的情況。視窗高度 800 時頁面載入中讀到的 innerHeight 是 713;300 時是 213;630 時是 543,三組都差 87。我用 --window-size=1200,630 截一頁:一條 position: fixed; bottom: 0 的紅色橫條,加一個 height: 100vh 的綠色直條。PNG 是 1200×630,紅條貼在第 630 像素的底邊,綠條從頂端一路延伸到紅條;紅條上的文字卻是載入時寫的 innerHeight=543。也就是說,載入時內容區比視窗矮 87px,擷取前 setContentsSize 才把它調成 630。
所以載入時讀一次 innerHeight 就不再更新的 JS,拿到的是 543,不是截圖時的 630;之後會不會跟著 resize 重算,取決於頁面怎麼寫。87 是怎麼組成的(我推論是瀏覽器介面佔掉的高度),我沒有追到原始碼,只確定在這台機器、這個版本上三組都是 87。
要截窄版面:用 DevTools Protocol 直接設定 viewport
--window-size 設的是初始視窗大小,碰到上面那個下限就無能為力。直接設定 viewport(頁面排版用的可視區域)的方法是 Chrome DevTools Protocol 的 Emulation.setDeviceMetricsOverride,它不受視窗最小寬度限制。不裝 Puppeteer 也能做,這台機器上的 Node.js 22.19 內建 fetch 與 WebSocket,整支腳本約 30 行,核心是這幾個呼叫:
// chrome 以 --headless=new --remote-debugging-port=9333 啟動後,連上頁面的 WebSocket
await send("Emulation.setDeviceMetricsOverride",
{ width: 375, height: 300, deviceScaleFactor: 1, mobile: true });
await send("Page.navigate", { url });
await sleep(1500); // 我的腳本只是固定等 1.5 秒,正式用要等 load 事件
await send("Runtime.evaluate", { expression: "innerWidth + 'x' + innerHeight" });
const { data } = await send("Page.captureScreenshot", { format: "png" });
同一頁的結果:
viewport 375x300
L RIGHT-EDGE
innerWidth=375
innerHeight=300
寬度是 375,高度也沒有少 87,RIGHT-EDGE 完整出現在右邊。要注意兩個前提:override 要在載入頁面之前設;測試頁有 <meta name="viewport" content="width=device-width,initial-scale=1">,而 mobile: true 會讓 Chrome 照手機的方式處理這個 meta。沒有這個 meta 的頁面,結果可能不同。
我的分法:OG 封面這種寬度遠大於 500、只靠 CSS 排版的圖,繼續用 --window-size,一行指令最省事。要截手機版面、或頁面會用 JS 讀視窗尺寸的,改走 setDeviceMetricsOverride,並在擷取前一刻用 Runtime.evaluate 重新讀一次 innerWidth、innerHeight 核對。
如果只記得一件事
驗截圖要驗頁面看到的尺寸,不是 PNG 的尺寸。開頭那張 375 像素寬的圖,PNG 的尺寸完全正確,錯的是裡面的版面。把 innerWidth 在擷取前一刻讀出來,和 PNG 的寬度擺在一起比,是我目前知道最便宜的檢查。
修訂紀錄
- 首次發布