浏览器 HTTP 压缩传输新方法: 压缩字典传输
压缩字典传输是一种通过共享压缩字典来大幅减少HTTP响应传输大小的方法。
概述
HTTP中使用压缩算法来减少通过网络下载的资源大小,从而降低带宽成本并缩短页面加载时间。无损HTTP压缩算法通过查找源文件中的冗余内容实现压缩,例如重复出现的字符串“function”
。它们仅保留冗余字符串的一个副本,并将资源中该字符串的出现位置替换为对该副本的引用。由于引用比字符串本身更短,因此压缩后的版本更小。
注意:此技术的前身称为 SDCH(HTTP 共享字典压缩),但从未得到广泛支持,并在 2017 年被移除。压缩字典传输是一种规范更完善、更健壮的实现,且获得更广泛的行业共识。
例如,考虑以下 JavaScript 代码:
function a() {
console.log(“Hello World!”);
}
function b() {
console.log(“I am here”);
}
这可以通过用对先前位置和字符数量的引用替换重复字符串来压缩,例如:
function a() {
console.log(“Hello World!”);
}
[0:9]b[10:20]I am here[42:46]
在此示例中,[0:9]
表示复制从第0个字符开始的9个字符。需注意这是一个简化示例,用于演示概念,实际算法比这更复杂。
客户端可在下载后通过解压恢复原始未压缩资源。
压缩字典
像Brotli压缩和 Zstandard压缩通过允许使用常见字符串的字典,实现了更高的效率,因此您无需在压缩资源中保留这些字符串的副本。这些算法默认附带一个预定义的字典,用于压缩HTTP响应。
压缩字典传输在此基础上进一步扩展,允许您提供专用于特定资源集的自定义字典。压缩算法在压缩和解压缩资源时可将其作为字节来源进行引用。
假设前例中的引用已包含在该通用字典中,则可进一步简化为:
[d0:9]a[d10:20] Hello World![d42:46]
[d0:9]b[d10:20]I am here[d42:46]
该字典可以是仅用于压缩字典传输的独立资源,也可以是网站本身需要的资源。
例如,假设您的网站使用了一个 JavaScript 库。你通常会加载该库的特定版本,并在库名称中包含版本号,例如<script src="my-library.v1.js">
。当浏览器加载你的页面时,它会将库作为子资源进行下载。
如果你随后更新到库的v2版本,该库的大部分代码可能保持不变。因此,网站可以通过告知浏览器将 my-library.v1.js
用作 my-library.v2.js
的压缩字典,从而大幅减少 my-library.v2.js
的下载大小。这样,v1 和 v2 之间相同的字符串无需包含在 v2 的下载中,因为浏览器已经拥有它们。my-library.v2.js
的下载大小大部分只是两个版本之间的差异。
压缩字典传输可以实现比使用默认内置字典压缩高出一个数量级的压缩效果:请参阅 压缩字典传输示例 以获取一些实际结果。
字典格式
压缩字典不遵循任何特定格式,也没有特定的MIME类型。它们是普通文件,可用于压缩内容相似的其他文件。
文件的旧版本通常包含大量相似内容,因此非常适合用作字典。将文件的旧版本用作字典,可使压缩算法高效地引用所有未更改的内容,并仅捕获新版本中相对较小的差异。这种方法被称为增量压缩。
另一种方法是将常见字符串(例如您的HTML模板)集中列在新的dictionary.txt
文件中,以便用于压缩网站上的HTML页面。您可以通过使用专业工具进一步优化,例如Brotli的字典生成器,该工具可将字典压缩至最小尺寸并最大限度减少重叠。
字典还可以用于有效压缩二进制格式。例如,WASM 二进制文件是大型资源,也可以从增量压缩中受益。
现有资源作为字典
要将资源用作字典,服务器应在提供该资源的响应中包含 Use-As-Dictionary
标头:
Use-As-Dictionary: match="/js/app.*.js"
该标头的值指定可将此资源用作字典的资源:在本例中,包括任何 URL 与给定 模式 匹配的资源。
当后续请求的资源与给定模式匹配(例如 app.v2.js
)时,请求将包含可用字典的 SHA-256 哈希值,该值位于 Available-Dictionary
标头中包含可用字典的 SHA-256 哈希值,同时在 Accept-Encoding
标头中包含 dcb
和/或 dcz
值(用于使用 Brotli 或 ZStandard 进行增量压缩):
Accept-Encoding: gzip, br, zstd, dcb, dcz
Available-Dictionary: :pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=:
服务器随后可返回一个适当编码的响应,并在 Content-Encoding
头中指定所选内容编码:
Content-Encoding: dcb
如果响应可缓存,则必须包含一个 Vary
头部,以防止缓存向不支持字典压缩的客户端提供字典压缩资源,或以错误的字典压缩响应:
Vary: accept-encoding, available-dictionary
在 Use-As-Dictionary
标头中还可以可选地提供一个 id
,以便服务器在不按哈希存储字典文件时更容易找到字典文件:
Use-As-Dictionary: match="/js/app.*.js", id="dictionary-12345"
如果提供此值,未来请求中将通过 Dictionary-ID
标头发送该值:
接受编码:gzip, br, zstd, dcb, dcz
可用字典::pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=:
字典 ID:“dictionary-12345”
服务器仍需验证 Available-Dictionary
标头中的哈希值——Dictionary-ID
是服务器识别字典的额外信息,但不能替代 Available-Dictionary
标头的要求。
独立字典
HTML 文档还可以通过 <script>
标签等元素向浏览器提供压缩字典,而该字典并非浏览器通过下载获取的资源。实现此功能有两种方法:
- 包含一个
<link>
元素,其rel
属性设置为compression-dictionary
:<link rel="compression-dictionary" href="/dictionary.dat" />
- 使用
Link
标头引用字典:链接:</dictionary.dat>; rel="compression-dictionary"
该字典随后在浏览器空闲时被下载,且响应必须包含Use-As-Dictionary
标头:
Use-As-Dictionary: match="/js/app.*.js"
从这里开始,当请求匹配的资源时,过程与前一个示例类似。
创建字典压缩响应
字典压缩响应可使用 Brotli 或 ZStandard 算法,但需满足两个额外要求:必须包含魔术头部和嵌入式字典哈希。
字典压缩资源可以动态创建,但对于静态资源,最好在构建时提前创建。当使用先前版本作为字典时,这需要决定创建多少个增量压缩版本——仅最后一个版本,还是最后X个版本(X为某个值)。
假设有一个名为 dictionary.text
的字典文件和一个名为 data.text
的待压缩文件,以下 Bash 命令将使用 Brotli 算法压缩该文件,生成名为 data.txt.dcb
的压缩文件:
echo -en ‘\xffDCB’ > data.txt.dcb && \
openssl dgst -sha256 -binary dictionary.txt >> data.txt.dcb && \
brotli --stdout -D dictionary.txt data.txt >> data.txt.dcb
使用相同的输入文件,以下 Bash 命令将使用 ZStandard 压缩文件,生成一个名为 data.txt.dcz
的压缩文件:
echo -en ‘\x5e\x2a\x4d\x18\x20\x00\x00\x00’ > data.txt.dcz && \
openssl dgst -sha256 -binary dictionary.txt >> data.txt.dcz && \
zstd -D dictionary.txt -f -o tmp.zstd data.txt && \
cat tmp.zstd >> data.txt.dcz
注意:您需要在本地安装 OpenSSL 以及 Brotli 或 ZStandard。
限制
压缩算法存在安全攻击风险,因此压缩字典传输存在以下限制:
- 字典必须与使用该字典的资源具有相同来源。
- 使用字典压缩的资源必须与文档源同源,或遵循CORS规则,并通过
crossorigin
属性请求,并通过适当的Access-Control-Allow-Origin
标头提供。 - 字典受常规 HTTP 缓存分区规则约束,因此即使不同源下载相同资源,字典也无法共享。每个源都需要重新下载字典。
此外,字典本身可能成为跟踪载体,因此当禁用 cookie 或启用其他额外隐私保护措施时,浏览器可能会限制此功能。
规范
规范 |
---|
压缩字典传输 |
这似乎是为了有限的收益增加了太多复杂性。有没有这样的情况,即在最高压缩级别下,gzip和br的压缩效果不够好?
每个被压缩的信息或文件都会附带一个字典。例如,对于许多HTML或CSS文件来说,这个字典数据很可能几乎完全冗余。
由于 zstd 已经很好地处理了独立的压缩字典,因此几乎没有增加复杂性。
标准压缩格式并不直接包含字典。在解压缩过程中,解压缩后的数据本身成为字典。这使得任何模式的首次出现压缩效率较低(但通常仍能通过熵编码实现压缩),而后续重复则变得成本较低。
Brotli 默认包含部分 HTML 和脚本的字典。该字典内置于解压器中,不会随文件发送。
解压字典并非魔法。它们本质上是解压文件的前缀,以便首次出现某些模式时可从字典中引用而非重新构建。这仅对文件开头附近数据的首次出现有效,而对于后续重复部分,字典便不再相关。
字典也需要下载,且你不会拥有完整字典链,因此无论文件是“字典+使用字典的文件”还是仅包含完整文件本身,解压时都需承担无字典状态下的解压成本。
> 字典也需要下载
这就是为什么建议使用同一文件的旧版本,你已经从之前访问该网站时缓存了该版本。你需要承担没有字典的解压成本,但仅限于首次访问。基本上,这是为了恢复频繁更改但每次仅更改一点的文件的缓存优势。
当然,Brotli的默认(内置)词典因包含“神圣罗马帝国皇帝”、“美利坚联盟国”、“多米尼加共和国”等字符串而臭名昭著,这与它的创建方式有关。可通过以下链接查看完整字典:https://gist.github.com/duskwuff/8a75e1b5e5a06d768336c8c7c37…。
使用实际待压缩内容创建的字典将产生截然不同的字典。
> 词典也需要下载,而且你不会一直有词典可用
我们已经有一种方法来管理这一点:为各种媒体类型标准化和版本化词典(也带有校验和),然后只需本地缓存它们,因为它们的设计应该是不可变的。
为防止词典因微小差异而过度增长,我们可以要求每个词典都符合RFC规范。
一些示例:https://github.com/WICG/compression-dictionary-transport/blo…
使用字典相较于未压缩的文件显示出显著的性能提升。
似乎网站并非在减少冗余,而是将冗余转移到你的硬盘上。一些示例中提到的 1MB 字典看似不大,但如果所有人都这样做,累积起来可能会相当可观。
这充分说明了这种方法的无用性。它只能在极度冗余的网站上节省几千字节,而这些网站本身浪费了数兆字节的数据。
例如,以CNN的例子来说:
> 使用旧版本作为新版本的字典时,JavaScript的大小比仅使用Brotli压缩时小98%。具体来说,278KB的JavaScript在仅使用Brotli压缩时为90KB,而使用Brotli和旧版本作为字典时仅为2KB。
哇!节省了 98%!太棒了!但从绝对值来看,90 KB 和 2 KB 的差异仅为 88 KB。而 cnn.com 在首次加载页面时会下载 63.7 MB 的数据。因此,实际节省的 88 KB 仅占总数据量的不到 0.14%,可以忽略不计。
你为什么认为如果将此应用于 63.7 MB 的 JavaScript 而不是单个文件,它就会停止工作?
如果你在发布一个 JavaScript 包,例如,它有小而频繁的更新,这应该是一个很好的用例。这里有一个配套的测试网站,看起来对估算很有趣:https://use-as-dictionary.com/generate/
在某些应用中,没有“足够好”这一说,即使是微小的改进也可能在大型系统中产生显著影响。这就像美国航空公司通过从沙拉中去除一颗橄榄每年节省$40,000的软件版本。
若您對此感興趣,我強烈推薦觀看Pat Meenan的這場演講。
https://www.youtube.com/watch?v=Gt0H2DxdAPY
这是一个有趣的想法。我好奇是否可以在这里实现隐写术。也就是说,通过使用不同的字典但保持相同的压缩规则集来修改消息。
允许修改消息显然意味着恶意软件等风险成为可能。
https://en.wikipedia.org/wiki/Steganography
为什么浏览器/服务器不能直接存储一个标准的英语词典,并通过索引进行通信?不在词典中的内容可以直接发送。我一直有这个想法,但不明白为什么没有实现。对于其他语言可能稍显复杂,但基本原理相同。
进一步思考,我们目前是在字符层面进行处理——使用Unicode表,那么为何不能直接查找单词甚至常见短语?
压缩受鸽巢原理限制。压缩并非免费获得。
π中包含所有可能的文本,但平均而言,编码文本位置的成本与文本本身相当或更高。
要实现压缩,你只能通过调整成本来实现,即让某些内容使用更少的位数来表示,但代价是其他内容需要使用更多的位数来消除歧义(例如,所有字节原本需要8位,你可以让特定字节使用1位,但其他所有字节都需要9位)。
要能够引用英语词典中的单词,你必须在压缩流中为它们分配一些位序列。
如果你使用最佳且最短的序列,你正在浪费它们来从一个僵化的固定词典中选择,而不是以某种更复杂的方式表示数据,这种方式在实际应用中更常用(解码器已经通过构建自适应词典和其他动态技术来实现这一点)。
如果你试图避免影响正常压缩,并用较不重要的较长位序列来表示词典中的单词,这些序列很可能比单词本身更长。
像Brotli这样的压缩算法已经这样做了:
https://www.rfc-editor.org/rfc/rfc7932#page-28
Brotli内置了字典。
这似乎会导致服务器负载大幅增加。
此前服务器会缓存静态资源的压缩版本。
而现在服务器要么需要实时压缩,要么必须维护一个庞大的缓存,不仅包含你最近的静态 JavaScript 文件,还包括所有过去的文件及其使用不同字典组合压缩的版本。
这可能使静态 HTML/CSS/JS 的服务所需资源轻松增加 10 倍。
您仍需像往常一样生成独立的压缩版本(或多个版本),同时使用多个字典生成压缩版本。
服务器在请求时确实需要做更多工作,但这并非实质性增加工作量——只需检查请求路径是否存在与客户端提供的字典哈希值匹配的压缩版本。
过去的版本存储在客户端的正是这些字典。服务器端只需保留与最近五个版本的差异(如果存储空间有限),或保留能覆盖大部分返回客户端的版本,然后在发布新版本时重建。
Cloudflare等服务似乎能很好地利用这一机制。
分析其平台上网站的常见响应,从这些数据构建高效字典,然后自动注入指向该网站专用字典的链接,使未来响应实现最佳压缩并节省带宽。整个过程对客户和终端用户完全透明。
按 URL 分配的字典(即每个 URL 对应一个独立字典)非常有用,因为它们允许对资源进行增量更新,且旧版本资源可作为最佳模板,且已存在时无需额外成本。
然而,我对多页面共享词典(即为某个网站或页面组构建一个词典)的实用性持怀疑态度。这是一种冒险,可能适得其反。
额外的词典需要下载,因此一开始就是额外的开销。它不仅要能匹配某些内容,还必须比常规(按页面)压缩更有效,才能比不使用更好,并且必须足够有用,以在开始产生净收益之前就收回自身成本。这基本上意味着字典中的所有内容都必须对用户有用,并且必须被使用多次,否则它只是一个不必要的初始延迟。
标准(按页)压缩已经非常擅长去除简单的重复模式,而Brotli甚至内置了一个默认的随机HTML样式片段字典。这进一步缩小了共享字典的适用范围,因为通用页面内容已经足以成为优势。它们需要包含更具体的内容才能超越标准压缩,但字典越具体,就越不可能与用户浏览的内容匹配。
看到访问控制失误时,训练数据中包含其他用户的随机数据,令人兴奋
在标题名称已使用冒号的情况下,使用冒号作为起始和结束分隔符显得非常奇怪。难道逗号或分号不是更合适吗?
这是根据规范编码的,规范规定标题中的二进制数据应由冒号包围:https://www.rfc-editor.org/rfc/rfc8941.html#name-byte-sequen…
哦,谢谢,我以为这是类似哈希或Base64编码的数据字符串,而不是二进制数据。我之前从未见过在头部使用此类二进制数据的用例。
这对于客户端具有频繁通信且长期连接的API来说非常有趣。例如,我正在考虑GitHub API。
那个
Link:
头部让我一时摸不着头脑。毫无疑问,有人会想出如何将此滥用为另一种cookie/跟踪技术。