mcpField
N
netlify-caching

netlify-caching

github.com/netlify云与 DevOps473.1k 次安装6 天前更新
代码仓库

关于 netlify-caching

netlify-caching 是一项面向 Netlify 开发者的技能,说明如何为动态响应和组件配置缓存、变体与失效。内容涵盖 Netlify CDN、Cache API 及缓存调试要点。

主要功能

  1. 01配置 CDN 缓存控制头
  2. 02按请求条件区分缓存键
  3. 03使用标签清除缓存
  4. 04通过 Cache API 读写响应
  5. 05检查缓存状态与重新验证

适用场景

  1. 01缓存 Netlify Functions 的动态响应
  2. 02按地区、Cookie 或查询参数区分内容
  3. 03内容更新后按标签清除缓存

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/netlify/context-and-tools --skill netlify-caching

安装后在 Claude Code 中输入/netlify-caching确认已加载。

详细介绍

Netlify 缓存

优先使用的缓存控制标头

动态响应(Functions、Edge Functions、代理)默认不会缓存。必须显式启用缓存。在响应中设置 Netlify-CDN-Cache-Control:

import type { Context } from "@netlify/functions";

export default async (req: Request, context: Context) => {
  return new Response("Hello world", {
    headers: {
      'Netlify-CDN-Cache-Control': 'public, durable, max-age=60, stale-while-revalidate=120'
    }
  });
};

标头选择(最具体的优先;CDN-Cache-Control/Cache-Control 始终会向下游传递):

  • Netlify-CDN-Cache-Control 仅适用于 Netlify CDN。优先使用它。
  • CDN-Cache-Control 适用于支持它的所有 CDN。
  • Cache-Control 适用于任何 CDN 或浏览器。

应避免的旧方案: On-demand Builders 不支持这些标头或 Netlify-Vary。它们采用 TTL 模式,且只根据 URL 路径生成缓存键。新代码中不要使用 ODB。

易踩的坑(请先阅读)

  • 静态资源的缓存有效期最长可达一年。 较短的 max-age 会被忽略。只有重新部署或手动清除缓存后,静态资源才会更新。
  • 任何页面启用了 basic-auth,都会禁用整个站点的缓存。
  • durable 仅适用于无服务器函数。 它对 Edge Function 响应不起作用。
  • Cache API 会静默丢弃超过 10 MB 的响应。 cache.put() 不会存储这些响应,后续查找也会未命中。请压缩内容或将其拆分为多个条目。
  • 切勿让敏感内容退出自动失效机制。部署或防火墙变更后,这些内容可能仍会公开缓存。

指令

  • public 会缓存响应。private 仅在浏览器中缓存,不会存入 Netlify 的共享缓存。no-store 会阻止缓存。
  • s-maxage=N 设置 Netlify 共享缓存中的秒数(在该缓存中优先于 max-age)。
  • max-age=N 设置任意缓存中的秒数。
  • stale-while-revalidate=N 在缓存过期后的 N 秒内提供过期内容,同时在后台重新验证。
  • durable(仅适用于无服务器函数)会将响应存入 Netlify 的持久缓存,以便其他边缘节点重复使用,而不必再次调用该函数。

未设置标头时的默认值:

  • 静态:Netlify-CDN-Cache-Control: public, s-maxage=31536000, must-revalidate
  • 动态:Cache-Control: public, max-age=0, must-revalidate

缓存键变化 — Netlify-Vary

响应中的指令以逗号分隔;值列表以竖线分隔:

Netlify-Vary: query=item_id|page, country=es+de|us, cookie=ab_test|is_logged_in
  • query=a|b 按部分查询参数区分;单独使用 query 则按所有参数区分。键区分大小写;参数顺序不影响结果。
  • header=Device-Type|App-Version 支持自定义标头和大多数标准标头。
  • language=en|es+pt 使用 + 表示分组。此设置会根据 Accept-Language 并结合质量权重进行匹配。
  • country=us|es+pt 使用 GeoIP 和 ISO 3166-1 两字母代码;+ 表示分组。
  • cookie=ab_test|is_logged_in 针对指定的键,而非整个 Cookie 标头。

不能按以下标头区分: Accept*、Cache-Control、Connection、Content-Length、Cookie、Host、If-*、Range、Referer、Upgrade、User-Agent。对于语言、Cookie 或格式,请使用 Vary: Accept-Language/Vary: Cookie,或使用相应的 Netlify-Vary 指令。

一致性规则: 同一 URL 的每个响应都必须返回相同的 Netlify-Vary。第一个缓存响应中的指令优先生效,之后的指令会被忽略。

Netlify-Vary 和标准 Vary 都会生效。格式或编码请使用 Vary,也可用它向 Cloudflare 等上游 CDN 传递指令。

缓存标签与退出自动失效

为响应添加标签,以便按标签清除缓存:

Netlify-Cache-Tag: tag1,tag2,tag3
  • Netlify-Cache-Tag(Netlify CDN)优先于 Cache-Tag(会向下游传递)。有些服务提供商会移除 Cache-Tag,因此通过这些提供商代理时,请同时设置两者。
  • 限制:不区分大小写,仅支持 UTF-8,每个标签不超过 1024 个字符,每个响应最多 500 个标签。

使用 Netlify-Cache-ID 可让响应退出自动原子部署失效机制。值以逗号分隔,会自动注册为可用于清除缓存的标签,并有单独的 500 个 ID 上限:

Netlify-Cache-ID: cms-proxy,product,image

退出后,在相关变更发生时按需清除缓存,例如清除 Netlify-Cache-ID 背后的重定向、代理或函数变更所涉及的缓存。

按需失效(清除缓存)

使用 purgeCache 从已部署的函数中清除缓存。站点 ID 会自动传入:

import { purgeCache } from "@netlify/functions";

export default async () => {
  await purgeCache(); // no args = purge everything for the site
  return new Response("Purged!", { status: 202 });
};

按标签清除缓存,也可以指定部署版本/子域名:

import { purgeCache } from "@netlify/functions";

export default async (req: Request) => {
  const cacheTag = new URL(req.url).searchParams.get("tag");
  if (!cacheTag) return;
  await purgeCache({
    tags: [cacheTag],
    deployAlias: "deploy-preview-11",
    domain: "early-access.company.com",
  });
  return new Response("Purged!", { status: 202 });
};

兼容 Lambda 的函数使用旧版 module.exports.handler = async (event, context) => {…} 签名。必须传入 context.clientContext.custom.purge_api_token:

import { purgeCache } from "@netlify/functions";

module.exports.handler = async (event, context) => {
  const token = context.clientContext.custom.purge_api_token;
  await purgeCache({ tags: ["tag1", "tag2"], token });
  return { body: "Purged!", statusCode: 202 };
};

直接调用 API(在函数外部):发送 POST https://api.netlify.com/api/v1/purge 请求,并设置 Authorization: Bearer <personal_access_token> 和 Content-Type: application/json:

curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <personal_access_token>" \
  --data '{"site_slug": "mysitename", "cache_tags": ["news"], "deploy_alias": "deploy-preview-11", "domain": "early-access.company.com"}' \
  'https://api.netlify.com/api/v1/purge'
  • 按站点清除缓存:传入 site_id 或 site_slug。
  • 按标签清除缓存:传入 cache_tags 和站点。
  • 省略 cache_tags 会清除整个站点的缓存。传入空的 cache_tags 列表则不会清除任何内容。
  • 标识符对应关系:在 UI 中(Project configuration > General > Project details > Project information),Project ID = site_id,Project name = site_slug。请参阅 https://docs.netlify.com/api-and-cli-guides/api-guides/get-started-with-api#get-site。
  • 速率限制: 每个标签或站点每 5s 最多只能清除缓存两次。超出限制会返回 429。

Cache API(caches 全局对象)

以编程方式读写来自 Functions/Edge Functions 的 HTTP 响应。可将其用于缓存路由的单个组件或任意 fetch 请求,并与基于标头的路由缓存搭配使用。

作用域规则: 可在任意位置调用 caches.open(),但只能在请求处理程序中调用 match/put/delete。在模块/全局作用域中调用这些方法会抛出异常。

import type { Config, Context } from "@netlify/functions";

const cache = await caches.open("my-cache"); // ok in global scope

export default async (req: Request, context: Context) => {
  const request = new Request("https://example.com/expensive-api");
  const cached = await cache.match(request);
  if (cached) return cached;

  const fresh = await fetch(request);
  if (fresh.ok) {
    cache.put(request, fresh.clone()).catch((error) => {
      console.error("Failed to add to the cache:", error);
    });
  }
  return fresh;
};

export const config: Config = { path: "/cache-api-example" };

CacheStorage 子集:

  • caches.match(request) 会从任意缓存中返回一个 Response,若无匹配项则返回 undefined。
  • caches.open(name) 会返回一个 Cache。不同名称会拆分缓存并降低命中率,因此应使用少量且有意义的名称。

Cache 方法(均需先调用 caches.open()):

  • cache.match(request) 会返回一个 Response,若无匹配项则返回 undefined。
  • cache.put(request, response) 会添加一个响应。
  • cache.add(request) / cache.addAll(requests) 会获取并存储响应。
  • cache.delete(request) 会返回 true。
  • 尚未实现 keys(),因此无法列出缓存内容。

一致性:读写操作具有强一致性。删除操作最终一致,因此已删除的条目可能仍会短暂返回。

无法缓存的内容:

  • 部分响应(206)、Vary: * 或非 GET 方法。
  • 超过 10 MB 的响应。这些响应不会被存储,且不会显示任何提示。

响应还必须满足以下所有条件,否则存储会报错:

  • 包含 max-age/s-maxage ≥ 1s 的 cache-control 标头
  • 包含 public(不能是 private/no-cache/no-store)
  • 状态码为 2xx

对于无法控制的响应,可使用 fetchWithCache 重写标头。

每次调用的限制: 最多进行 100 次查找和 20 次插入/删除。超过限制后,后续查找都不会返回结果,写入/删除操作也不会执行。每个请求中的 edge 函数共享限制,但无服务器函数和 edge 函数之间的限制彼此独立。

缓存数据按区域存储(不会复制)。重新部署以及 max-age/s-maxage 到期时,缓存会自动失效。

@netlify/cache 模块

安装此模块即可获取辅助工具、时间常量(MINUTE/HOUR/DAY)以及用于本地开发的 caches 导出:

npm install @netlify/cache

本地开发的变通方案: caches 全局对象并非 Node.js 的一部分。Netlify 会在其 Functions/Edge 运行时(包括线上环境和 netlify dev 下)提供该对象。如果使用框架自身的开发服务器,该全局对象未定义,调用时会抛出异常。请改为导入:

import { caches } from "@netlify/cache";
const cache = await caches.open("my-cache");

这需要 Netlify CLI 20.0.3+。本地不会持久化任何内容:查找不会返回结果,写入/删除也不会修改数据。与使用全局对象相比,功能上没有变化。

cacheHeaders(settings) → 标头对象
import { cacheHeaders, DAY } from "@netlify/cache";

const headers = {
  "x-custom-header": "some value",
  ...cacheHeaders({
    ttl: 2 * DAY,          // s-maxage
    swr: HOUR,             // stale-while-revalidate
    durable: true,
    tags: ["product", "sale"],
    overrideDeployRevalidation: ["tag"], // opt out of atomic-deploy invalidation
    vary: {
      cookie: ["ab_test_name", "ab_test_bucket"],
      query: ["item_id", "page"], // or true for all
      country: ["us", ["es", "pt"]], // nested = OR
      language: ["en"],
      header: ["Device-Type"],
    },
  }),
};

如果只需要通用(非 Netlify)标头,请改用 cdn-cache-control npm 模块。

fetchWithCache(resource, options?, cacheSettings?)

可直接替代 fetch,返回缓存的响应;若无缓存,则会获取、存储并返回响应。cacheSettings 会覆盖冲突的响应标头。启用 swr 时,会自动在后台重新验证。

import { fetchWithCache, DAY } from "@netlify/cache";

const response = await fetchWithCache("https://example.com/expensive-api", {
  ttl: 2 * DAY,
  tags: ["product", "sale"],
  vary: { cookie: ["ab_test_name"], query: ["item_id", "page"] },
});
getCacheStatus(response | headers | headerString)

返回 { hit, caches: { durable: { hit, stale, stored, ttl }, edge: { hit, stale } } }。

const { hit, edge, durable } = getCacheStatus(response);
needsRevalidation(response) → boolean

仅当直接调用 cache.match/cache.put 时需要(与 fetchWithCache + swr 配合使用时则不需要)。当 Cache API 响应在其 SWR 窗口内已过期时,该函数会返回 true。先返回过期响应,然后在 context.waitUntil 中重新验证,并通过 cache.put 存储新副本:

if (cached) {
  if (needsRevalidation(cached)) {
    context.waitUntil(
      fetch(request).then((fresh) => {
        const response = new Response(fresh.body, {
          headers: { ...Object.fromEntries(fresh.headers), ...cacheHeaders({ ttl: MINUTE, swr: HOUR }) },
        });
        return cache.put(request, response);
      })
    );
  }
  return cached;
}

持久缓存

添加 durable(仅适用于无服务器环境),使没有本地副本的边缘节点先检查共享持久缓存,再调用函数。这可以减少调用次数,并缩短缓存未命中的延迟。

  • 它最终会保持一致,因此多个区域仍可能针对每个版本调用函数几次。
  • 它与站点的函数区域共置。
  • 它支持 Netlify-Vary、SWR 和按需失效。
  • **Next.js:**Next Runtime 5.5.0+ 会自动使用持久缓存。

使用 Cache-Status 调试

Netlify 会在所有响应中设置 Cache-Status(RFC 9211)。请在已部署的 URL 上检查该标头。查找以 "Netlify Edge" 或 "Netlify Durable" 开头的值:

  • "Netlify Edge"; fwd=miss 表示未缓存任何内容。
  • "Netlify Edge"; hit 表示响应由缓存提供。
  • "Netlify Edge"; hit; fwd=stale 表示在重新验证期间提供了过期响应(SWR)。
  • "Netlify Durable"; fwd=uri-miss; stored=true; ttl=3600 表示持久缓存未命中,随后已存储内容。
  • "Netlify Durable"; hit; ttl=1234 表示持久缓存命中。

负数 ttl 表示距离过期已过去的秒数。每个请求可能命中不同的缓存实例。没有生产流量或未启用 durable 时,命中前可能会遇到多个空缓存;重复请求可预热其中一个缓存。

Netlify 缓存规范

以下是组织约定,并非文档事实——它们由 ctx-gen 合并到渲染后的技能中,且不会自动生成。由技能维护者负责维护。

  1. CDN 只缓存 GET 响应。无论标头如何,POST/PUT 等请求都不会缓存——请通过 GET 路由公开可缓存的数据(将输入放在 URL 或查询字符串中)。
  2. 如果没有 Netlify-Vary: query=...,完整查询字符串会作为缓存键——每个不同的查询字符串(utm_*、fbclid 等)都是一个单独的缓存条目。只列出实际会改变响应的参数。
  3. netlify dev 不会模拟 CDN 缓存——本地每次都缓存未命中是预期行为,并非错误。请通过已部署 URL(Deploy Preview 或生产环境)的 Cache-Status 标头验证缓存行为。
  4. purgeCache() 只有在已部署函数内部才会自动获取凭据。从 CI、本地脚本或构建环境调用时,请传入 token(从环境变量读取的个人访问令牌,绝不可硬编码)和 siteID。

评论

支持 Markdown