← 中文资源

简体中文版本

Preload 预加载提示

Preload 提示让浏览器提前以开发者可控的优先级抓取关键资源,在解析器深入 HTML 或 CSS 才发现之前就开始下载。

阅读英文原文

概念定义

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;
}