Hugging Face · 官方博客

在Transformers.js中实验提议的跨源存储API

Experimenting with the proposed Cross-Origin Storage API in Transformers.js

二〇二六年六月二十三日 · 英文原文

谷歌 Chrome 团队开发者关系工程师 Thomas Steiner 提出 Cross-Origin Storage(COS)API,通过加密哈希而非 URL 标识文件,使不同 origin 的 Web 应用可共享同一缓存。Transformers.js 库已通过 `env.experimental_useCrossOriginStorage = true` 标志实验性支持该 API。在示例中,Whisper 模型(177 MB)和 Wasm 运行时(4,733 kB)在跨 origin 场景下只需下载一次,后续应用直接从 COS 读取。该 API 支持 `origins` 参数控制可见性,并通过哈希验证保证完整性。Chrome 团队正考虑原生实现。

](https://huggingface.co/tomayac)

(本文是谷歌 Chrome 团队开发者关系工程师 Thomas Steiner 的客座文章。)

Transformers.js 为 Web 开发者提供了一种简单的方式,通过任务特定的 pipeline(管道)在其 Web 应用中使用 transformer(变换器)的强大能力。为了在浏览器中运行推理,开发者创建一个 pipeline() 实例,并指定他们想要使用的任务。作为一个具体示例,以下代码片段展示了如何设置一个自动语音识别(ASR)pipeline。

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);

Image 2: 自动语音识别 pipeline 的最小化示例。

缓存挑战

你会注意到在源代码中,我指定了 Xenova/whisper-tiny.en 作为模型,这对于常见的英语自动语音识别任务来说是一个非常合适的选择。事实上,根据 Transformers.js 的默认模型解析,它甚至是 默认 模型,如链接的摘录所示。

模型资源

当你在浏览器中运行这个示例时,Transformers.js 会自动处理下载和缓存相关的模型资源以及 Wasm 文件。以下截图显示了访问该应用后 Chrome DevTools 的 Cache storage 部分。当你重新加载页面时,资源会从 Cache API 提供,模型几乎立即返回结果。

Image 3: 访问应用后,Chrome DevTools 的 Cache storage 部分显示 Whisper AI 模型资源和 Wasm 运行时文件。

然而,Xenova/whisper-tiny.en 是一个流行的模型(如前所述,甚至是 Transformers.js 中 ASR 的 默认 模型),你可以想象不止一个你访问的应用会使用它。为了模拟这种情况,这里是之前的同一个示例应用,但来自一个不同的 origin。当你访问这个不同 origin 的应用时,浏览器不会几乎立即可用,而是必须再次下载并缓存所有模型资源,即使它们与之前逐字节相同。即使在这个玩具示例中,这也导致了 177 MB 的重复下载和存储,你可以在 Chrome DevTools 的 Application panelStorage 部分查看。你可以想象这会迅速累积。

Image 4: Chrome DevTools 的 Storage 概览显示使用了 177 MB 的存储空间。

Wasm 运行时资源

但情况更糟。让我们在玩具示例中添加第二个 pipeline:情感分析。情感分析默认使用 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));

Image 5: image

两个完全不同的 AI 模型,但它们依赖于相同的 4,733 kB ort-wasm-simd-threaded.asyncify.wasm WebAssembly(Wasm)运行时文件,该文件来自 Transformers.js 所基于的底层 ONNX Runtime 库。打开来自不同 origin 的扩展演示,你会注意到在 Network 标签页中,Wasm 运行时也被再次下载和缓存。

Image 6: Chrome DevTools 的 Network 面板显示 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

现在你可能会说,如果不同的应用,即使运行在不同的 origin 上,最终都从相同的 CDN URL 提供资源,只要最终的 URL 相同,缓存就不应该是问题。不幸的是,长期以来浏览器中的缓存并非如此。文章 通过分区缓存获得安全性和隐私 详细介绍了所有细节,但本质上,缓存是按 origin 隔离的,以防止时序攻击:网站响应 HTTP 请求所需的时间可以揭示浏览器过去是否访问过相同的资源,这使浏览器容易受到安全和隐私泄露的影响。

Chrome 的实现

具体实现可能因浏览器而异,但在 Chrome 中,缓存资源除了使用 资源 URL 外,还使用网络隔离键(Network Isolation Key)作为键。网络隔离键由顶级站点当前帧站点组成。以托管在 origin https://googlechrome.github.iohttps://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://rawcdn.rawgit.net https://rawcdn.rawgit.net

因此,即使资源 URL 完全相同,由于网络隔离键不匹配,不会发生缓存命中,这意味着重复下载和重复存储。这就是 Cross-Origin Storage 提案旨在解决的挑战。

Cross-Origin Storage API 登场

💡 注意: Cross-Origin Storage API 是一个早期阶段的提案,尚未最终确定。虽然提议的 API 尚未在任何浏览器中原生实现,但你无需等待即可进行实验。安装 Cross-Origin Storage 扩展 以在所有页面上注入 navigator.crossOriginStorage polyfill,并测试完整流程。

提议的 Cross-Origin Storage(COS)API 引入了一个专用的 navigator.crossOriginStorage 接口,通过该接口,Web 应用可以跨 origin 边界存储和检索大文件,这些文件不是通过 URL 标识,而是通过加密哈希标识。

Image 7: Cross-Origin Storage API 的徽标:一个风格化的行走人物,类似于通常在斑马线标志上看到的。

关于加密哈希的最后一点是关键。因为 COS 通过文件的哈希而不是 URL 或 origin 来标识文件,所以你在访问 https://googlechrome.github.io 时下载的同一个 ort-wasm-simd-threaded.asyncify.wasm Wasm 运行时会被识别为与 https://rawcdn.rawgit.net 即将请求的相同,无论这两个 origin 从哪里获取它。请参阅以下代码片段,它说明了基本流程。

const hash = {
  algorithm: 'SHA-256',
  value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4',
};

try {
  const handle = await navigator.crossOriginStorage.requestFileHandle(hash);
  // 缓存命中!将文件作为 Blob 获取并直接使用。
  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,供下一个需要它的应用使用,这可能是你的应用,也可能是另一个不相关的应用,可能位于完全不同的 origin 上。

该 API 特意模仿了 File System Standard 中的 FileSystemDirectoryHandle.getFileHandle(),你可能从 Origin Private File System(OPFS)API 中熟悉它。hash 参数扮演着与 OPFS 中 name 参数相同的角色:唯一标识一个资源。options.create 标志的工作方式相同:缺失或 false 表示只读访问,true 表示你打算写入。

控制谁可以读取什么

并非每个资源都应该全局共享。COS 通过存储文件时的 origins 选项,让开发者精确控制可见性。

一个重要规则:可见性可以升级,但绝不能降级。如果一个文件已经全局可用,后续尝试使用受限的 origins 列表存储它会被静默忽略。这可以防止恶意行为者重新存储公共资源并缩小其可用性。相反的情况是可能的:最初使用受限 origins 列表存储的文件,以后可以变得更宽松。任何站点,而不仅仅是原始存储者,都可以为相同的哈希(哈希不是秘密)调用 requestFileHandle(),并带有 create: true 和更广泛的 origins 值,并且由于浏览器验证哈希匹配,该资源从那时起对更广泛的受众可用。请注意,升级站点必须通过返回的句柄写入完整的文件。此要求是为了防止站点利用升级路径作为侧信道来检测特定文件是否已存储在 COS 中。

设计上的完整性

COS 的一个微妙但重要的特性是,当你写入文件时,浏览器会验证哈希。如果你写入的数据与声明的哈希不匹配,写入会失败并返回错误。这使得完整性检查自动化:从 COS 读取文件的应用可以确信它得到了预期的确切字节。这与它在网络下载后自己计算哈希所获得的保证相同。

这在 Transformers.js 场景中变得加倍有用。如今,在下载模型权重后,大多数应用没有实际的方法来验证 CDN 是否提供了正确的字节。使用 COS,存储中的每个文件在写入时都会隐式验证,无论它来自哪里——官方的 Hugging Face CDN 还是随机站点的自托管镜像。

不牺牲实用性的隐私

当然,跨 origin 共享缓存会引发与分区 HTTP 缓存相反的问题:如果任何站点可以通过哈希探测文件的存在,攻击者难道不能通过检查某个游戏引擎 Wasm 模块是否被缓存来了解用户的浏览历史吗?

COS 通过两种互补机制解决了这个问题:

至关重要的是,这意味着错误不是确定的答案。它可能意味着“未存储”,也可能意味着“已存储,但浏览器不告诉你”。应用应始终以相同的方式处理:回退到网络。

这对 Transformers.js 示例意味着什么

回到之前的玩具示例:ort-wasm-simd-threaded.asyncify.wasm 运行时大小为 4,733 kB,并且由每个基于 Transformers.js 的应用共享,无论它使用哪个 AI 模型。使用 COS,第一个加载它的应用会下载一次,并使用其 SHA-256 哈希以 origins: '*' 存储。每个后续应用,无论是在 https://googlechrome.github.iohttps://rawcdn.rawgit.net 还是任何其他 origin 上,都会立即在 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";

// 👇 选择加入实验性的 Cross-Origin Storage 缓存后端。
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.enXenova/distilbert-base-uncased-finetuned-sst-2-english(当然还有 ort-wasm-simd-threaded.asyncify.wasm)只需要通过网络传输一次,无论有多少不同的 origin 请求它们。

注意标志上的 experimental_ 前缀。这是有意的,表示底层的浏览器 API 尚未标准化,并且可能在没有主版本号提升的情况下发生变化。

立即尝试

COS API 尚未在任何浏览器中原生实现,但你无需等待即可进行实验。安装 Cross-Origin Storage 扩展 以在所有页面上注入 navigator.crossOriginStorage polyfill,并测试完整流程。你可以查看扩展的源代码并按照使用说明开始使用。

Image 8: Cross-Origin Storage 扩展的 Chrome Web Store 页面。

安装扩展后,你可以立即尝试完整的端到端体验:打开第一个启用了 COS 的玩具示例,让它加载 Xenova/whisper-tiny.en,然后打开来自第二个 origin 的启用了 COS 的玩具示例。与之前看到的 177 MB 重新下载不同,模型在毫秒内从 COS 提供。当你打开扩展的弹出窗口时,你可以看到 COS 的实际运行。如果你选择 按资源查看,你可以看到 SHA-256 哈希为 950978b1dbcbf250335358c1236053ba19a7f7849b33dc777f4421b72b7626fa 的资源在 https://googlechrome.github.iohttps://rawcdn.rawgit.net 之间共享。这可能不明显,但你可以通过比较 Hugging Face 上的 SHA-256 哈希来验证,你正在查看的是 https://huggingface.co/Xenova/whisper-tiny.en/blob/main/onnx/decoder_model_merged.onnx。目前,该扩展主要面向像你这样的高级用户。一旦在浏览器中实现,浏览器设置页面中会有更友好的集成。下面的截图显示了扩展的弹出窗口,其中 按资源查看 标签页处于活动状态,你可以看到共享资源及其哈希,以及在其 COS 缓存中拥有该资源的两个 origin。

Image 9: 在 Cross-Origin Storage 扩展中看到的资源,显示它在两个 origin 之间共享。

行动号召

如果你正在构建自己的 Transformers.js 应用,行动号召很简单:在第一次调用 pipeline() 之前添加 env.experimental_useCrossOriginStorage = true,安装扩展,然后观察重复下载从你的 Network 标签页中消失。每个选择加入的站点都会让其他站点用户的体验更快、更便宜。选择加入完全没有风险:如果由于用户没有安装 COS 扩展而导致 COS API 不受支持,代码只会回退到默认路径(Web Cache API)。

Transformers.js 并不是唯一在实验 COS 的库。WebLLM(选择加入,请参阅文档)和 wllama(自动,请参阅 PR)同样对这个提议的 API 感到兴奋。

在 Chrome 团队中,我们正在考虑在浏览器中原生实现 COS API。作为一个早期阶段的提案,我们欢迎对 API 以及提案本身的反馈。Cross-Origin Storage 仓库是提交问题、表达支持或打开 PR 的地方。

译自 Hugging Face · 官方博客 · 录于 二〇二六年六月二十三日