Transformers.js 为 Web 开发者提供了一种简单的方式,通过任务特定的管道(pipeline)在其 Web 应用中发挥 Transformer 架构的强大能力。要在浏览器中运行推理,开发者需要创建一个 `pipeline()` 实例,并指定该管道要使用的任务。作为一个具体示例,以下代码片段展示了如何设置一个自动语音识别(ASR)管道。
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0';
const asr = await pipeline(
'automatic-speech-recognition',
'Xenova/whisper-tiny.en',
{ device: 'webgpu' },
);
const result = await asr('jfk.wav');
console.log(result);
缓存挑战
你会注意到,在源代码中我指定了 `Xenova/whisper-tiny.en` 作为模型,这对于常见的英语自动语音识别任务来说是一个非常合适的选择。事实上,根据 Transformers.js 的默认模型解析规则(如所引用的摘录所示),它甚至是默认模型。
模型资源
当你在浏览器中运行这个示例时,Transformers.js 会自动处理相关模型资源和 Wasm 文件的下载与缓存。以下截图展示了访问该应用后 Chrome DevTools 的缓存存储部分。当你重新加载页面时,资源会从缓存 API 中提供,模型几乎能立即返回结果。
然而,`Xenova/whisper-tiny.en` 作为一个流行模型(如前所述,甚至是 Transformers.js 中 ASR 的默认模型),你可以想象,你访问的不仅仅是一个应用会使用它。为了模拟这种情况,这里还是之前的同一个示例应用,但托管在另一个不同的源上。当你访问这个不同源的应用时,浏览器并不能几乎立即使用模型,而是必须再次下载并缓存所有模型资源,即使这些资源与之前逐字节完全相同。即使在这个简单的示例中,这也导致了高达 177 MB 的重复下载和存储,你可以在 Chrome DevTools 应用面板的存储部分查看。可以想象,这种情况会迅速累积。
Wasm 运行时资源
但情况更糟。让我们给这个简单示例添加第二个管道:情感分析。情感分析默认使用 `Xenova/distilbert-base-uncased-finetuned-sst-2-english` 模型。由于没有指定模型,Transformers.js 的默认模型解析规则会自动为你选择它。
const classifier = await pipeline('sentiment-analysis');
const sentiment = await classifier(result.text);
pre.append('\n\n' + JSON.stringify(sentiment, null, 2));
两个完全不同的大模型,但它们都依赖于同一个 4,733 kB 的 ort-wasm-simd-threaded.asyncify.wasm WebAssembly(Wasm)运行时文件,该文件来自 Transformers.js 所基于的底层 ONNX Runtime 库。在不同源上打开扩展演示,你会在网络标签页中注意到,Wasm 运行时也会被重新下载并再次缓存。
因此,即使你运行的应用不共享相同的大模型,你的浏览器仍然会对你已经拥有的共享 Wasm 资源发出冗余请求,并且还会再次缓存它们,从而占用硬盘空间。
缓存隔离
大模型资源服务
默认情况下,大模型资源来自 Hugging Face Hub,最终来自 Hugging Face CDN。浏览器会请求类似 `https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json` 的资源,然后该请求会被重定向到最终的 CDN URL,例如本例中的 `https://huggingface.co/api/resolve-cache/models/Xenova/distilbert-base-uncased-finetuned-sst-2-english/0b6928efcb76139cae2c6881d49cda67fe119f42/config.json?%2FXenova%2Fdistilbert-base-uncased-finetuned-sst-2-english%2Fresolve%2Fmain%2Fconfig.json=&etag=%223c36342ef1f74de2797d667c68c6b7b988d0b87c%22`。
Wasm 运行时资源服务
默认情况下,Wasm 运行时资源来自 jsDelivr CDN。例如,在撰写本文时,ort-wasm-simd-threaded.asyncify.wasm 来自 `https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm`。
你可能会说,如果不同的应用,即使运行在不同的源上,最终都是从同一个 CDN URL 提供其资源,那么只要最终的 URL 相同,缓存就不应该成为问题。不幸的是,长期以来,浏览器中的缓存机制并非如此运作。文章《通过分区缓存获得安全性与隐私性》详细介绍了所有细节,但本质上,缓存是按源进行隔离的,以防止时序攻击:网站响应 HTTP 请求所花费的时间可能会泄露浏览器过去曾访问过同一资源,这使得浏览器容易受到安全和隐私泄露的攻击。
Chrome 的实现
具体的实现可能因浏览器而异,但在 Chrome 中,缓存资源除了使用资源 URL 作为键之外,还使用网络隔离键作为键。网络隔离键由顶级站点和当前框架站点组成。以前面托管在源 https://googlechrome.github.io 和 https://rawcdn.rawgit.net 上的玩具示例为例。如果它们都使用来自 https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm 的 Wasm 运行时,它们的缓存键将如下表所示。
| 网络隔离键 | 资源 URL | |
|---|---|---|
| 顶级站点 | 当前框架站点 | |
https://googlechrome.github.io | https://googlechrome.github.io | https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm |
https://rawcdn.rawgit.net | https://rawcdn.rawgit.net | https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0-dev.20260416-b7804b056c/dist/ort-wasm-simd-threaded.asyncify.wasm |
因此,即使资源 URL 完全相同,由于网络隔离键不匹配,也不会发生缓存命中,这意味着重复下载和重复存储。这就是跨源存储提案旨在解决的挑战。
跨源存储 API 登场
💡 注意:跨源存储 API 是一个早期阶段的提案,尚未最终确定。虽然提议的 API 尚未在任何浏览器中原生实现,但你无需等待即可进行实验。安装跨源存储扩展,即可在所有页面上注入 navigator.crossOriginStorage 的 polyfill,并测试完整的流程。
提议的跨源存储(COS)API 引入了一个专用的 navigator.crossOriginStorage 接口,通过该接口,Web 应用可以跨源边界存储和检索大型文件,这些文件不是通过 URL 来标识,而是通过加密哈希来标识。
最后一点关于加密哈希是关键。因为 COS 通过文件的哈希值而非其 URL 或来源来识别文件,所以你在访问 `https://googlechrome.github.io` 时下载的同一个 `ort-wasm-simd-threaded.asyncify.wasm` Wasm 运行时,会被识别为与 `https://rawcdn.rawgit.net` 即将请求的文件相同,无论这两个来源是从哪里获取它的。请参见以下代码片段,它说明了基本流程。
const hash = {
algorithm: 'SHA-256',
value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4',
};
try {
const handle = await navigator.crossOriginStorage.requestFileHandle(hash);
const fileBlob = await handle.getFile();
} catch (err) {
const fileBlob = await fetch('https://cdn.jsdelivr.net/.../ort-wasm-simd-threaded.asyncify.wasm')
.then(r => r.blob());
const handle = await navigator.crossOriginStorage.requestFileHandle(
hash,
{ create: true, origins: '*' },
);
const writableStream = await handle.createWritable();
await writableStream.write(fileBlob);
await writableStream.close();
}
如果资源在 COS 中,你会得到一个 `FileSystemFileHandle`,你可以通过 `getFile()` 直接从其中读取 blob(生成的 `File` 继承自 `Blob`)。如果资源不在 COS 中,你会回退到网络,并将该资源写入 COS,供下一个需要它的应用使用,这个应用可能是你的应用,也可能是另一个不相关的应用,甚至可能来自完全不同的源。
该 API 特意模仿了文件系统标准中的 `FileSystemDirectoryHandle.getFileHandle()`,你可能从源私有文件系统(OPFS)API 中熟悉它。哈希参数扮演的角色与 OPFS 中的名称参数相同:唯一标识一个资源。`options.create` 标志的工作方式也相同:缺失或为 `false` 表示只读访问,为 `true` 表示你打算写入。
控制谁能读取什么
并非每个资源都应在全局共享。COS 通过存储文件时的 `origins` 选项,让开发者能够精确控制可见性。
- 设置 `origins: '*'` 会使文件全局可用。任何源都可以通过哈希找到它。这对于 AI 模型资源或 Transformers.js 示例中的 Wasm 运行时来说是正确选择:其核心意义在于,Web 上的每个应用都能受益于单个缓存副本。
- 传递一个特定的源列表,例如 `origins: ['https://write.example.com', 'https://calculate.example.com']`,会将访问权限限制在这些站点。这对于在公司自有资产之间共享、且不应被其他任何人发现的专有资源非常适用,例如商业办公套件中使用的专有校对 AI 模型。
- 完全省略来源会使文件仅对同站来源可用。对于组织内所有子域共享的资源而言,这是一个合理的默认设置,但不应跨越组织边界使用。
一条重要规则:可见性可以提升,但绝不能降低。如果某个文件已全局可用,后续试图以受限来源列表存储该文件的操作将被静默忽略。这可以防止恶意行为者重新存储公共资源并缩小其可用范围。反向操作是可行的:最初以受限来源列表存储的文件,后续可以放宽访问权限。任何站点(不限于原始存储者)都可以针对同一哈希值调用 `requestFileHandle()`(哈希值并非秘密),并传入 `create: true` 和更宽泛的 origins 值,只要浏览器验证哈希值匹配,该资源从此便对更广泛的受众可用。请注意,提升权限的站点仍必须通过返回的句柄写入完整文件。此要求旨在防止站点利用权限提升路径作为侧信道,来检测特定文件是否已存储在 COS 中。
设计上的完整性
COS 一个微妙但重要的特性是,浏览器在写入文件时会验证哈希值。如果写入的数据与声明的哈希值不匹配,写入操作将失败并报错。这使得完整性检查成为自动行为:从 COS 读取文件的应用程序可以确信它获取的正是预期的字节内容。这与通过网络下载后自行计算哈希值所能获得的保证相同。
这在 Transformers.js 场景中具有双重价值。目前,大多数应用程序在下载模型权重后,实际上无法验证 CDN 是否提供了正确的字节内容。而使用 COS,存储中的每个文件在写入时都会隐式完成验证,无论文件来自何处——无论是官方的 Hugging Face CDN 还是某个随机站点的自建镜像。
不牺牲实用性的隐私保护
当然,跨源共享缓存也提出了与分区 HTTP 缓存相反的问题:如果任何网站都可以通过哈希值来探测某个文件是否存在,那么攻击者是否可以通过检查某个游戏引擎的 Wasm 模块是否被缓存,来了解用户的浏览历史呢?
COS 通过两种互补机制来解决这个问题:
- 首先,`origins` 字段:不应被全局探测的专有资源,只需避免使用 `origins: '*'` 来存储即可。通过开发者教育,鼓励开发者在合理的情况下考虑这一点。
- 其次,可用性门控:即使是全局声明的文件,如果该文件尚未在足够多的不同源上被访问过,浏览器也可能抑制对该文件存在性的确认。一个只出现在一两个网站上的文件,仍可能被用作跨站标识符,因此浏览器可能会返回一个错误,仿佛该文件根本不存在,无论磁盘上实际存储了什么。在 Chrome 团队,我们意识到不常见资源可能造成的隐私泄露,并计划通过限制哪些具体资源可以被缓存来普遍缓解这一问题。具体的缓解措施仍在完善中。
关键在于,这意味着错误并非一个确定的答案。它可能意味着“未存储”,也可能意味着“已存储,但浏览器不告诉你”。应用应始终以相同方式处理:回退到网络请求。
这对 Transformers.js 示例意味着什么
回到之前的玩具示例:`ort-wasm-simd-threaded.asyncify.wasm` 运行时体积为 4,733 kB,所有基于 Transformers.js 的应用无论使用哪种 AI 模型,都会共享该文件。使用 COS 后,第一个加载该文件的应用会将其下载一次,并以 SHA-256 哈希值作为键、`origins: '*'` 为权限存储起来。后续任何应用,无论来自 `https://googlechrome.github.io`、`https://rawcdn.rawgit.net` 还是其他源,都能立即在 COS 中找到它。那 177 MB 重复的 Whisper 模型权重呢?同样如此:`Xenova/whisper-tiny.en` 只会被下载一次,第二次通过哈希识别,从 COS 毫秒级返回。当然,`Xenova/distilbert-base-uncased-finetuned-sst-2-english` 也是如此。
Transformers.js 本身已经在库级别试点 COS API。Pull Request #1549 引入了一个实验性的 COS 缓存后端,通过一个 opt-in 标志启用。在设置 pipeline 之前,只需一行代码即可开启:
import { env, pipeline } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0";
env.experimental_useCrossOriginStorage = true;
const asr = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en', { device: 'webgpu' });
const result = await asr('jfk.wav');
console.log(result);
设置该标志后,Transformers.js 会通过获取原始 Xet 指针(示例原始指针文件)并提取其 `oid sha256:` 字段,来解析每个 Xet 跟踪的模型文件(大型 ONNX 权重文件)的 SHA-256 哈希值。然后,它将该哈希值作为 `navigator.crossOriginStorage` 的键。如果模型已存在于 COS 中(因为其他站点已先存储了它),则无需网络往返即可立即提供。如果不存在,则回退到常规下载,并将结果存入 COS 供后续调用者使用。以玩具示例来说,实际优势在于 `Xenova/whisper-tiny.en`、`Xenova/distilbert-base-uncased-finetuned-sst-2-english`(当然还有 `ort-wasm-simd-threaded.asyncify.wasm`)只需跨网络传输一次,无论有多少不同源请求它们。
注意标志上的 `experimental_` 前缀。这是有意为之,表明底层浏览器 API 尚未标准化,且可能在不进行主版本号升级的情况下发生变化。
今天就试试吧。
COS API 目前尚未在任何浏览器中原生实现,但你无需等待即可进行实验。安装 Cross-Origin Storage 扩展,即可在所有页面上注入 `navigator.crossOriginStorage` polyfill,并测试完整流程。你可以查看该扩展的源代码,并按照使用说明开始操作。
安装扩展后,你现在就可以体验完整的端到端流程:打开第一个启用了 COS 的玩具示例,让它加载 `Xenova/whisper-tiny.en`,然后从第二个源打开同样启用了 COS 的玩具示例。此时,模型不再像之前那样需要重新下载 177 MB,而是从 COS 中毫秒级加载。当你打开扩展的弹出窗口时,可以看到 COS 正在运行。如果按资源查看,你可以看到 SHA-256 哈希值为 `950978b1dbcbf250335358c1236053ba19a7f7849b33dc777f4421b72b7626fa` 的资源在 `https://googlechrome.github.io` 和 `https://rawcdn.rawgit.net` 之间共享。这一点可能不太明显,但你可以通过对比 Hugging Face 上的 SHA-256 哈希值来验证,你看到的正是 `https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnx`。目前,该扩展主要面向像你这样的高级用户。一旦在浏览器中实现,将会在浏览器的设置页面中提供更友好的集成。下面的截图显示了扩展的弹出窗口,其中“按资源查看”选项卡处于激活状态,你可以看到共享资源及其哈希值,以及将其存储在 COS 缓存中的两个源。
行动号召
如果你正在构建自己的 Transformers.js 应用,行动号召很简单:在首次调用 `pipeline()` 之前添加 `env.experimental_useCrossOriginStorage = true`,安装扩展,然后观察网络面板中重复的下载消失。每个选择加入的站点都能让其他站点的用户体验更快、成本更低。选择加入完全没有风险:如果用户未安装 COS 扩展导致 COS API 不受支持,代码会回退到默认路径(Web Cache API)。
Transformers.js 并非唯一在尝试 COS 的库。WebLLM(可选启用,详见文档)和 wllama(自动启用,参见 PR)同样对这一提议中的 API 感到兴奋。
在 Chrome 团队中,我们正在考虑在浏览器中原生实现 COS API。作为一项早期阶段的提案,我们欢迎各方就 API 本身以及提案的形态提供反馈。Cross-Origin Storage 仓库是提交问题、表达支持或发起 PR 的地方。
本文提及的模型 2
transformers.js
javascript
2026 年 4 月 23 日
公告
transformers.js
transformers
Transformers.js v4:现已登陆 NPM!
2026 年 2 月 9 日