2026-08-24 16:05:52.845346+08 / restful-api

Performance

介绍 RESTful API 中压缩、部分响应、ETag、分页、Prefer 和 HTTP 缓存等性能优化实践。

Restful ApiApi DesignOpenapi

Performance

性能是 RESTful API 设计的重要方面,尤其是对于返回大载荷或运行在高流量环境中的 API。

API 设计应尽量减少:

  • 网络带宽使用
  • 响应延迟
  • 不必要的数据传输
  • 对未变化资源的重复获取
  • 不必要的服务端处理

常见技术包括:

  • 响应压缩
  • 部分响应和字段过滤
  • 使用 ETag 的条件请求
  • 合理使用 HTTP 缓存
  • 分页
  • 最小化响应
  • 在适当场景进行异步处理

减少带宽使用和延迟

当预期载荷大小或请求量使优化有明显收益时,API 应提供减少带宽使用和延迟的机制。

这对以下场景尤其重要:

  • 大型资源集合
  • 大型资源表示
  • 高流量 API
  • 移动客户端
  • 运行在慢速或高延迟网络中的客户端

常见优化方式包括:

  1. 在适当情况下压缩请求和响应载荷。
  2. 允许客户端只请求需要的资源字段。
  3. 使用 ETag 和条件请求,避免传输未变化的资源。
  4. 当客户端可以接受精简响应或异步处理时使用 Prefer Header。
  5. 对大型集合使用分页。
  6. 缓存稳定或很少修改的资源。

示例:

http
GET /users/123 HTTP/1.1
Accept: application/json
Accept-Encoding: gzip

服务端可以返回:

http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip

性能优化应根据 API 的实际特征选择,不能盲目套用。例如,非常小的响应从压缩中获得的收益可能很小,但仍会消耗 CPU 资源。


使用响应压缩

当响应足够大、压缩可以显著减少传输数据时,API 应支持载荷压缩。

gzip 是常见且广泛支持的 HTTP 压缩格式。客户端通过 Accept-Encoding 请求 Header 声明支持的压缩算法:

http
GET /users HTTP/1.1
Host: api.example.org
Accept-Encoding: gzip

如果服务端使用 gzip,应通过 Content-Encoding 响应 Header 声明:

http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip

服务端仍应支持未压缩响应。对于没有发送 Accept-Encoding: gzip 的客户端,应能够返回普通表示。

以下情况可能不适合压缩:

  • 响应非常小
  • 载荷已经压缩
  • 压缩的 CPU 成本过高
  • 压缩引入的延迟超过节省的网络传输时间

因此,压缩行为应基于载荷特征和预期流量决定。


适当支持部分响应

对于字段很多或结构深度嵌套的资源,API 应考虑允许客户端只请求实际需要的字段。

这样可以显著减少:

  • 响应大小
  • 网络带宽
  • JSON 序列化成本
  • 客户端解析成本

可以使用 fields 查询参数实现这一点。

完整响应示例:

json
{
  "id": "123",
  "name": "John Doe",
  "address": "1600 Pennsylvania Avenue Northwest, Washington, DC, United States",
  "birthday": "1984-09-13",
  "friends": [
    {
      "id": "1",
      "name": "Jane Doe"
    }
  ]
}

如果客户端只需要用户姓名和朋友姓名,可以请求:

http
GET /users/123?fields=(name,friends(name)) HTTP/1.1

返回:

json
{
  "name": "John Doe",
  "friends": [
    {
      "name": "Jane Doe"
    }
  ]
}

fields 参数决定响应中返回哪些属性。例如,fields=(name) 只返回 name;fields=(name,friends(name)) 返回顶层 name 和 friends 集合中的 name。

字段选择语法

一种可行的 fields 语法如下:

text
<fields>            ::= [ <negation> ] <fields_struct>
<fields_struct>     ::= "(" <field_items> ")"
<field_items>       ::= <field> [ "," <field_items> ]
<field>             ::= <field_name> | <fields_substruct>
<fields_substruct>  ::= <field_name> <fields_struct>
<field_name>        ::= <dash_letter_digit> [ <field_name> ]
<negation>          ::= "!"

示例:

text
fields=(name)
fields=(name,email)
fields=(name,friends(name))
fields=(id,name,address)

如果支持排除语义,必须清晰记录其规则。

不要定义令人意外的默认字段过滤

fields 查询参数不应有隐藏的或令人意外的默认值。没有显式提供 fields 参数时,客户端通常应收到文档中定义的标准资源表示,而不是被静默省略部分属性。

这符合最小意外原则:客户端不应意外收到不完整的资源表示。

记录部分响应行为

OpenAPI 无法完整表达由动态字段选择产生的所有响应结构。因此,支持字段过滤的 API 应清晰记录 fields 参数的行为,包括嵌套字段表达式。

完整响应 Schema 仍应描述完整的资源表示。


使用条件请求避免不必要的传输

API 可以使用实体标签标识资源的特定版本。

服务端可以返回 ETag:

http
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "7e2f31a"

客户端可以保存该 ETag,并在后续请求中使用:

http
GET /users/123 HTTP/1.1
If-None-Match: "7e2f31a"

如果资源没有变化,服务端可以返回:

http
HTTP/1.1 304 Not Modified
ETag: "7e2f31a"

此时不需要再次传输资源表示。这对大型资源、频繁请求的资源、参考数据、主数据以及很少变化的资源尤其有用。


ETag 与 If-Match

ETag 也可以与 If-Match 一起用于条件更新。

如果客户端之前获取到 ETag: "version-42",后续可以发送:

http
PUT /users/123 HTTP/1.1
Content-Type: application/json
If-Match: "version-42"

只有当当前资源仍然匹配所提供的实体标签时,服务端才应执行操作。这可以防止客户端意外覆盖其他客户端已经做出的修改。

缓存校验通常使用 If-None-Match;条件修改通常使用 If-Match。


对大型集合使用分页

大型集合通常不应作为单个无界响应返回,而应支持分页。

例如:

http
GET /users?limit=100
GET /users?page=2&page-size=100
GET /users?limit=100&cursor=eyJpZCI6IjEyMyJ9

分页可以限制:

  • 响应大小
  • 内存使用
  • 序列化成本
  • 网络带宽
  • 客户端处理成本

同一个 API 中应保持分页机制的一致性。


适当使用 Prefer Header

HTTP Prefer Header 可以用来表达客户端的处理偏好。

两个常见示例是:

http
Prefer: return=minimal
Prefer: respond-async

当客户端不需要操作完成后的完整资源表示时,可以使用 Prefer: return=minimal,让服务端避免生成和传输不必要的响应表示。

对于可能需要较长处理时间的操作,可以使用 Prefer: respond-async,让服务端在支持该行为时异步处理请求。

API 必须记录所支持偏好的确切行为。


HTTP 缓存

HTTP 缓存可以显著提升性能,但会带来与以下因素相关的复杂性:

  • 身份认证
  • 数据一致性
  • 缓存失效
  • 资源更新
  • 多用户场景
  • 共享缓存与私有缓存
  • 缓存生命周期

因此,应有意识地引入缓存。对于没有明确缓存需求的 API,建议采用保守默认值:

http
Cache-Control: no-cache, no-store, must-revalidate, max-age=0

特别是 Cache-Control: no-store 会阻止客户端和中间代理存储响应,适合作为默认策略。


记录可缓存端点

如果 API 明确支持缓存,必须记录其缓存行为。这适用于有意启用缓存的 GET、HEAD 和 POST 操作。

文档应说明 Cache-Control、Vary 和 ETag 等相关 Header。当 Cache-Control 已经提供所需的缓存生命周期策略时,应避免同时使用 Expires 和 Cache-Control 定义重叠语义。


Cache-Control

Cache-Control 定义客户端和中间缓存如何存储及复用响应。

http
Cache-Control: private, must-revalidate, max-age=300

含义是:

  • private:响应只面向单个用户,共享缓存不得存储。
  • must-revalidate:响应过期后,必须重新校验才能复用。
  • max-age=300:响应在 300 秒内可以被视为新鲜。

对于认证用户专属 API,在明确启用缓存时,private 通常比 public 更安全。


Vary

Vary 响应 Header 用于标识会影响响应表示选择的请求 Header。

例如:

http
Vary: Accept, Accept-Encoding

这表示缓存必须考虑 Accept 和 Accept-Encoding,因为同一个 URI 可能根据客户端是否支持 gzip 返回不同表示。

如果语言也会影响响应,还可以使用:

http
Vary: Accept, Accept-Encoding, Accept-Language

具体取值取决于端点的实际特征。


OpenAPI 中记录缓存 Header

缓存相关响应 Header 可以作为可复用的 OpenAPI 组件进行记录,包括 Cache-Control、Vary 和 ETag,然后从各个操作中引用这些组件。


缓存校验

客户端应能够高效判断缓存的资源表示是否仍然有效。

http
GET /users/123 HTTP/1.1
If-None-Match: "7e2f31a"

如果资源未变化,返回 304 Not Modified,并且响应不得包含完整资源表示。如果资源已变化,则返回 200 OK 和新的 ETag,让客户端更新本地缓存。


使用 HEAD 进行缓存校验

对于大型资源,API 可以支持 HEAD 请求,让客户端在不下载完整表示的情况下检查元数据。

http
HEAD /large-resources/123 HTTP/1.1

客户端可以将返回的实体标签与本地缓存版本比较。使用 If-None-Match 的条件 GET 也是常用机制。


根据数据大小选择缓存策略

小型数据集

小型数据集可以提供完整集合端点,例如 GET /countries,并为完整集合关联 ETag。客户端之后可以使用 If-None-Match 检查是否发生变化。

中型数据集

中型集合可以将缓存与分页结合,例如:

http
GET /products?limit=100&cursor=abc123

API 可以为分页或集合版本提供实体标签,避免客户端重复下载未变化集合,同时避免响应过大。

大型资源

对于单个大型资源,可以考虑支持 HEAD、ETag、If-None-Match 和条件 GET,让客户端判断是否需要下载完整表示。


适当优先使用服务端或网关缓存

通用 HTTP 客户端和代理缓存可能较为复杂,因为缓存必须正确理解 Cache-Control、Vary、ETag、身份认证、用户专属表示和内容协商。

对于很多系统,在应用、服务或 API Gateway 层直接缓存更容易控制。具体实现取决于基础设施,但对客户端暴露的 HTTP 语义仍应清晰定义。


缓存失效

缓存设计不能脱离资源更新单独考虑。启用缓存前应明确:

  1. 缓存什么?
  2. 可以缓存多长时间?
  3. 底层资源变化时会发生什么?
  4. 如何检测过期数据?
  5. 如何使过期数据失效?
  6. 缓存表示是否与用户相关?
  7. 是否可以安全地在多个客户端之间共享?

例如,GET /countries 适合较长时间缓存,因为国家参考数据很少变化;而 GET /accounts/123/balance 可能需要很短的缓存时间甚至完全不缓存,因为余额变化频繁且可能是用户专属数据。


性能优化总结

REST API 性能优化首先应避免不必要的工作和数据传输。

技术 主要收益
压缩 减少传输字节数
部分响应 避免返回未使用字段
分页 限制集合响应大小
ETag 标识资源版本
If-None-Match 避免重新下载未变化资源
If-Match 支持安全的条件修改
Cache-Control 定义缓存策略
Vary 防止错误复用缓存变体
HEAD 在没有响应体的情况下获取元数据
Prefer: return=minimal 避免不必要的响应载荷
Prefer: respond-async 支持异步处理

良好的 API 应在不削弱 HTTP 语义清晰性和可预测性的前提下优化性能。

核心原则是:

当客户端不需要数据时,不要传输、计算或重新生成这些数据。

Performance | Remi Resume