在 Transformers.js 中实验提议的跨源存储 API
这个Chrome提案让不同网站的AI模型共享缓存,对用Transformers.js的Web开发者是切实的性能改进,但还只是早期实验。
Transformers.js 在浏览器中运行 AI 模型时,不同来源的 Web 应用会重复下载并缓存相同的模型资源(如 Xenova/whisper-tiny.en)和 Wasm 运行时文件(如 4,733 kB 的 ort-wasm-simd-threaded.asyncify.wasm),即使资源 URL 相同,浏览器因 Network Isolation Key 隔离缓存,单次 demo 就产生 177 MB 冗余下载和存储。Cross-Origin Storage API 是一项早期提案,旨在让跨来源应用共享缓存的模型和运行时资源。目前该 API 尚未在浏览器原生实现,但可通过 Chrome 扩展注入 polyfill 进行实验。
实验性:在 Transformers.js 中使用提议的跨源存储 API
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 缓存存储部分。当你重新加载页面时,资源会从 Cache API 中提供,模型几乎立即返回结果。
然而,Xenova/whisper-tiny.en 是一个流行的模型(并且如前所述,甚至是 Transformers.js 中的 ASR 默认模型),你可以想象不止一个你访问的应用会使用它。为了模拟这种情况,这里提供了与之前相同的示例应用,但通过不同的源(origin)提供服务。当你访问这个不同源的应用时,浏览器无法几乎立即使用它,而是必须重新下载并缓存所有模型资源,即使这些资源与之前的逐字节完全相同。即使在这个玩具示例中,这也会导致 177 MB 的重复下载和存储,你可以在 Chrome DevTools Application 面板的 Storage 部分中查看。你可以想象这很快就会累积起来。
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));
两个完全不同的 AI 模型,但它们都依赖同一个 4,733 kB 的 ort-wasm-simd-threaded.asyncify.wasm 文件——这是来自 Transformers.js 底层 ONNX Runtime 库的 WebAssembly(Wasm)运行时文件。在另一个源(origin)上打开扩展演示时,你会在“网络”标签页中注意到 Wasm 运行时也会被重新下载并再次缓存。
因此,即使你运行的应用不共享相同的 AI 模型,浏览器仍会对你已拥有的共享 Wasm 资源发出冗余请求,而且还会再次缓存它们,从而占用你硬盘上的空间。
缓存隔离
AI 模型资源分发
默认情况下,AI 模型资源来自 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 索引外,还会额外使用网络隔离键(Network Isolation Key)进行键控。网络隔离键由顶层站点(top-level site)和当前帧站点(current-frame site)组成。以之前托管在源 `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 尚未在任何浏览器中原生实现,但你无需等待即可进行实验。安装 Cross-Origin Storage 扩展,即可在所有页面上注入 `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 运行时来说是正确的选择:关键在于,所有网络应用都能从单一缓存副本中受益。
- 传入一个明确的来源列表,例如 `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 缓存后端,通过一个可选启用标志来控制。只需在设置 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
社区
· 或发表评论






