概念定义
Early Hints 是一种信息性 HTTP 状态码,用于在最终响应之前先行携带提示。服务器先发送带 Link 头的 103(预加载样式、脚本或 preconnect 目标),慢速工作完成后再发送 200 等真实状态。浏览器立即利用提示而不是空等。
工作机制
服务器在同一条连接上先写 103 中间响应再写最终响应。允许的头是安全子集,核心是 Link。提示必须是幂等 advisory:提前行动不会因最终响应不同而产生错误行为。
使用场景
带慢速数据获取的 SSR 页面收益最大,因为关键 CSS 与 preconnect 在数据返回前就可知。CDN 可为缓存页面缓存并重放提示。度量点在首字节时间与首次渲染时间的差距,收益就出现在这个窗口里。
常见误区
Early Hints 不是服务器推送的替代品,它只能提示声明式加载,不能注入任意数据。若最终响应改变了提示的 URL,会造成浪费抓取甚至重复下载。中间盒对 1xx 响应处理不当会破坏整个交换,上线前必须验证整条代理链。提示与常规优先级调度竞争,集合务必小而关键。
代码示例
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Link:
target: str
rel: str
title: str | None = None
def split_parts(header: str) -> list[str]:
parts: list[str] = []
current: list[str] = []
in_quotes = False
in_angle = False
for char in header:
if char == '"':
in_quotes = not in_quotes
elif char == "<" and not in_quotes:
in_angle = True
elif char == ">" and not in_quotes:
in_angle = False
if char == "," and not in_quotes and not in_angle:
parts.append("".join(current).strip())
current = []
else:
current.append(char)
if "".join(current).strip():
parts.append("".join(current).strip())
return parts
def parse_link_header(header: str) -> dict[str, Link]:
links: dict[str, Link] = {}
for part in split_parts(header):
if not part.startswith("<") or ">" not in part:
continue
target, _, rest = part[1:].partition(">")
attributes: dict[str, str] = {}
for attribute in rest.split(";"):
attribute = attribute.strip()
if not attribute or "=" not in attribute:
continue
name, _, raw_value = attribute.partition("=")
attributes[name.strip().lower()] = raw_value.strip().strip('"')
rel = attributes.get("rel")
if rel:
links[rel] = Link(target=target, rel=rel, title=attributes.get("title"))
return links
def main() -> None:
header = (
'<https://api.example.com/items?page=2&limit=20>; rel="next"; '
'title="Next, page 2", '
'<https://api.example.com/items?page=9&limit=20>; rel="last"'
)
links = parse_link_header(header)
print(links["next"].target)
print(links["next"].title)
assert links["last"].target.endswith("page=9&limit=20")
assert parse_link_header("") == {}
if __name__ == "__main__":
main()
export function buildPaginationLinkHeader(
baseUrl: string,
options: { page: number; perPage: number; total: number },
): string {
if (typeof baseUrl !== "string" || baseUrl.length === 0) {
throw new TypeError("baseUrl must be a non-empty string");
}
const { page, perPage, total } = options;
if (!Number.isInteger(page) || page < 1) {
throw new RangeError("page must be a positive integer");
}
if (!Number.isInteger(perPage) || perPage < 1) {
throw new RangeError("perPage must be a positive integer");
}
if (!Number.isInteger(total) || total < 0) {
throw new RangeError("total must be a non-negative integer");
}
const lastPage = Math.max(1, Math.ceil(total / perPage));
if (page > lastPage) throw new RangeError("page must not exceed the last page");
const base = new URL(baseUrl);
const link = (target: number, rel: string): string => {
const url = new URL(base);
url.searchParams.set("page", String(target));
url.searchParams.set("limit", String(perPage));
return "<" + url.toString() + '>; rel="' + rel + '"';
};
const parts = [link(page, "self"), link(1, "first")];
if (page > 1) parts.push(link(page - 1, "prev"));
if (page < lastPage) parts.push(link(page + 1, "next"));
parts.push(link(lastPage, "last"));
return parts.join(", ");
}