压缩

OxPHP 使用 Brotli、Zstandard 或 gzip 压缩 HTTP 响应,具体取决于客户端接受哪一种。它会自动应用于一份固定的内容类型列表(文本、JSON、XML、SVG、WebAssembly 以及若干字体格式),因此无需改动应用代码,传输体积就会下降。

工作原理

客户端的 Accept-Encoding 请求头只读取一次,发生在请求进入路由管线之前,并与服务器提供的编码进行比对。接下来会发生什么,取决于字节来自哪里。

已有存储压缩副本的缓存静态文件会直接用该副本作答。请求期间不做任何压缩,下面的检查也都不适用。参见缓存的静态文件。

其他所有响应(PHP 输出、自定义错误页、尚无存储副本的静态文件)都会在响应构建完成后,按顺序经过以下相同的检查:

  1. 跳过部分响应。 206 永远不会被编码,因为它的 Content-Range 偏移量描述的是未编码的表示形式。
  2. 内容类型检查。 在可压缩类型列表中查找响应的 MIME 类型。
  3. 已编码检查。 已带有 Content-Encoding 响应头的响应保持原样。
  4. 大小检查。 只有介于 256 字节和 3 MiB(3,145,728 字节)之间的响应体才会被压缩,两端均包含在内。响应带有 Content-Length 头时使用该头的值;PHP 响应通常不带,此时改为测量缓冲后的响应体。
  5. 压缩。 以配置的级别应用协商得到的编码。如果压缩后的输出并不比原始内容更小,则改为发送未压缩的响应。
Note

压缩发生在 PHP 执行之后、静态文件服务之后,以及自定义错误页替换之后,因此超过 256 字节的自定义 404 响应体会像其他响应一样被压缩。内置的错误响应体(404 Not Found、500 Internal Server Error)只有几十个字节,低于大小下限。除此之外,状态码从不参与判断。

小的响应体直接内联压缩;超过某个阈值后,这项工作会转移到阻塞线程上,以免拖住异步运行时。阈值为 64 KiB,一旦级别高到成本曲线开始陡升,就降到 4 KiB:Brotli 为质量 4 以上,gzip 为级别 6 以上,Zstandard 为级别 9 以上。在出厂默认值下 Brotli 已处于陡升的那一半,因此超过 4 KiB 的 Brotli 响应体都在异步线程之外压缩。

flowchart TD
    AE["Accept-Encoding,每个请求读取一次"]
    W["按权重排序。服务器<br/>不提供的编码得分为零并被淘汰"]
    Q{"是否提供<br/>存储副本?"}
    A["平局时依次取 br、zstd、gzip。<br/>以 br 11、zstd 19、gzip 9 一次性构建"]
    P["平局时依次取 zstd、br、gzip。<br/>以配置的 zstd 6、br 5、gzip 6<br/>当场压缩"]
    H["Content-Encoding、重新计算的 Content-Length、<br/>Vary、弱化的 ETag、无 Accept-Ranges"]
    AE --> W
    W --> Q
    Q -->|是| A
    Q -->|否| P
    A --> H
    P --> H

选择编码

Accept-Encoding 按权重对编码排序(RFC 9110 §12.5.3),OxPHP 也按这种方式读取它:

  • 编码名称按不区分大小写的方式匹配,未知的编码会被忽略。
  • br;q=0 表示拒绝 Brotli。这与不写该编码不同:* 仍可覆盖被省略的编码,但覆盖不了被明确拒绝的编码。
  • * 以赋予它的权重,覆盖请求头中未明确列出的任何编码。
  • 无法解析的权重会让该编码保持默认权重;格式错误的参数不会让该编码被丢弃。这一点对 q=0 很重要。qvalue 最多有三位小数,因此 q=0.000 表示拒绝该编码,而 q=0.0000 根本不是有效权重,会被当作完全接受。同一编码出现两次时,按第一次的权重读取。
  • 在客户端接受且服务器提供的编码中,权重最高的胜出。被配置撤下的编码在排序前就得分为零,因此如果客户端最偏好的编码服务器不提供,它会退到下一个选择,而不是得到未压缩的响应。
  • 什么都不接受的请求头(为空,或 *;q=0)得到未编码的响应。OxPHP 从不因内容编码而返回 406,也从不把 identity 当作一个标记来读取。

通常情况下权重是相同的,因为浏览器会不带权重地发送它支持的所有编码。OxPHP 根据压缩后字节的去向来打破这种平局:

响应属于 首选编码 原因
由存储副本提供 Brotli,然后 Zstandard,然后 gzip 字节以最高质量压缩一次,此后都从内存提供,因此只有体积重要,而在其范围顶端,Brotli 是三者中最小的
其他所有情况 Zstandard,然后 Brotli,然后 gzip 字节在客户端等待时压缩、用完即弃,因此每个请求都要付出成本。在请求负担得起的级别上,实测 Zstandard 的体积与 Brotli 相差不过几个百分点,CPU 却远不到后者的一半

第一行只在存储副本存在之后才适用。此前的命中,以及所有过大而无法缓存的文件,都走第二行,这就是为什么静态资源通常先以 Zstandard 作答,之后才以 Brotli 作答。

这种偏好只用于打破平局。写了 br;q=0.2, gzip;q=0.9 的客户端在两条路径上都会得到 gzip,为它构建的存储副本也是 gzip 的。

三种都不接受的客户端会收到未编码的响应。实际上 gzip 是兜底选择:过去二十年里的每一个 HTTP 客户端都接受它,而 Zstandard 需要 2024 年或之后发布的浏览器。

明文 HTTP 就是 gzip 站点

浏览器是否启用较新的编码,看的是连接,而不仅仅是自身的新旧:br 和 zstd 在 HTTPS 上会被声明,在明文 HTTP 上则不会。一个不使用 TLS 提供服务的站点,无论访问者用的是什么浏览器,都是 gzip 站点。

Vary: Accept-Encoding

由于结果取决于请求头,压缩窗口内的每个响应都带有 Vary: Accept-Encoding,无论是否压缩,无论是静态还是动态。服务器会为某些客户端编码的表示形式,即使当前客户端拿到的是原始字节,也随该请求头而变化;如果共享缓存得到的信息与此相反,它就会把那些原始字节转交给一个请求了某种编码的客户端。

有两种情况不在这条规则之内:

  • 服务器从未完整持有的动态响应(任何随 PHP 产出而流式发送的内容)不带 Vary:这条路径在任何请求头下都无法压缩,所以响应确实不会变化。大到需要流式发送的静态文件不属于这一类,它会带上该头,因为同一个文件在客户端协商了编码时会被完整读取并压缩。
  • 304 既不带 Vary 也不带 Content-Encoding:它刷新的是客户端已持有的表示形式,而不是描述一个新的表示形式。

Vary 会作为独立的一行头部添加,而不是合并到应用已发送的那一行中,因此设置了 Vary: Cookie 的响应发出时会带有两行 Vary。这是合法的,而且 OxPHP 在追加之前会读取所有这些行,因此 Accept-Encoding 永远不会重复;但只读取第一行的中间层会漏掉它。

缓存的静态文件

放得进内容缓存(1 MiB 或更小)且可压缩(属于列表中的类型,至少 256 字节)的静态文件只压缩一次,而不是每个请求都压缩。

  1. 1
    第一个请求:缓存未命中。 从磁盘读取文件并存入缓存,然后像其他响应一样为该请求压缩。
  2. 2
    下一个请求:触发构建。 文件已被缓存,如果该请求协商出的编码还没有存储副本,服务器会请求在后台线程上构建一份。触发构建的请求按每请求的方式提供,构建期间到达的每个请求也是如此。
  3. 3
    副本就绪。 之后协商出相同编码的请求都由存储副本作答。

这里没有命中计数:触发条件是该编码首次在没有存储副本的情况下命中缓存,因此一个只服务过 gzip 客户端的文件,会在第一个命中它的 Brotli 客户端到来时得到 Brotli 副本。

存储副本以每种编码的最高级别构建(Brotli 11、Zstandard 19、gzip 9),与配置的每请求级别无关,存储副本从不使用后者。它们没有单独的设置:阻止构建某个副本的唯一办法,是彻底撤下该编码。最高质量通常比 Brotli 自身的每请求级别少产生 8% 到 12% 的字节。

每种编码都有自己的存储副本,按需构建,因此一个只服务过 gzip 客户端的文件永远不会付出 Brotli 压缩的成本。它们都共享被缓存文件的验证器,并与缓存的字节一起被丢弃:当条目为保持在 64 MiB 缓存预算之内而被淘汰时(存储副本也计入该预算),或者当 STATIC_REVALIDATE 开启并发现磁盘上的文件已变化时。该设置默认关闭,因此在默认部署中,淘汰和重启是存储副本被丢弃的仅有两种方式。无法压缩的字节会被标记为不可压缩并且不再重试,304 既不会触发构建,也不会提供存储副本。

每个文件的编码会变化一次

由于两种平局规则不同,同时接受 Brotli 和 Zstandard 的客户端通常会看到每个文件的编码变化一次:最初的命中以 Zstandard 作答,为该请求当场压缩;存储副本就绪之后的命中则以 Brotli 作答。两者都是同一资源的有效表示形式,也都带有 Vary,所以不会出问题。它们还共享同一个弱 ETag,因此持有 Zstandard 变体的缓存能成功重新校验并保留它;这次切换影响的是新客户端,而不是已存储副本的客户端。

过大无法缓存的文件

在缓存上限与压缩窗口上限之间(1 MiB 到 3 MiB,框架打包文件、source map 和 WebAssembly 模块就落在这里),只要客户端协商了编码且类型可压缩,文件就会被完整读取并为每个请求压缩。它不会被缓存,也永远不会得到存储副本,因此这是服务器每个请求所做的最昂贵的压缩。

Tip

在 1 到 3 MiB 范围内的资源前面放一个 CDN,或把它们控制在 1 MiB 缓存上限以下,这项工作就只做一次。

读取这样的文件时,它会一直驻留在内存中,直到响应写完,因此同一时间只会读取有限数量的此类文件。超出该限制到达的请求不会等待空位,而是以流式、未编码的方式得到文件。因此在大型资源承受持续负载时,有些客户端拿到的是压缩版本,有些则不是;两种答复对该 URL 来说都正确,而且都带有 Vary: Accept-Encoding。

除了这种情况,超过 1 MiB 的文件都会从磁盘原样流式发送:不可压缩的类型、不接受任何编码的客户端,或任何超过 3 MiB 的文件。

静态文件按大小的成本如下:

flowchart TD
    F["静态文件"]
    C1{"256 B 或更大?"}
    C2{"1 MiB 或更小?"}
    C3{"3 MiB 或更小?"}
    N1["原样发送,并被缓存。<br/>编码帧的开销会超过节省"]
    N2["被缓存,首次缓存命中时<br/>在后台一次性构建存储副本"]
    N3["每个请求都完整读取并压缩。<br/>太大无法缓存,因此不保留副本"]
    N4["从磁盘流式发送,从不压缩。<br/>超出压缩窗口"]
    F --> C1
    C1 -->|否| N1
    C1 -->|是| C2
    C2 -->|是| N2
    C2 -->|否| C3
    C3 -->|是| N3
    C3 -->|否| N4

该路径假定类型可压缩,且客户端协商了编码。去掉其中任一条件,文件在 1 MiB 以内会被缓存,超过则流式发送,范围请求全程有效,任何地方都不做编码。

配置

变量 默认值 描述
COMPRESSION_ENCODINGS br,zstd,gzip 服务器提供哪些编码,以逗号分隔。接受 br(或 brotli)、zstd 和 gzip,不区分大小写,允许前后有空白。这里书写的顺序会被忽略;服务器针对每个响应自行选择(参见选择编码)。将整个值设为 off(或 none)可完全关闭压缩。off 不是列表中的标记,因此 br,off 会被当作错误拒绝
COMPRESSION_BROTLI_LEVEL 5 Brotli 质量(0 到 11)
COMPRESSION_ZSTD_LEVEL 6 Zstandard 级别(0 到 19)
COMPRESSION_GZIP_LEVEL 6 Gzip 级别(0 到 9)
COMPRESSION_LEVEL (未设置) COMPRESSION_BROTLI_LEVEL 的已弃用名称。设置它会在启动时记录一条警告。显式设置的 COMPRESSION_BROTLI_LEVEL 会覆盖它的非零值,但覆盖不了 COMPRESSION_LEVEL=0,后者无论其他设置如何都会关闭全部压缩

一种编码只有在 COMPRESSION_ENCODINGS 中列出且其级别不为 0 时才会被提供;任一条件不满足都会撤下它。被撤下的编码会从整个功能中消失:它永远不会被协商,永远不会在请求路径上被压缩,也永远不会被构建为存储副本。

取值会被校验,而不会被纠正

未知的编码名称、超出范围的级别,或不是数字的级别,都会以一条指明该变量的错误中止启动。不会做任何截断,也不会静默丢弃任何值。空赋值(COMPRESSION_ENCODINGS=,即宿主变量缺失时 ${VAR:-} 产生的形式)被视为未设置,并恢复默认值。它并不表示 off。

从 COMPRESSION_LEVEL 迁移

早期版本只有一种编码和一个旋钮:COMPRESSION_LEVEL,一个默认为 4 的 Brotli 质量,设为 0 时关闭压缩。该变量仍会被读取,两种含义都保留,因此现有部署可以继续工作,但会记录一条弃用警告。

  • COMPRESSION_LEVEL=N(非零)改为 COMPRESSION_BROTLI_LEVEL=N。
  • COMPRESSION_LEVEL=0 改为 COMPRESSION_ENCODINGS=off。

保留旧值同样会让 Zstandard 和 gzip 以默认值开启,这通常正是你想要的:过去不接受 Brotli 的客户端得到的是未压缩响应,现在则得到 gzip。

为什么是这些默认值

  • Brotli 选 5,而不是 4。 Brotli 的质量拐点(哈希器的切换)就在这两者之间。在 4 时,对于 4 KB 以上的 JSON 和真实的压缩后资源,它产生的字节多于 gzip 在其默认级别下的输出,还花了更多 CPU,这样就完全没有理由优先选它而不是 gzip。在 5 时,它在每一个实测的响应体上都比 gzip 小,CPU 大约是 gzip 的两倍。
  • Gzip 选 6。 这是 zlib 自己的默认值,接近收益递减点。级别 9 的成本大约是它的两倍,只换来一两个百分点。
  • Zstandard 选 6,而不是它自己的默认值 3。 在几 KB 以上的响应体上,级别 6 花的时间比早期版本压缩一切所用的 Brotli 质量还少,产生的字节也更少,因此与那些版本发送的内容相比没有任何退步。

级别 9 到 11 更适合离线或构建期压缩,而不是每请求的工作;缓存的静态文件仍然使用它们,因为那份成本只付一次。

可压缩的内容类型

这份列表是封闭的:没有前缀或通配规则,因此不在列表中的类型永远不会被压缩,哪怕它压缩效果很好。在列表内部,匹配是宽松的。参数和前后空白会被去掉,类型本身按不区分大小写的方式比较,因此 text/html; charset=utf-8 和 Application/JSON 都能匹配。

Text 类型:

  • text/html
  • text/css
  • text/plain
  • text/xml
  • text/javascript

Application 类型:

  • application/javascript
  • application/json
  • application/xml
  • application/xhtml+xml
  • application/rss+xml
  • application/atom+xml
  • application/manifest+json
  • application/ld+json
  • application/wasm

其他类型:

  • image/svg+xml
  • font/ttf
  • font/otf
  • application/x-font-ttf
  • application/x-font-opentype
  • application/vnd.ms-fontobject

不会被压缩的情况

当满足以下任一条件时,响应将不经压缩直接发送:

  • 客户端在 Accept-Encoding 请求头中 br、zstd 和 gzip 都不接受,或只以零权重接受它们(br;q=0, zstd;q=0, gzip;q=0),或根本没有发送该请求头
  • 客户端接受的每一种编码都已被 COMPRESSION_ENCODINGS 或 0 级别撤下
  • 响应已带有 Content-Encoding 响应头(例如应用生成的预压缩内容)
  • 响应体小于 256 字节或大于 3 MiB
  • 响应是 206 Partial Content:范围偏移量描述的是未编码的表示形式,因此部分响应体永远不会被编码
  • 内容类型不在上面的列表中。image/png、image/jpeg、font/woff2 和 application/zip 被排除,是因为这些格式本身已带有压缩;text/event-stream 被排除,是因为事件流必须逐个事件到达客户端。text/csv、application/pdf、image/x-icon 以及完全没有 Content-Type 的响应也被排除,因为它们不在列表中
  • 服务器没有完整持有响应体:使用 oxphp_stream_flush() 的 PHP 脚本、Server-Sent Events,以及边从磁盘读取边发送的静态文件。OxPHP 以整体方式压缩响应。Brotli、Zstandard 和 gzip 都能增量编码,因此这是服务器的限制,而不是编码的限制
  • 响应来自在管线运行之前就已作答的插件,例如被限流的 429,它会在压缩步骤之前返回
  • 响应来自内部监听器(INTERNAL_ADDR),它提供的 /metrics 和健康检查端点始终不编码,无论抓取方接受什么

响应头

服务器编码的响应会按如下方式改写,无论是为该请求当场压缩的,还是由存储副本提供的:

响应头 值
Content-Encoding br、zstd 或 gzip,取协商得到的那一种
Content-Length 更新为压缩后的响应体大小
Vary 追加 Accept-Encoding,使 HTTP 缓存为每种编码分别存储版本
ETag 强标签会被弱化为 W/"…"。编码后的字节是另一种表示形式,如果与原始字节共用一个强标签,客户端就可能用原始片段来续传编码后的下载
Accept-Ranges 被移除。字节偏移量对编码后的响应体毫无意义

同一个静态文件的所有编码共享这个弱标签,它由原始大小和修改时间派生而来。区分各个变体靠的是 Vary;标签只决定新鲜度,因此重新校验某个变体的缓存无法告诉服务器它持有的是哪一个。

范围与压缩

对于每一个将会为该客户端压缩的响应,范围处理都会在响应构建之前被关闭。对于同时发送 Range 和 Accept-Encoding 的客户端,压缩窗口内的可压缩静态文件会以完整的 200 作答;来自这类客户端的无法满足的范围请求得到的也是这个 200,而不是 416。不接受任何编码的客户端保留范围功能,可续传的下载正是在这里。

可观测性

指标端点上的两个计数器报告压缩实际做了什么:

  • oxphp_compressed_responses_total:以某种内容编码发送的响应数
  • oxphp_compression_bytes_saved_total:原始大小减去压缩后大小的累计值

两者都会计入存储副本的命中和每请求的压缩,因此拿它们与 oxphp_requests_total 求比值,比单独一次 curl -I 更能可靠地回答"压缩是否作用到了我的流量上"。

疑难排查

响应没有被压缩

确认客户端确实发送了 Accept-Encoding 请求头。浏览器会发送,但某些 HTTP 测试工具默认不发送,而没有该请求头的请求会得到未编码的响应。

零权重表示拒绝该编码:Accept-Encoding: br;q=0, zstd;q=0, gzip;q=0 会为该请求禁用压缩,与不发送该请求头的效果相同。

使用 curl 检查:

bash
curl -H "Accept-Encoding: br, zstd, gzip" -I http://localhost/

在响应头中查找 Content-Encoding。如果缺失,请检查:

  1. COMPRESSION_ENCODINGS 仍然列出了你请求的编码,且其级别不为 0
  2. 响应体至少有 256 字节,且不超过 3 MiB
  3. 响应的 Content-Type 在上面的可压缩列表中。匹配时忽略大小写和参数
  4. 处理程序没有流式输出。调用 oxphp_stream_flush()、发送 Server-Sent Events,或以其他方式在长度确定之前就开始写出的脚本会被原样透传,在会 flush 的应用中这是最常见的原因
不同客户端得到不同的编码

协商本来就会产生这种结果,而且几乎所有情况都可以归入三种。浏览器在明文 HTTP 上不声明 br 和 zstd:它们在那里发送 Accept-Encoding: gzip, deflate,无论多新都以 gzip 作答。命令行工具往往根本不发送 Accept-Encoding,因此以未编码方式作答。而在 HTTPS 上,新到会发送 zstd 的浏览器在动态响应上得到 Zstandard,较旧的浏览器则得到 Brotli。这些情况都不是配置错误,而且被压缩的响应都带有 Vary: Accept-Encoding,缓存会将它们区分开。

同一个静态文件的编码发生了变化

这是预期行为:对可缓存静态文件的最初几次命中会用 Zstandard 压缩以回应该请求,而一旦存储的 Brotli 副本在后台构建完成,之后的命中就由它作答。参见缓存的静态文件。

范围请求返回了整个文件

当客户端同时发送 Accept-Encoding 时,对于压缩窗口内的可压缩文件,这是预期行为:提供给它的表示形式是编码过的,原始字节的偏移量无法描述它。不带 Accept-Encoding 的同一请求会得到它的 206。参见响应头。

压缩反而让响应变大了

无法压缩的响应体(base64 数据块、由随机标识符组成的载荷、被包装成 image/svg+xml 的已压缩图像)经过三种编码中的任何一种之后,都可能不比原来小。OxPHP 会比较两者,只要压缩没有带来好处就发送原始内容,因此无需任何配置。这不可能是小响应的问题:256 字节是尝试压缩的下限。

开启压缩后吞吐量下降了

压缩既可能提高也可能降低吞吐量。往哪个方向走,取决于开启压缩之前服务器缺的是什么;同一台服务器上的同一个设置,可能让吞吐量朝任一方向变化好几倍。

面对饱和的链路,它是能拿到的最大收益:缩小到八分之一的文本响应,送达速度就快八倍,而它花掉的 CPU 原本就在空闲地等待线路。

面对狭窄的异步运行时,它是一种成本。低于移交阈值的响应体在异步工作线程上压缩,而运行时默认只占用进程可用 CPU 数的一半,因此这项工作是从承载所有打开连接的线程池中拿走的,而机器的其余部分可能还有余力。它并不需要多少就会占满:两个运行时线程光是压缩 1.5 KB 的 JSON 响应体就会饱和。识别它的症状是:吞吐量下降,同时机器和 PHP 工作进程池都变得更闲,因为用尽的并不是它们。

需要检查的是: 运行时是否比机器窄。TOKIO_WORKERS 默认为可用核心数的一半,调高它能让这项工作用上机器的更多部分。能多用多少取决于机器以及上面还运行着什么,所以要实测而不是假设,并且在实测的同时观察工作进程池:它的线程与运行时的线程是分开的,因此在已经受 PHP CPU 限制的部署上,更宽的运行时是从应用那里抢走周期,而不是找到新的周期。

改为把小响应体送到阻塞线程池并不能解决这个问题。在双线程运行时上,一个内联压缩的 1.5 KB JSON 响应体大约占用运行时线程 7 µs;交给阻塞线程池再取回来,则会占用 13 到 19 µs,因为提交工作、挂起和唤醒本身就是运行时的工作。这个运行时上的吞吐量会降到原来的三分之一到二分之一,运行时上其他所有工作的等待时间不降反升。大小阈值正是考虑到这一成本而设定的。

压缩导致 CPU 占用过高

在每种编码范围的顶端(Brotli 8 到 11、gzip 8 和 9、Zstandard 12 到 19),压缩成本都会急剧上升,而每请求的默认值远低于这一区间。如果你观察到压缩带来的高 CPU 消耗:

修复方法: 将 COMPRESSION_ZSTD_LEVEL 降到 3,将 COMPRESSION_GZIP_LEVEL 降到 4。Brotli 是三者中最昂贵的编码,但把 COMPRESSION_BROTLI_LEVEL 降到 4 并不是节省这部分 CPU 的办法:在 4 时,它产生的字节比 gzip 在其默认级别下还多,成本却仍然高于 gzip。应当改为从 COMPRESSION_ENCODINGS 中去掉 br。新到会发送 zstd 的客户端会退到 Zstandard 而不是 gzip,因此这一改动对它们毫无损失;只有接受 Brotli 但不接受 Zstandard 的客户端(Safari、2024 年之前的浏览器、大多数中间层)会转向 gzip,而在真实响应体上 gzip 大 2% 到 6%,CPU 约为一半。

缓存的静态文件不在这部分成本之内:无论剩下哪些编码,它们的压缩副本都只在后台构建一次。不过 1 MiB 到 3 MiB 之间的文件在其中。它们每个请求都要压缩,而且无法缓存;参见过大无法缓存的文件。

预压缩的资源被再次压缩

OxPHP 不会在原始文件旁查找 .br、.zst 或 .gz 文件:对 /app.js 的请求永远不会由 /app.js.br 作答,而直接请求压缩文件得到的是一个浏览器不会解码的不透明下载。静态资源的构建期压缩没有可接入的地方;服务器自己的存储副本承担了这一角色,质量同样是最高级别,而且不需要构建步骤。

跳过重复压缩只适用于已带有 Content-Encoding 响应头的响应,实际上就是指自行编码响应体并自行设置该头的 PHP 脚本。

Docker 示例

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "8080:80" volumes: - ./src:/var/www/html environment: - DOCUMENT_ROOT=/var/www/html/public - ENTRY_FILE=index.php - COMPRESSION_ENCODINGS=br,zstd,gzip - COMPRESSION_BROTLI_LEVEL=5 - COMPRESSION_ZSTD_LEVEL=6 - COMPRESSION_GZIP_LEVEL=6

给你的智能体的提示词

这些提示词假定你的智能体已连接 OxPHP 文档 MCP 服务器——需要哪些页面它会自己去读。

prompt
弄清楚 OxPHP 对我的应用到底会压缩哪些东西,并调好各个编码。通过 OxPHP 文档 MCP 服务器读取 `features/compression` 页面。 过一遍这个仓库返回的内容类型和响应大小,告诉我其中哪些落在可压缩类型列表或 256 B 到 3 MiB 的范围之外,标出介于 1 MiB 和 3 MiB 之间、每个请求都要重新压缩的静态资源,并为它承载的流量结构推荐 `COMPRESSION_ENCODINGS` 和各编码的级别。如果配置里仍在设置已弃用的 `COMPRESSION_LEVEL`,把它迁移掉。

另请参阅

  • 静态文件 — 文件服务、MIME 检测与 HTTP 缓存
  • 指标 — 压缩计数器以及该端点的其余内容
  • 配置参考 — 完整的环境变量列表
发现错误?报告问题 →