在 Transformers.js 中实验跨域存储 API
速览
本文介绍了在 Transformers.js 环境中实验提议的跨域存储 API 的过程。该 API 旨在解决 Web 应用中跨域数据访问的安全与便利性问题。通过实际测试,展示了其在特定场景下的应用潜力。
AI 深度解读
在 Transformers.js 中实验跨域存储 API:解决浏览器缓存隔离难题
背景
Transformers.js 为 Web 开发者提供了一套简便的接口,使其能够在 Web 应用中利用 Transformer 模型的力量。通过特定的任务管道(pipelines),开发者可以轻松地在浏览器端运行推理任务。例如,以下代码展示了如何设置一个自动语音识别(ASR)管道:
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/[email protected]';
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 是一个针对常见英语 ASR 任务的极佳选择,也是 Transformers.js 默认模型解析机制中的默认 ASR 模型。当在浏览器中运行此示例时,Transformers.js 会自动处理相关模型资源和 Wasm 文件的下载与缓存。
然而,随着 Web 应用中 AI 模型的普及,一个显著的性能和存储问题浮现出来:缓存隔离导致的重复下载。即使多个不同来源(origin)的应用使用完全相同的模型文件或底层 Wasm 运行时,浏览器的默认缓存机制也会将它们视为不同的资源,导致重复下载和磁盘空间浪费。
核心内容
缓存挑战:模型与运行时资源的重复
1. AI 模型资源的重复下载
以 Xenova/whisper-tiny.en 为例,这是一个非常流行的模型。如果另一个来自不同来源(origin)的 Web 应用也使用了相同的模型,浏览器不会复用第一个应用已缓存的资源。即使两个应用请求的是字节完全相同的文件,浏览器也会重新下载并缓存这 177 MB 的数据。这不仅浪费带宽,还占用宝贵的本地存储空间。
2. WebAssembly (Wasm) 运行时的重复下载
问题不仅限于 AI 模型。考虑一个更复杂的场景:在一个玩具示例中添加情感分析管道。默认情况下,情感分析使用 Xenova/distilbert-base-uncased-finetuned-sst-2-english 模型。
const classifier = await pipeline('sentiment-analysis');
const sentiment = await classifier(result.text);
pre.append('\n\n' + JSON.stringify(sentiment, null, 2));
虽然这两个 AI 模型完全不同,但它们都依赖于同一个底层 ONNX Runtime 库提供的 Wasm 运行时文件:ort-wasm-simd-threaded.asyncify.wasm(大小约为 4,733 kB)。当在不同来源的应用中运行此示例时,浏览器会在 Network 标签页中显示该 Wasm 运行时被再次下载和缓存。
这意味着,即使应用之间没有共享相同的 AI 模型,浏览器仍然会对共享的 Wasm 资源发出冗余请求,并再次缓存它们,从而消耗硬盘空间。
缓存隔离的技术根源
1. 资源来源
- AI 模型资源:默认来自 Hugging Face Hub,最终通过 Hugging Face CDN 提供服务。例如,请求
https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english/resolve/main/config.json会被重定向到最终的 CDN URL。 - Wasm 运行时资源:默认来自 jsDelivr CDN。例如,
ort-wasm-simd-threaded.asyncify.wasm来自https://cdn.jsdelivr.net/npm/onnxruntime-web@...。
2. 为什么相同 URL 无法共享缓存?
尽管不同来源的应用最终可能从相同的 CDN URL 获取资源,但浏览器的缓存机制并非基于 URL 简单共享。为了防止定时攻击(timing attacks)——即网站通过 HTTP 请求响应时间来推断浏览器是否曾访问过同一资源,从而泄露用户隐私——浏览器实施了缓存分区(Cache Partitioning)。
3. Chrome 的具体实现
在 Chrome 中,缓存键(Cache Key)由资源 URL 和网络隔离键(Network Isolation Key, NIK) 共同组成。NIK 由顶级站点(top-level site)和当前帧站点(current-frame site)构成。
假设两个应用分别托管在 https://googlechrome.github.io 和 https://rawcdn.rawgit.net,它们都请求同一个 Wasm 文件:
https://cdn.jsdelivr.net/npm/[email protected]/dist/ort-wasm-simd-threaded.asyncify.wasm
由于它们的 NIK 不同,浏览器会生成两个不同的缓存键。因此,即使资源 URL 完全相同,也不会发生缓存命中(Cache Hit),导致重复下载和存储。这就是跨域存储提案(Cross-Origin Storage proposal) 旨在解决的核心挑战。
跨域存储 API(Cross-Origin Storage API)的引入
跨域存储(COS)API 是一个早期的提案,目前尚未在任何浏览器中原生实现。为了实验该功能,可以使用 Cross-Origin Storage 扩展来注入 navigator.crossOriginStorage polyfill。
该 API 引入了一个专用的 navigator.crossOriginStorage 接口,允许 Web 应用跨越来源边界存储和检索大文件。其核心创新在于:文件不是通过 URL 或来源来标识,而是通过加密哈希(Cryptographic Hash)来标识。
工作原理
- 哈希标识:COS 使用文件的 SHA-256 哈希值作为唯一标识符。
- 跨域识别:无论文件来自哪个来源,只要哈希值相同,COS 就认为它们是同一个文件。
- 流程示例:
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 中,API 返回一个
FileSystemFileHandle,可以通过getFile()直接读取 Blob(生成的File对象继承自Blob)。 - 缓存未命中:如果资源不在 COS 中,应用回退到网络请求,下载资源后将其写入 COS。写入时指定
origins: '*',使得其他无关的、甚至来自完全不同来源的应用也能访问该文件。
这种设计巧妙地绕过了基于来源的缓存隔离限制,实现了基于内容哈希的全局缓存共享。
关键要点
- 浏览器缓存隔离机制:出于安全和隐私考虑(防止定时攻击),现代浏览器(如 Chrome)使用网络隔离键(NIK)对缓存进行分区,导致不同来源的应用即使请求相同的 CDN 资源,也无法共享缓存。
- AI Web 应用的存储浪费:Transformers.js 等库在浏览器中运行 AI 模型时,会导致模型文件和底层 Wasm 运行时被重复下载和存储,造成显著的带宽和磁盘空间浪费。
- 跨域存储 API 的核心创新:通过加密哈希(而非 URL)标识文件,实现了跨来源的文件共享。只要文件内容相同,哈希值就相同,从而实现全局
