概念定义
preload 是一种声明式 link 关系,指示浏览器立即开始抓取某个资源,并用 as 属性决定缓存桶与优先级类别。它服务于晚发现的依赖:解析到深层 HTML 或 CSS 之后才会用到的字体、脚本或首屏图片,可以提前进入下载队列。
工作机制
每个提示声明 href、as 指定的目标类型,以及可选的 type、crossorigin 与 fetchpriority。解析到提示即发起请求,命中 HTTP 缓存,并与后续解析器发现的请求去重。若提示的目标类型与真实请求不匹配,会触发第二次下载并在控制台告警。
使用场景
适合“发现得晚、需要得早”的资源:CSS 中声明的字体、首屏主图、关键脚本、阻塞渲染的样式导入链。服务端还可在完整响应生成前通过 Early Hints 把同样的声明推给浏览器。
常见误区
preload 只负责取回字节,不负责执行或渲染。它与其他请求共享带宽,过度预加载反而拖延关键路径。带 crossorigin 语义的资源必须与真实请求完全一致,否则重复抓取。应结合 Resource Timing 验证每条提示确实让请求提前,而不是徒增流量。
代码示例
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 interface ParsedLink {
url: string;
rel: string[];
params: Record<string, string>;
}
function skipWhitespace(header: string, index: number): number {
let cursor = index;
while (cursor < header.length && /\s/.test(header[cursor])) cursor += 1;
return cursor;
}
function parseParameters(
header: string,
start: number,
): { params: Record<string, string>; next: number } {
const params: Record<string, string> = {};
let index = start;
while (index < header.length && header[index] !== ",") {
index = skipWhitespace(header, index);
if (index >= header.length || header[index] === ",") break;
if (header[index] !== ";") throw new SyntaxError("expected ';' at index " + index);
index = skipWhitespace(header, index + 1);
const keyStart = index;
while (index < header.length && /[A-Za-z0-9!#$%&'*+\-.^_\x60|~]/.test(header[index])) {
index += 1;
}
if (index === keyStart) throw new SyntaxError("missing link parameter name");
const key = header.slice(keyStart, index).toLowerCase();
index = skipWhitespace(header, index);
if (header[index] !== "=") throw new SyntaxError("missing '=' for " + key);
index = skipWhitespace(header, index + 1);
let value = "";
if (header[index] === '"') {
index += 1;
let closed = false;
while (index < header.length) {
const character = header[index];
if (character === "\\") {
index += 1;
if (index >= header.length) throw new SyntaxError("unterminated escape");
value += header[index];
index += 1;
} else if (character === '"') {
index += 1;
closed = true;
break;
} else {
value += character;
index += 1;
}
}
if (!closed) throw new SyntaxError("unterminated quoted parameter");
} else {
const valueStart = index;
while (index < header.length && header[index] !== ";" && header[index] !== ",") {
index += 1;
}
value = header.slice(valueStart, index).trim();
if (value === "") throw new SyntaxError("empty link parameter value");
}
params[key] = value;
}
return { params, next: index };
}
export function parseLinkHeader(header: string, baseUrl?: string): ParsedLink[] {
if (typeof header !== "string") throw new TypeError("header must be a string");
const links: ParsedLink[] = [];
let index = 0;
while (index < header.length) {
index = skipWhitespace(header, index);
while (header[index] === ",") index = skipWhitespace(header, index + 1);
if (index >= header.length) break;
if (header[index] !== "<") throw new SyntaxError("expected '<' at index " + index);
const end = header.indexOf(">", index + 1);
if (end === -1) throw new SyntaxError("unterminated link target");
const target = header.slice(index + 1, end).trim();
if (target === "") throw new SyntaxError("empty link target");
index = end + 1;
const { params, next } = parseParameters(header, index);
index = next;
if (header[index] === ",") index += 1;
let url = target;
if (baseUrl !== undefined) {
try {
url = new URL(target, baseUrl).toString();
} catch {
throw new SyntaxError("invalid link URL: " + target);
}
}
links.push({
url,
rel: (params.rel ?? "").split(/\s+/).filter((entry) => entry.length > 0),
params,
});
}
return links;
}
export function findLink(links: readonly ParsedLink[], rel: string): string | undefined {
return links.find((link) => link.rel.includes(rel))?.url;
}