SingleFile 原本是个浏览器扩展,一键把当前网页存成单个 HTML 文件。single-file-cli 是它的命令行版:给一个网址,它调用 Chrome 真实渲染页面,再把样式、图片、字体全部内联进一个文件。

实测三个不同复杂度的页面:极简页面 7.25 秒存成 2777 字节,本站文章页 10.15 秒存成 669714 字节,维基百科条目 11.47 秒存成 404328 字节。三份存档里的外部图片引用都是 0 个。

npm 装完就能用,但 Node 22 上有个坑

npm install single-file-cli

node --import ./sf_polyfill.mjs node_modules/single-file-cli/single-file-node.js \
     --browser-executable-path "C:\Program Files\Google\Chrome\Application\chrome.exe" \
     --browser-wait-delay 2000 https://example.com out.html

如果 Node 版本是 22 或 24,直接跑会报错 CloseEvent is not defined 并退出。原因是它依赖的 puppeteer 假设 Node 有全局 CloseEvent,而这两个版本都没有。

解决办法是启动前先补一个:

if (typeof globalThis.CloseEvent === "undefined") {
  globalThis.CloseEvent = class CloseEvent extends Event {
    constructor(type, init = {}) {
      super(type, init);
      this.code = init.code || 0;
      this.reason = init.reason || "";
    }
  };
}

把这段存成 sf_polyfill.mjs,用 node --import 指向它启动就行。路径要写成 file:/// 开头的绝对地址,相对路径在 ESM 里会报 ERR_UNSUPPORTED_ESM_URL_SCHEME。

它运行时 0 行输出,跑完直接在指定位置生成文件。确认成功只能看目标文件在不在、字节数对不对。

三个页面,7 到 11 秒

页面耗时存档体积原始 HTML倍数
example.com7.25 秒2777 字节577 字节4.8 倍
本站文章页10.15 秒669714 字节25694 字节26.1 倍
维基百科条目11.47 秒404328 字节——
等待延迟参数2000 毫秒———

7.25 秒这个底线时间基本是 Chrome 启动加页面渲染的固定成本。参数里给了 2000 毫秒的等待延迟,所以真正的抓取动作大概占一半。

页面越复杂,多出来的时间主要花在下载和编码内联资源上。维基条目有 67 个内联图片,比本站文章页的 11 个多得多,但两者耗时只差 1.3 秒,因为图片本身都不大。

体积是原始 HTML 的 26 倍

本站文章页的原始 HTML 只有 25694 字节,存下来变成 669714 字节。多出来的 640 KB 全是内联进去的图片和样式:

指标本站文章页维基条目
img 标签数10 个9 个
data URI 数11 个67 个
指向外部的 img src0 个0 个
style 标签4 个16 个
script 标签1 个1 个
存档体积669714 字节404328 字节

26 倍这个数字看着夸张,但它换来的是「不依赖任何外部资源」。图片被 base64 编码进 HTML,体积天生会涨约三分之一,再加上原本压缩传输的图片被解开后重新编码,膨胀更明显。

同样的内容如果只存 HTML 不存图,文件只有 25 KB 左右,但打开一片空白,图全裂。多出来的 640 KB 买的就是这些图。

断网打开,8 张图全在

把维基条目的存档用 file:// 打开,不联外网,9 个 img 标签里有 8 个正常显示。第 9 个的 src 是空的 data:,,它本身就不是图片节点。

title: Web scraping - Wikipedia
images loaded: 8 / 9     (第 9 个 src 是 data:, 空占位)

<img src="data:image/webp;base64,UklGRjwNAABXRUJQV...">
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz...">

第一次打开时只显示 4 张,滚动到底部后变成 8 张,原因是页面本身用了懒加载,图片要进入视口才渲染。不是存档漏了资源,是原页面的行为被一起保存下来了。

和 monolith 的分工

站内另有一篇 monolith,同样是「把网页存成一个文件」。两者的区别在渲染方式:monolith 自己解析 HTML 再抓资源,速度快但不执行 JavaScript;SingleFile 走真实 Chrome 渲染,页面上的动态内容能存下来,代价是要启动浏览器、慢一些。

需要存「JavaScript 渲染出来的内容」,选 SingleFile。只是存一个静态文档、想快,monolith 更合适,它没有浏览器启动那 7 秒的固定开销。

留档的忠实度也不同:SingleFile 存渲染后的 DOM,页面长什么样就存什么样;monolith 存原始 HTML 加资源。

什么时候值得用

需要长期留存的资料页,尤其是那些过几个月就改版或消失的文档、公告、论坛帖。存成单个 HTML 之后,不用管对方网站还在不在,双击就能看。

另一个场景是取证:页面上有什么、什么时候存的,669714 字节的单个文件就是全部证据。

不适合批量抓取:每次起一个 Chrome,7 秒起步,100 个页面就是 12 分钟以上。