不少人电脑里装着两三款截图工具,真到要交一份完整页面长图的时候,还是拿不出手:中间的商品图是白的,底部被切掉一截,导出的图放大后文字糊成一团。问题往往不在"工具不够多",而在于没搞清楚浏览器自带的截图能力边界在哪。这篇聊的就是 Chrome(也就是大家常说的谷歌浏览器)的截图——不装扩展、不借助第三方工具,靠浏览器原生能力和一点参数控制,把整页截图、元素截图、高清截图、批量截图这几件事做到能直接交付的水准。内容偏实操,会讲到命令入口在哪、DPR 怎么定、超长页为什么会截断、懒加载怎么预热,也会给一份能直接跑起来的批量脚本。刚入门的前端、需要出验收图的测试、经常整理网页资料的产品和运营,都能从里面找到能马上用的部分;老手可以重点看第三、四节的参数取舍和坑位说明。
1. 截图这件事,Chrome 手上有几套家伙
1.1 三条路径各自的能力天花板
把"在浏览器里截图"拆开看,实际有三条完全不同的路径,很多人混着用,结果在错误的地方使了劲。第一条是操作系统级快捷键,Windows 上是 Win+Shift+S,macOS 上是 Cmd+Shift+4 那套。它截的是"眼睛能看到的东西",好处是快、任何界面都能用,坏处是它根本不知道网页有多长,页面滚动到视口外的部分完全无能为力,而且截出来的尺寸受屏幕分辨率限制死。
第二条是浏览器扩展。这类工具确实成熟,整页截、区域截、加箭头、打马赛克、加水印一条龙。但它们的工作方式决定了两个隐性成本:一是权限,绝大多数截图扩展申请的是"读取和更改您在所有网站上的数据",这个权限范围比截图本身需要的大得多;二是环境限制,很多受管设备、公司配发的电脑是禁止安装扩展的,或者扩展商店本身访问不畅,这时候装不了就是装不了。
第三条是开发者工具(DevTools)。它自带截图命令,不需要任何扩展,能截当前视口、能截完整页面、能截单个 DOM 元素,还能通过设备模拟控制输出像素密度。缺点也明显:入口藏得深,藏在命令面板里,不知道的人翻遍设置菜单也找不到;而且它执行的是"渲染截图"而不是"屏幕抓取",页面状态不对的时候截出来就是废图。三条路径的对照我整理成了下面这张表,选哪条基本看场景。
| 路径 | 能截整页 | 能控清晰度 | 能截单个元素 | 需要装东西 | 典型适用场景 |
|---|---|---|---|---|---|
| 系统快捷键 | 否 | 否 | 否 | 否 | 快速抓一段报错、发给同事看 |
| 浏览器扩展 | 是 | 部分支持 | 部分支持 | 是 | 需要标注、打码、加箭头的交付图 |
| DevTools 命令 | 是 | 是 | 是 | 否 | 出验收图、存档、批量抓取 |
1.2 我把截图扩展卸掉的实际原因
说个可能不太讨喜的结论:大部分截图扩展在"整页截图"这件事上的效果,并不比 DevTools 的原生命令更好,因为它们底层做的事情高度重合——要么调浏览器的截图接口,要么把页面画到 canvas 上再导出。前者和 DevTools 走的是同一条路,后者在遇到跨域图片、特殊字体、复杂 CSS 的时候反而更容易出问题。
我卸掉扩展的直接触发点是权限。一个只用来截图的工具,拿到"读取所有网站数据"的权限,意味着它能读到登录态、表单内容、页面上的任何文字。这不是说哪个具体工具有问题,而是这个权限模型本身就不划算。加上后来发现 DevTools 的命令可以绑快捷键,按一下就能出整页图,扩展那点便利性优势就没了。
真正让我保留扩展的场景只剩一个:需要往图上画箭头、框重点、打马赛克的时候。这类标注编辑是 DevTools 完全不具备的能力,出教程图、写问题反馈、做验收说明的时候省事很多。所以我的建议是分场景——纯截图用原生,要标注再上工具,别为了一个功能装一个全家桶。
1.3 一个容易被忽略的前提:DevTools 停靠会改变视口
这条放在第一节最后说,因为它影响后面所有操作的正确性。DevTools 默认是"停靠"在页面右侧或下方的,这时候页面的可用宽度会被压缩。你看到的是 1440 宽的页面,实际渲染宽度可能只有 900。在这个状态下截出来的图,布局是窄屏布局,和用户真实看到的完全不是一回事。
正确做法是把 DevTools 变成独立窗口:点开发者工具右上角的三个点(或齿轮),找到"Dock side"(停靠位置),选"Undock into separate window"(取消停靠到独立窗口)。这样浏览器视口恢复到全宽,DevTools 浮在另一个窗口里,截图尺寸才和真实访问一致。这个小细节我在带新人的时候几乎每次都要讲一遍,因为踩过的人不少,而且症状很隐蔽——图截出来了,就是排版跟预期不一样,还以为是网站的问题。
2. 命令面板:整页截图的开关藏在这里
2.1 三个入口,选最顺手的那一个
打开 DevTools 的方式有好几种,F12、Ctrl+Shift+I(macOS 是 Cmd+Option+I)、右键"检查",效果一样。打开之后重点是下一步:调出命令面板。快捷键是 Ctrl+Shift+P,macOS 上是 Cmd+Shift+P,这个组合和编辑器里调命令面板的习惯是一致的。
命令面板是个输入框,敲 "screenshot" 就会过滤出四个命令。注意是英文关键词,即使你的界面是中文的,命令本身也建议用英文搜,中文界面下对应的名称是"捕获截图""捕获完整尺寸截图""捕获区域截图""捕获节点截图",但搜英文命中率更高,也不容易因为版本差异找不到。
提示:如果你经常用整页截图,强烈建议给这个命令绑一个快捷键。在 DevTools 设置(按 F1 或点齿轮)里找到 Shortcuts 页,搜 "screenshot",可以直接录制组合键。绑完之后就是按两下键盘出图,比开扩展还快。
2.2 四个命令到底各管什么
命令面板里的四个截图命令,名字很像,功能差别不小,混用会浪费很多时间。我按使用频率从高到低说。
Capture full size screenshot(捕获完整尺寸截图)是最常用的。它会按文档的完整高度重排视口,把从页面顶部到最底部的全部内容渲染成一张图,包括当前没滚动到的部分。这是"整页长截图"的正解,不需要手动拼接。
Capture screenshot(捕获截图)截的是当前视口可见区域,就相当于把屏幕上看到的那一块抓下来,但走的是渲染通道,不受系统缩放、多显示器 DPI 的影响,比系统快捷键更"干净"。
Capture node screenshot(捕获节点截图)是这四个里被低估的一个。它只截当前在 Elements 面板里选中的那个 DOM 节点,及其子内容。截一张卡片、一个表格、一个图表、一段代码块,用它最省事,不用先算坐标再框选。
Capture area screenshot(捕获区域截图)允许你用鼠标在页面上拖一个矩形出来,只截这个范围。它的定位是"我只要这一块,但这一块不在某个独立节点里"的情况。缺点是不能精确到像素,靠手感拖。
2.3 元素截图的三个前提条件
节点截图好用,但有三个前提,缺一个就出问题。第一,必须先在 Elements 面板里选中那个节点,选中状态下页面会有一层高亮遮罩,这个遮罩不会进入截图,不用管。第二,元素如果在视口外,命令照样能截到,因为它是按渲染树走的,不依赖滚动位置,这一点比很多人想的要强。
第三,也是最容易翻车的:元素如果设了overflow: hidden,超出部分会被裁掉,截出来就是残缺的。遇到这种情况,临时把 overflow 改成 visible 再截,截完改回去。还有一种情况是元素宽度是自适应的,截图时它会按当前容器宽度渲染,如果你想要一个更宽的版本,得先调整视口宽度或者容器宽度。
另外提醒一句,截图前记得在页面空白处点一下,取消文本选中状态。选中的文字会带一层蓝色高亮,这个高亮是会进图的,很多人截完才发现标题被涂了一半蓝,又得重来。
2.4 命令面板其实不止能截图
顺手说个延伸用法。命令面板里除了截图,还能做很多平时要点好几层菜单的事,比如 "Show Coverage"(看代码覆盖率)、"Disable JavaScript"(临时禁用脚本)、"Show Rendering"(渲染面板)。养成用命令面板的习惯之后,DevTools 的使用效率会明显上一个台阶,因为不用记菜单在哪,只要记得关键词。
3. 清晰度、长度、内存三方拉扯下的参数取舍
3.1 DPR 决定了清晰度的天花板
截图糊不糊,核心变量是设备像素比,也就是 DPR(devicePixelRatio)。它的含义是"一个 CSS 像素对应几个物理像素"。截图输出的实际像素宽度 = CSS 宽度 × DPR。一个 1440 CSS 像素宽的页面,DPR 为 1 时输出 1440 像素宽的图,DPR 为 2 时输出 2880 像素宽,DPR 为 3 时输出 4320 像素宽。
但这里有个平方关系要拎清楚:宽度翻倍,高度也翻倍,总面积是四倍,到 DPR 3 就是九倍。一张本来 2MB 的图,DPR 调到 3 之后可能变成十几 MB,处理器和内存压力也跟着涨。所以"越高清越好"是个伪命题,得看图的用途。
我的经验取值是这样的:存档和文档配图,DPR 用 2 足够,放大到 200% 还能看清字;只是内部沟通、贴到聊天工具里,DPR 1 就行,反正对方也是在小窗里看;要做投屏演示或者打印,DPR 2 到 3;超长页面(比如几万像素高的商品详情页),DPR 反而要压到 1 或者 1.5,原因下一小节说。
调整 DPR 的位置在设备模拟里:按 Ctrl+Shift+M(macOS 是 Cmd+Shift+M)打开设备工具栏,右上角有个 DPR 下拉框,默认是 "Device pixel ratio",可以手动选 1、2、3 或者自定义。设完之后再执行截图命令,输出的就是对应倍率。
3.2 超长页面为什么会截断,甚至出黑块
这是整页截图最典型的翻车现场:页面很长,截出来的图底部有一段纯黑,或者干脆被切掉。原因不在 Chrome 的截图逻辑,而在图形层的纹理尺寸上限。浏览器渲染页面时会分配一块纹理,这块纹理有最大尺寸限制,常见的是 16384 × 16384 像素,部分显卡或驱动下只有 8192。
算一下就明白了。一个高度 20000 CSS 像素的页面,DPR 设 2,需要渲染的物理高度就是 40000 像素,直接超过 16384 的上限,超出的部分要么黑,要么空白,要么整张图直接失败。这也解释了为什么"短页面截得好好的,长页面就不行"——不是操作问题,是撞到了硬件墙。
应对办法有三条。第一,降 DPR。同样 20000 高的页面,DPR 设 1,物理高度 20000,还在安全范围内(虽然接近上限)。第二,分段截再拼。用 CDP 的 clip 参数指定 y 偏移和高度,一块一块截,最后用工具纵向拼接。第三,如果只是要"内容完整"不要"一张图",那按屏截多张反而更好管理和查看。
分段截的具体做法在后面第五节的脚本里会给示例,核心是clip: { x, y, width, height, scale },其中 y 逐段递增,每段高度不超过 8000 像素留点余量。拼接可以用命令行图片工具做纵向追加,也可以写几行脚本处理。
3.3 内存压力和标签页崩溃
比截断更难受的是截到一半标签页直接崩了,之前加载的页面状态全丢。这在低配笔记本、同时开二三十个标签、或者页面本身很重(大量图片、视频、复杂动画)的情况下并不罕见。
几个降低风险的习惯:截图前把无关标签页关掉,尤其是那些也在跑动画或者视频的;DPR 别一上来就设 3,先出 DPR 1 的版本看看效果,不够再提;超长页面老老实实分段,不要指望一次成功;如果页面有自动播放的视频或者不停刷新的图表,先暂停它们。
还有一个小技巧:截图前在 DevTools 的 Performance 面板里点一下录制再停掉,意义不大,但可以在 Console 里跑一句performance.memory看看当前堆占用(仅部分环境支持)。更实用的做法是直接看任务管理器——Chrome 自带任务管理器(Shift+Esc),能看到每个标签的内存占用,超过 1GB 的标签页截长图风险就比较高。
4. 按下截图键之前的准备:懒加载、吸顶栏与动画
4.1 懒加载图片:先滚一遍再截
现在绝大多数内容型站点都用懒加载,图片只有进入视口附近才开始请求。整页截图的原理是把视口拉高到整个文档高度再渲染,理论上所有图片都"进入视口"了,应该都能加载。但实际经常不是这样,因为很多实现用的是 IntersectionObserver 配合节流,视口瞬间变化时回调不一定来得及执行完,结果就是截出来一片灰底占位图。
最可靠的解法是手工"预热":在截图之前,用脚本把页面从头到尾平滑滚一遍,让每一屏的懒加载都实际触发一次,再滚回顶部等图片加载完,然后截图。在 Console 里粘这段就能跑:
await new Promise((resolve) => { let y = 0; const step = window.innerHeight * 0.8; const timer = setInterval(() => { window.scrollBy(0, step); y += step; if (y >= document.documentElement.scrollHeight) { clearInterval(timer); window.scrollTo(0, 0); setTimeout(resolve, 800); } }, 150); });这里有几个参数值得说明。step取视口高度的 0.8 倍,是为了保证相邻两次滚动有重叠,不会漏掉正好卡在两屏交界处的元素;间隔 150 毫秒是给网络请求留时间,网速慢的话可以调到 250;滚回顶部之后等 800 毫秒,是等最后一批图片解码完成。
如果站点图片特别多,滚一遍不够,可以滚两遍。第一遍触发请求,第二遍确认都已加载。真正的验证方法是打开 Network 面板,按 Img 过滤,看还有没有 pending 的请求。
4.2 吸顶导航和固定定位元素的处理
整页截图里,position: fixed的元素(典型的是吸顶导航栏、悬浮客服按钮、右下角的回到顶部)会出现得比较奇怪。有些实现下它们只出现在图的顶部一次,这是正常的;但也有实现会让它们跟着视口"粘"在某个中间位置,或者重复出现好几次,看起来很诡异。
我的处理原则是:截图是为了看内容,不是为了还原交互,所以这些浮动元素在长图里基本是干扰,直接临时隐藏最省事。在 Console 里执行:
document.querySelectorAll('header, nav, [class*="float"], [class*="fixed"]').forEach((el) => { const pos = getComputedStyle(el).position; if (pos === 'fixed' || pos === 'sticky') { el.style.setProperty('visibility', 'hidden', 'important'); } });注意这里用的是visibility: hidden而不是display: none。区别在于前者保留占位,不会引起页面重排导致布局变化;后者会把元素从文档流里拿掉,可能让下面的内容往上顶,反而破坏了原本的排版。这个细节我是踩过一次才记住的——当时为了去掉一个悬浮广告位,用了 display:none,结果整页布局往上缩了一截,截出来的图和实际页面根本对不上。
上面那段选择器写得很粗,实际用的时候建议先用 Elements 面板找到那个具体的元素,右键 Copy selector,拿到精确选择器再操作,避免误伤正常内容。
4.3 字体、动画和 hover 状态的定格
字体问题比较隐蔽。页面用了自定义 web font 的时候,如果字体文件还没下载完就截图,会看到文字先用系统默认字体渲染,字形、字宽都不一样,整页排版看起来就"差一点"。判断方法是在 Console 里执行document.fonts.status,返回loaded才表示全部就绪。更稳的写法是await document.fonts.ready,等它 resolve 之后再截图。
动画的处理更简单粗暴。循环播放的轮播图、呼吸灯效果、加载骨架屏,任何一个在截图瞬间处于中间态,都会让图看起来"脏"。统一做法是注入一段样式把动画和过渡全停掉:
const style = document.createElement('style'); style.textContent = ` *, *::before, *::after { animation-play-state: paused !important; animation-delay: -1ms !important; transition: none !important; } `; document.head.appendChild(style);用animation-play-state: paused而不是直接animation: none,是因为后者会让元素瞬间跳回初始状态,有些依赖动画结束态的组件(比如淡入完成后才显示的内容)会直接消失,截出来缺一块。暂停则是把它定格在当前帧,视觉上更接近真实状态。
hover 状态则是另一回事。按钮的悬停样式、表格的行高亮、下拉菜单展开态,这些只有在交互时才出现。DevTools 的 Elements 面板里有个:hov按钮,可以强制给选中元素加上:hover、:active、:focus、:visited状态,加完之后页面上就固定显示悬停效果,这时候再截图就能拿到"交互中"的画面。写操作文档、做组件验收的时候这个功能特别好用。
5. 把截图写进脚本:批量出图的流水线
5.1 先看清楚底层的截图接口
命令面板背后调的是 Chrome DevTools Protocol(CDP)的Page.captureScreenshot方法。了解它的参数,才能理解为什么有些效果在界面上调不出来,但在脚本里可以。
主要参数有这么几个:format指定输出格式,可选 png、jpeg、webp;quality是压缩质量,0 到 100,只对 jpeg 和 webp 生效,png 传了也没用;clip是一个矩形区域,包含 x、y、width、height 和一个 scale 缩放因子,这是分段截图的关键;captureBeyondViewport是个布尔值,设成 true 才能截到视口之外的内容,整页截图必须开;fromSurface控制是从渲染表面抓还是从视图抓,一般保持默认 true;optimizeForSpeed牺牲一点图片质量换速度,批量跑的时候可以开。
直接用 CDP 的写法长这样,适合嵌到已有的调试流程里:
const client = await page.createCDPSession(); const { data } = await client.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true, clip: { x: 0, y: 0, width: 1440, height: 8000, scale: 1 } }); require('fs').writeFileSync('segment-0.png', Buffer.from(data, 'base64'));clip里的 height 就是这一段的高度。要分段的话,把 y 依次加上 8000,循环调用,最后拼起来。scale 参数是额外倍率,和 DPR 是两个维度,一般保持 1,清晰度交给 DPR 控制,两个都调容易算乱。
5.2 一份能直接跑的批量脚本
单张截图手动操作就够了,需要一次出几十张的时候就得靠脚本。下面这份用 Node 加 Puppeteer,读一个 JSON 列表,逐条打开、预热、截图、命名。我在几个项目里都用这个骨架,改改选择器就能用。
const fs = require('fs'); const puppeteer = require('puppeteer'); const targets = JSON.parse(fs.readFileSync('targets.json', 'utf8')); async function warmup(page) { await page.evaluate(() => document.fonts.ready); await page.evaluate(async () => { await new Promise((resolve) => { let y = 0; const step = window.innerHeight * 0.8; const timer = setInterval(() => { window.scrollBy(0, step); y += step; if (y >= document.documentElement.scrollHeight) { clearInterval(timer); window.scrollTo(0, 0); setTimeout(resolve, 800); } }, 150); }); }); } (async () => { const browser = await puppeteer.launch({ headless: 'new' }); for (const item of targets) { const page = await browser.newPage(); try { await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 2 }); await page.goto(item.url, { waitUntil: 'networkidle2', timeout: 60000 }); await warmup(page); await page.screenshot({ path: item.out, fullPage: true, type: 'png' }); console.log('ok', item.out); } catch (err) { console.error('fail', item.url, err.message); } finally { await page.close(); } } await browser.close(); })();配套的targets.json可以这样写,把输出文件名也放进去,避免事后对不上号:
[ { "url": "https://example.com/a", "out": "2024-06-01_home_1440_dpr2.png" }, { "url": "https://example.com/b", "out": "2024-06-01_detail_1440_dpr2.png" } ]几个容易出问题的点。waitUntil: 'networkidle2'表示网络空闲(并发请求不超过 2 个)才继续,比load更稳,但遇到长轮询或者 websocket 的站点可能永远不空闲,这时候得改成domcontentloaded再手动等固定时间。deviceScaleFactor就是 DPR,和界面上调的那个是同一个东西。headless: 'new'在新版本里可以直接写true,老版本写'new'会报错,按你装的版本选。
5.3 并发、超时和失败重试
批量跑最容易犯的错是一口气开二十个页面。内存扛不住,网络也被自己打满,结果一半超时。我的做法是并发控制在 2 到 3,用队列慢慢推,整体耗时反而更短,因为失败率低。
超时时间给宽一点,60 秒起步。有些页面首屏就有几十张图,网络稍差就要十几秒。失败重试两次足够,重试前把 page 关掉重新开,不要复用已经出错的页面,那个状态往往已经脏了。
还有个细节:每次截图都新建 page,截完立刻 close。别想着复用一个 page 循环 goto,因为前一个页面的 CSS、localStorage、Service Worker 都可能影响下一个页面,出问题时排查成本远高于省下的那点启动时间。
5.4 命名规范:三个月后你还找得到
批量截图最大的后遗症是文件管理。几十张screenshot(1).png、screenshot(2).png堆在一个文件夹里,过两周自己都不知道哪张对应哪个页面。我现在的命名规则是日期_页面标识_视口宽_dpr倍率.扩展名,比如20240601_product-list_1440_dpr2.png。
这个格式的好处是排序即分组,同一天的排在一起,同一个页面的不同倍率也挨着;信息自解释,不用打开图就知道是什么;前缀是日期,天然按时间倒序。如果项目多,再套一层目录,按项目名分文件夹。这套规则听起来啰嗦,但真正在处理几十上百张图的时候,能省下的时间很可观。
6. 从出图到交付:格式、压缩和一个反直觉的结论
6.1 PNG、JPEG、WebP 的实际差别
格式选择很多人随手就定了,其实差别挺大。PNG 是无损压缩,支持透明通道,文字边缘锐利,特别适合界面截图、代码截图、图表这类有大片纯色和细线条的内容。缺点是体积大,一张 2880 宽的整页 PNG 轻松上到七八 MB。
JPEG 是有损压缩,不支持透明,适合照片类内容。截图用 JPEG 的话,文字边缘会出现一圈杂色,放大看尤其明显,所以我一般不推荐截图存 JPEG,除非是那种以图片为主、文字很少的页面,而且体积确实需要压。
WebP 是两者之间的折中,支持有损也支持无损,同样画质下体积通常比 PNG 小 30% 到 50%,现在的浏览器和大部分看图工具都支持。我的习惯是:存档留 PNG,对外分享和嵌文档用 WebP。
| 格式 | 压缩方式 | 透明 | 文字锐度 | 体积 | 建议用途 |
|---|---|---|---|---|---|
| PNG | 无损 | 支持 | 高 | 大 | 存档、需要再编辑 |
| JPEG | 有损 | 不支持 | 低 | 小 | 照片为主的页面 |
| WebP | 两种都支持 | 支持 | 高 | 中 | 分享、嵌文档、网页展示 |
6.2 压缩这一步别跳过
PNG 截出来之后还有不小的压缩空间,用无损优化工具处理一遍,通常能再小 20% 到 40%,画质一点不掉。命令行工具里pngquant和oxipng都很好用,前者是有损量化但肉眼几乎看不出差别,后者是纯无损优化。批量处理的写法很简单,一行循环就能跑完一个目录。
for f in *.png; do oxipng -o 2 --strip safe "$f" done-o 2是优化等级,1 到 6,等级越高越慢,2 到 3 在日常使用里性价比最好。--strip safe会去掉不影响显示的元数据,注意是 safe 模式,不会破坏色彩配置文件。
如果转 WebP,cwebp的默认质量 75 已经够用,文字多的图可以提到 85 到 90,避免出现文字边缘的块状噪点。这个参数需要试几次,每次拿同一张图比一下,找到体积和清晰度的平衡点。
6.3 一个和直觉相反的结论:清晰度不是越高越好
聊到最后想纠正一个常见误区。很多人觉得截图当然越清晰越好,DPR 直接拉满,结果交付出去的文档里嵌着几张十几 MB 的图,打开卡半天,而且因为图太宽,在文档里被自动缩放,读者反而看不清整体结构。
截图清晰度的目标是"在最终使用场景下看得清",不是"像素越多越好"。嵌在文档里、宽度被限制在 800 像素的图,源图 1440 宽(DPR 1)就完全够了,做成 4320 宽纯属浪费。反过来,如果这张图要投到大屏上给人看细节,那 DPR 2 甚至 3 才有意义。
我现在的做法是同一个页面出两版:DPR 1 的用于文档嵌入和快速预览,DPR 2 的用于存档和需要放大看的场合。多花几分钟,后面省下的是别人打开文档时的等待时间,以及自己解释"这图为什么这么大"的口舌。
顺带说个和截图质量无关但很影响体验的小事:截整页长图之后,在发给别人之前自己从头到尾看一遍。重点看三处——顶部导航区域、页面中段第一个图片模块、最底部。这几个地方分别是固定定位问题、懒加载问题和截断问题的高发区,花三十秒扫一遍,能避免大部分"发出去才发现有问题"的尴尬。