headless-caching-strategy
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCaching & Performance for Headless VTEX
无头VTEX店铺的缓存与性能优化
Overview
概述
What this skill covers: Caching strategies for headless VTEX storefronts, including which APIs can be aggressively cached, which must never be cached, CDN configuration, BFF-level caching with patterns, and cache invalidation strategies.
stale-while-revalidateWhen to use it: When building or optimizing a headless VTEX storefront for performance. Proper caching is the single most impactful performance optimization for headless commerce — it reduces latency, server load, and API rate limit consumption while improving shopper experience.
What you'll learn:
- How to classify VTEX APIs into cacheable (public/read-only) vs non-cacheable (transactional/personal)
- How to implement CDN caching for Intelligent Search and Catalog APIs
- How to add BFF-level caching with for optimal freshness/performance balance
stale-while-revalidate - How to implement cache invalidation when catalog data changes
本技能涵盖内容:无头VTEX店铺前端的缓存策略,包括哪些API可被激进缓存、哪些绝对不能被缓存、CDN配置、基于模式的BFF层缓存,以及缓存失效策略。
stale-while-revalidate适用场景:在构建或优化无头VTEX店铺前端以提升性能时使用。合理的缓存是无头电商中影响最大的性能优化手段——它能减少延迟、降低服务器负载、减少API调用频次限制的消耗,同时提升购物者体验。
你将学到:
- 如何将VTEX API分为可缓存(公开/只读)和不可缓存(事务性/个性化)两类
- 如何为Intelligent Search和Catalog API实现CDN缓存
- 如何通过模式添加BFF层缓存,以实现数据新鲜度与性能的最优平衡
stale-while-revalidate - 当商品目录数据变更时,如何实现缓存失效
Key Concepts
核心概念
Essential knowledge before implementation:
实现前需掌握的关键知识:
Concept 1: API Cacheability Classification
概念1:API可缓存性分类
VTEX APIs fall into two categories based on whether their responses can be cached:
Cacheable APIs (public, read-only, non-personalized):
| API | Example Endpoints | Recommended TTL |
|---|---|---|
| Intelligent Search | | 2-5 minutes |
| Catalog (public) | | 5-15 minutes |
| Intelligent Search autocomplete | | 1-2 minutes |
| Intelligent Search top searches | | 5-10 minutes |
Non-cacheable APIs (transactional, personalized, or sensitive):
| API | Example Endpoints | Why Not Cacheable |
|---|---|---|
| Checkout | | Cart data is per-user, changes with every action |
| Profile | | Personal data, GDPR/LGPD sensitive |
| OMS (Orders) | | Order status changes, user-specific |
| Payments | | Financial transactions, must always be real-time |
| Pricing (private) | | May have per-user pricing rules |
VTEX API根据响应是否可缓存分为两类:
可缓存API(公开、只读、非个性化):
| API | 示例端点 | 推荐TTL |
|---|---|---|
| Intelligent Search | | 2-5分钟 |
| Catalog(公开) | | 5-15分钟 |
| Intelligent Search autocomplete | | 1-2分钟 |
| Intelligent Search top searches | | 5-10分钟 |
不可缓存API(事务性、个性化或敏感数据):
| API | 示例端点 | 不可缓存原因 |
|---|---|---|
| Checkout | | 购物车数据为用户专属,随操作实时变更 |
| Profile | | 含个人数据,受GDPR/LGPD等隐私法规约束 |
| OMS(订单管理系统) | | 订单状态实时变更,且为用户专属 |
| Payments | | 金融交易类接口,必须返回实时数据 |
| Pricing(私有) | | 可能包含针对特定用户的定价规则 |
Concept 2: Cache Layers
概念2:缓存层级
In a headless VTEX architecture, caching can happen at multiple layers:
- CDN Edge Cache: Caches responses closest to the user. Best for Intelligent Search (called directly from frontend). Use headers.
Cache-Control - BFF In-Memory Cache: Caches VTEX API responses within the BFF process. Fast but limited by server memory. Good for category trees and top searches.
- BFF Distributed Cache (Redis/Memcached): Shared cache across multiple BFF instances. Best for catalog data that multiple users request.
- Browser Cache: Client-side caching via headers. Good for static catalog data, but be careful with personalized data.
Cache-Control
在无头VTEX架构中,缓存可在多个层级实现:
- CDN边缘缓存:在离用户最近的节点缓存响应,最适合直接从前端调用的Intelligent Search。通过头配置。
Cache-Control - BFF内存缓存:在BFF进程内缓存VTEX API响应,速度快但受服务器内存限制,适合类目树和热门搜索等数据。
- BFF分布式缓存(Redis/Memcached):在多个BFF实例间共享的缓存,最适合多用户请求的商品目录数据。
- 浏览器缓存:通过头实现客户端缓存,适合静态商品目录数据,但需注意避免缓存个性化数据。
Cache-Control
Concept 3: Stale-While-Revalidate (SWR)
概念3:Stale-While-Revalidate(SWR)模式
The pattern serves cached (potentially stale) data immediately while asynchronously fetching fresh data in the background. This provides:
stale-while-revalidate- Instant responses: Users see data immediately, even if slightly stale
- Eventual freshness: Cache is updated in the background for the next request
- Resilience: If the origin is down, stale data is still served
The HTTP header pattern:
Cache-Control: public, max-age=120, stale-while-revalidate=60- Serves cached data for 120 seconds without checking origin
- Between 120-180 seconds, serves stale data while fetching fresh data
- After 180 seconds, waits for fresh data before responding
stale-while-revalidate- 即时响应:用户可立即看到数据,即使数据略有过期
- 最终一致性:后台更新缓存,确保后续请求能获取最新数据
- 容错性:若源服务不可用,仍可返回过期数据保障服务可用
对应的HTTP头示例:
Cache-Control: public, max-age=120, stale-while-revalidate=60- 120秒内直接返回缓存数据,无需检查源服务
- 120-180秒之间,返回过期数据并在后台获取最新数据
- 180秒后,等待获取最新数据后再响应
Concept 4: Cache Invalidation
概念4:缓存失效
Catalog data changes (product updates, price changes, new products) must eventually reflect on the storefront. Strategies:
- Time-based (TTL): Set appropriate expiration times. Shorter TTL = fresher data but more origin load.
- Event-driven: Use VTEX webhooks/hooks to invalidate specific cache entries when data changes.
- Manual purge: Provide admin endpoints to force-clear cache for specific products or categories.
Architecture/Data Flow:
text
Frontend (Browser)
│
├── Direct to CDN (Intelligent Search)
│ └── CDN Edge Cache (TTL: 2-5 min, SWR: 60s)
│ └── VTEX Intelligent Search API
│
└── BFF Endpoints
│
├── Cacheable routes (catalog, category tree)
│ └── BFF Cache Layer (Redis/in-memory)
│ └── VTEX Catalog API
│
└── Non-cacheable routes (checkout, profile, orders)
└── Direct proxy to VTEX (NO CACHING)商品目录数据变更(如商品更新、价格调整、新品上架)最终需同步到店铺前端,常见策略:
- 基于时间(TTL):设置合理的过期时间。TTL越短,数据越新鲜,但源服务负载越高。
- 事件驱动:使用VTEX webhooks/hooks在数据变更时失效特定缓存条目。
- 手动清理:提供管理员端点,可强制清理特定商品或类目的缓存。
架构/数据流:
text
前端(浏览器)
│
├── 直接请求CDN(Intelligent Search)
│ └── CDN边缘缓存(TTL: 2-5分钟, SWR: 60秒)
│ └── VTEX Intelligent Search API
│
└── 请求BFF端点
│
├── 可缓存路由(商品目录、类目树)
│ └── BFF缓存层(Redis/内存)
│ └── VTEX Catalog API
│
└── 不可缓存路由(结账、用户档案、订单)
└── 直接代理到VTEX(禁止缓存)Constraints
约束规则
Rules that MUST be followed to avoid failures, security issues, or platform incompatibilities.
必须遵守的规则,以避免故障、安全问题或平台兼容性问题。
Constraint: MUST Cache Public Data Aggressively
约束:必须激进缓存公开数据
Rule: Search results, catalog data, category trees, and other public read-only data MUST be cached at appropriate levels (CDN, BFF, or both). Without caching, every user request hits VTEX APIs directly.
Why: Without caching, a headless storefront generates an API request for every single page view, search, and category browse. This quickly exceeds VTEX API rate limits (causing 429 errors and degraded service), adds 200-500ms of latency per request, and creates a poor shopper experience. A store with 10,000 concurrent users making uncached search requests will overwhelm any API.
Detection: If a headless storefront calls Intelligent Search or Catalog APIs without any caching layer (no CDN cache headers, no BFF cache, no headers) → STOP immediately. Caching must be implemented for all public, read-only API responses.
Cache-Control✅ CORRECT:
typescript
// BFF route with in-memory cache for category tree
import { Router, Request, Response } from "express";
const router = Router();
interface CacheEntry<T> {
data: T;
expiresAt: number;
staleAt: number;
}
const cache = new Map<string, CacheEntry<unknown>>();
function getCached<T>(key: string): { data: T; isStale: boolean } | null {
const entry = cache.get(key) as CacheEntry<T> | undefined;
if (!entry) return null;
const now = Date.now();
if (now > entry.expiresAt) {
cache.delete(key);
return null;
}
return {
data: entry.data,
isStale: now > entry.staleAt,
};
}
function setCache<T>(key: string, data: T, maxAgeMs: number, swrMs: number): void {
const now = Date.now();
cache.set(key, {
data,
staleAt: now + maxAgeMs,
expiresAt: now + maxAgeMs + swrMs,
});
}
const VTEX_ACCOUNT = process.env.VTEX_ACCOUNT!;
const CATALOG_BASE = `https://${VTEX_ACCOUNT}.vtexcommercestable.com.br/api/catalog_system/pub`;
// Category tree — cache for 15 minutes, SWR for 5 minutes
router.get("/categories", async (_req: Request, res: Response) => {
const cacheKey = "category-tree";
const cached = getCached<unknown>(cacheKey);
if (cached && !cached.isStale) {
res.set("X-Cache", "HIT");
return res.json(cached.data);
}
// If stale, serve stale data and refresh in background
if (cached && cached.isStale) {
res.set("X-Cache", "STALE");
res.json(cached.data);
// Background refresh
fetch(`${CATALOG_BASE}/category/tree/3`)
.then((r) => r.json())
.then((data) => setCache(cacheKey, data, 15 * 60 * 1000, 5 * 60 * 1000))
.catch((err) => console.error("Background cache refresh failed:", err));
return;
}
// Cache miss — fetch and cache
try {
const response = await fetch(`${CATALOG_BASE}/category/tree/3`);
const data = await response.json();
setCache(cacheKey, data, 15 * 60 * 1000, 5 * 60 * 1000);
res.set("X-Cache", "MISS");
res.json(data);
} catch (error) {
console.error("Error fetching categories:", error);
res.status(500).json({ error: "Failed to fetch categories" });
}
});❌ WRONG:
typescript
// No caching — every request hits VTEX directly
router.get("/categories", async (_req: Request, res: Response) => {
// This fires on EVERY request — 10,000 users = 10,000 API calls
const response = await fetch(
`https://mystore.vtexcommercestable.com.br/api/catalog_system/pub/category/tree/3`
);
const data = await response.json();
res.json(data); // No cache headers, no BFF cache, no CDN cache
});规则:搜索结果、商品目录数据、类目树及其他公开只读数据必须在合适的层级(CDN、BFF或两者同时)进行缓存。若不缓存,每个用户请求都会直接命中VTEX API。
原因:若不缓存,无头店铺前端的每一次页面浏览、搜索和类目查看都会发起API请求,这会迅速触发VTEX API频次限制(导致429错误和服务降级),每个请求增加200-500ms延迟,同时带来糟糕的购物体验。若有10000个并发用户发起无缓存的搜索请求,会直接压垮API服务。
检测方式:若无头店铺前端调用Intelligent Search或Catalog API时未使用任何缓存层(无CDN缓存头、无BFF缓存、无头)→ 立即停止,必须为所有公开只读API响应实现缓存。
Cache-Control✅ 正确示例:
typescript
// 为类目树实现内存缓存的BFF路由
import { Router, Request, Response } from "express";
const router = Router();
interface CacheEntry<T> {
data: T;
expiresAt: number;
staleAt: number;
}
const cache = new Map<string, CacheEntry<unknown>>();
function getCached<T>(key: string): { data: T; isStale: boolean } | null {
const entry = cache.get(key) as CacheEntry<T> | undefined;
if (!entry) return null;
const now = Date.now();
if (now > entry.expiresAt) {
cache.delete(key);
return null;
}
return {
data: entry.data,
isStale: now > entry.staleAt,
};
}
function setCache<T>(key: string, data: T, maxAgeMs: number, swrMs: number): void {
const now = Date.now();
cache.set(key, {
data,
staleAt: now + maxAgeMs,
expiresAt: now + maxAgeMs + swrMs,
});
}
const VTEX_ACCOUNT = process.env.VTEX_ACCOUNT!;
const CATALOG_BASE = `https://${VTEX_ACCOUNT}.vtexcommercestable.com.br/api/catalog_system/pub`;
// 类目树 — 缓存15分钟,SWR为5分钟
router.get("/categories", async (_req: Request, res: Response) => {
const cacheKey = "category-tree";
const cached = getCached<unknown>(cacheKey);
if (cached && !cached.isStale) {
res.set("X-Cache", "HIT");
return res.json(cached.data);
}
// 若数据过期,返回过期数据并在后台刷新
if (cached && cached.isStale) {
res.set("X-Cache", "STALE");
res.json(cached.data);
// 后台刷新缓存
fetch(`${CATALOG_BASE}/category/tree/3`)
.then((r) => r.json())
.then((data) => setCache(cacheKey, data, 15 * 60 * 1000, 5 * 60 * 1000))
.catch((err) => console.error("后台缓存刷新失败:", err));
return;
}
// 缓存未命中 — 拉取数据并缓存
try {
const response = await fetch(`${CATALOG_BASE}/category/tree/3`);
const data = await response.json();
setCache(cacheKey, data, 15 * 60 * 1000, 5 * 60 * 1000);
res.set("X-Cache", "MISS");
res.json(data);
} catch (error) {
console.error("拉取类目数据失败:", error);
res.status(500).json({ error: "拉取类目数据失败" });
}
});❌ 错误示例:
typescript
// 无缓存 — 每个请求都直接命中VTEX
router.get("/categories", async (_req: Request, res: Response) => {
// 每个请求都会触发该逻辑 — 10000个用户 = 10000次API调用
const response = await fetch(
`https://mystore.vtexcommercestable.com.br/api/catalog_system/pub/category/tree/3`
);
const data = await response.json();
res.json(data); // 无缓存头、无BFF缓存、无CDN缓存
});Constraint: MUST NOT Cache Transactional or Personal Data
约束:禁止缓存事务性或个性化数据
Rule: Responses from Checkout API, Profile API, OMS API, and Payments API MUST NOT be cached at any layer — not in the CDN, not in BFF memory, not in Redis, and not in browser cache.
Why: Caching transactional data can cause catastrophic failures. A cached OrderForm means a shopper sees stale cart contents (wrong items, wrong prices). Cached profile data can leak one user's personal information to another user (especially behind shared caches). Cached order data shows stale statuses. Any of these is a security vulnerability, data privacy violation (GDPR/LGPD), or business logic failure.
Detection: If you see caching logic (Redis , in-memory cache, headers with ) applied to checkout, order, profile, or payment API responses → STOP immediately. These endpoints must always return fresh data.
setCache-Controlmax-age > 0✅ CORRECT:
typescript
// BFF checkout route — explicitly no caching
import { Router, Request, Response } from "express";
import { vtexCheckout } from "../vtex-checkout-client";
export const checkoutRoutes = Router();
// Set no-cache headers for ALL checkout responses
checkoutRoutes.use((_req: Request, res: Response, next) => {
res.set({
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
Pragma: "no-cache",
Expires: "0",
"Surrogate-Control": "no-store",
});
next();
});
checkoutRoutes.get("/cart", async (req: Request, res: Response) => {
const orderFormId = req.session.orderFormId;
if (!orderFormId) {
return res.status(400).json({ error: "No active cart" });
}
try {
// Always fetch fresh — never cache
const result = await vtexCheckout({
path: `/api/checkout/pub/orderForm/${orderFormId}`,
cookies: req.session.vtexCookies || {},
userToken: req.session.vtexAuthToken,
});
req.session.vtexCookies = result.cookies;
res.json(result.data);
} catch (error) {
console.error("Error fetching cart:", error);
res.status(500).json({ error: "Failed to fetch cart" });
}
});❌ WRONG:
typescript
// CATASTROPHIC: Caching checkout data in Redis
import Redis from "ioredis";
const redis = new Redis();
checkoutRoutes.get("/cart", async (req: Request, res: Response) => {
const orderFormId = req.session.orderFormId;
const cacheKey = `cart:${orderFormId}`;
// WRONG: cached cart could have wrong items, old prices, or stale quantities
const cached = await redis.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached)); // Serving stale transactional data!
}
const result = await vtexCheckout({
path: `/api/checkout/pub/orderForm/${orderFormId}`,
cookies: req.session.vtexCookies || {},
});
// WRONG: caching cart data that changes with every user action
await redis.setex(cacheKey, 300, JSON.stringify(result.data));
res.json(result.data);
});规则:Checkout API、Profile API、OMS API和Payments API的响应绝对不能在任何层级缓存——包括CDN、BFF内存、Redis和浏览器缓存。
原因:缓存事务性数据可能导致灾难性故障。缓存的OrderForm会让用户看到过期的购物车内容(错误的商品、错误的价格);缓存的用户档案数据可能导致用户信息泄露(尤其是在共享缓存场景下);缓存的订单数据会显示过期的订单状态。这些情况均属于安全漏洞、数据隐私违规(违反GDPR/LGPD)或业务逻辑故障。
检测方式:若发现针对结账、订单、用户档案或支付API响应的缓存逻辑(如Redis 、内存缓存、的头)→ 立即停止,这些端点必须始终返回最新数据。
setmax-age > 0Cache-Control✅ 正确示例:
typescript
// BFF结账路由 — 明确禁止缓存
import { Router, Request, Response } from "express";
import { vtexCheckout } from "../vtex-checkout-client";
export const checkoutRoutes = Router();
// 为所有结账响应设置禁止缓存的头
checkoutRoutes.use((_req: Request, res: Response, next) => {
res.set({
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
Pragma: "no-cache",
Expires: "0",
"Surrogate-Control": "no-store",
});
next();
});
checkoutRoutes.get("/cart", async (req: Request, res: Response) => {
const orderFormId = req.session.orderFormId;
if (!orderFormId) {
return res.status(400).json({ error: "无活跃购物车" });
}
try {
// 始终拉取最新数据 — 绝不缓存
const result = await vtexCheckout({
path: `/api/checkout/pub/orderForm/${orderFormId}`,
cookies: req.session.vtexCookies || {},
userToken: req.session.vtexAuthToken,
});
req.session.vtexCookies = result.cookies;
res.json(result.data);
} catch (error) {
console.error("拉取购物车数据失败:", error);
res.status(500).json({ error: "拉取购物车数据失败" });
}
});❌ 错误示例:
typescript
// 灾难性错误:在Redis中缓存结账数据
import Redis from "ioredis";
const redis = new Redis();
checkoutRoutes.get("/cart", async (req: Request, res: Response) => {
const orderFormId = req.session.orderFormId;
const cacheKey = `cart:${orderFormId}`;
// 错误:缓存的购物车数据可能包含错误的商品、过期价格或失效数量
const cached = await redis.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached)); // 返回过期的事务性数据!
}
const result = await vtexCheckout({
path: `/api/checkout/pub/orderForm/${orderFormId}`,
cookies: req.session.vtexCookies || {},
});
// 错误:缓存随用户操作实时变更的购物车数据
await redis.setex(cacheKey, 300, JSON.stringify(result.data));
res.json(result.data);
});Constraint: MUST Implement Cache Invalidation Strategy
约束:必须实现缓存失效策略
Rule: Every caching implementation MUST have a clear invalidation strategy. Cached data must have appropriate TTLs and there must be a mechanism to force-invalidate cache when the underlying data changes.
Why: Without invalidation, cached data becomes permanently stale. Products that are out of stock continue to appear available. Price changes don't reflect until the arbitrary TTL expires. New products are invisible. This leads to a poor shopper experience, failed orders (due to stale availability), and incorrect pricing.
Detection: If a caching implementation has no TTL (, expiration time) or has very long TTLs (hours/days) without any invalidation mechanism → STOP immediately. All caches need bounded TTLs and ideally event-driven invalidation.
max-age✅ CORRECT:
typescript
// Cache with TTL + manual invalidation endpoint + event-driven invalidation
import { Router, Request, Response } from "express";
const router = Router();
// In-memory cache with TTL tracking
const productCache = new Map<string, { data: unknown; expiresAt: number }>();
function setProductCache(productId: string, data: unknown, ttlMs: number): void {
productCache.set(productId, {
data,
expiresAt: Date.now() + ttlMs,
});
}
function getProductCache(productId: string): unknown | null {
const entry = productCache.get(productId);
if (!entry || Date.now() > entry.expiresAt) {
productCache.delete(productId);
return null;
}
return entry.data;
}
// Regular product endpoint with cache
router.get("/products/:productId", async (req: Request, res: Response) => {
const { productId } = req.params;
const cached = getProductCache(productId);
if (cached) {
return res.json(cached);
}
const response = await fetch(
`https://${process.env.VTEX_ACCOUNT}.vtexcommercestable.com.br/api/catalog_system/pub/products/search?fq=productId:${productId}`
);
const data = await response.json();
setProductCache(productId, data, 5 * 60 * 1000); // 5-minute TTL
res.json(data);
});
// Manual invalidation endpoint (secured with API key)
router.post("/cache/invalidate", (req: Request, res: Response) => {
const adminKey = req.headers["x-admin-key"];
if (adminKey !== process.env.ADMIN_API_KEY) {
return res.status(403).json({ error: "Unauthorized" });
}
const { productId, pattern } = req.body as { productId?: string; pattern?: string };
if (productId) {
productCache.delete(productId);
return res.json({ invalidated: [productId] });
}
if (pattern === "all") {
const count = productCache.size;
productCache.clear();
return res.json({ invalidated: count });
}
res.status(400).json({ error: "Provide productId or pattern" });
});
// Webhook endpoint for VTEX catalog change events
router.post("/webhooks/catalog-change", (req: Request, res: Response) => {
const { IdSku, productId } = req.body as { IdSku?: string; productId?: string };
if (productId) {
productCache.delete(productId);
console.log(`Cache invalidated for product ${productId}`);
}
// Also invalidate related search cache entries
// In production, use a more sophisticated invalidation strategy
res.status(200).json({ received: true });
});
export default router;❌ WRONG:
typescript
// Cache with no TTL and no invalidation — data becomes permanently stale
const cache = new Map<string, unknown>();
router.get("/products/:productId", async (req: Request, res: Response) => {
const { productId } = req.params;
// Once cached, this data NEVER expires — price changes, stock updates are invisible
if (cache.has(productId)) {
return res.json(cache.get(productId));
}
const response = await fetch(`https://mystore.vtexcommercestable.com.br/api/catalog_system/pub/products/search?fq=productId:${productId}`);
const data = await response.json();
cache.set(productId, data); // No TTL! No invalidation! Stale forever!
res.json(data);
});规则:所有缓存实现必须有明确的失效策略。缓存数据必须设置合理的TTL,且当底层数据变更时必须有强制失效缓存的机制。
原因:若无失效策略,缓存数据会永久过期。售罄的商品仍显示为有货,价格变更需等到TTL过期才会生效,新品长期无法展示。这会导致糟糕的购物体验、订单失败(因过期库存数据)和定价错误。
检测方式:若缓存实现无TTL(、过期时间)或TTL过长(数小时/数天)且无任何失效机制→立即停止。所有缓存都需要设置有限的TTL,理想情况下结合事件驱动的失效机制。
max-age✅ 正确示例:
typescript
// 带TTL + 手动失效端点 + 事件驱动失效的缓存实现
import { Router, Request, Response } from "express";
const router = Router();
// 带TTL追踪的内存缓存
const productCache = new Map<string, { data: unknown; expiresAt: number }>();
function setProductCache(productId: string, data: unknown, ttlMs: number): void {
productCache.set(productId, {
data,
expiresAt: Date.now() + ttlMs,
});
}
function getProductCache(productId: string): unknown | null {
const entry = productCache.get(productId);
if (!entry || Date.now() > entry.expiresAt) {
productCache.delete(productId);
return null;
}
return entry.data;
}
// 带缓存的常规商品端点
router.get("/products/:productId", async (req: Request, res: Response) => {
const { productId } = req.params;
const cached = getProductCache(productId);
if (cached) {
return res.json(cached);
}
const response = await fetch(
`https://${process.env.VTEX_ACCOUNT}.vtexcommercestable.com.br/api/catalog_system/pub/products/search?fq=productId:${productId}`
);
const data = await response.json();
setProductCache(productId, data, 5 * 60 * 1000); // 5分钟TTL
res.json(data);
});
// 手动失效端点(需API密钥验证)
router.post("/cache/invalidate", (req: Request, res: Response) => {
const adminKey = req.headers["x-admin-key"];
if (adminKey !== process.env.ADMIN_API_KEY) {
return res.status(403).json({ error: "未授权访问" });
}
const { productId, pattern } = req.body as { productId?: string; pattern?: string };
if (productId) {
productCache.delete(productId);
return res.json({ invalidated: [productId] });
}
if (pattern === "all") {
const count = productCache.size;
productCache.clear();
return res.json({ invalidated: count });
}
res.status(400).json({ error: "请提供productId或pattern参数" });
});
// VTEX商品目录变更事件的Webhook端点
router.post("/webhooks/catalog-change", (req: Request, res: Response) => {
const { IdSku, productId } = req.body as { IdSku?: string; productId?: string };
if (productId) {
productCache.delete(productId);
console.log(`已失效商品${productId}的缓存`);
}
// 同时失效相关的搜索缓存条目
// 生产环境中,请使用更复杂的失效策略
res.status(200).json({ received: true });
});
export default router;❌ 错误示例:
typescript
// 无TTL且无失效策略的缓存 — 数据会永久过期
const cache = new Map<string, unknown>();
router.get("/products/:productId", async (req: Request, res: Response) => {
const { productId } = req.params;
// 一旦缓存,数据永不过期 — 价格变更、库存更新均无法展示
if (cache.has(productId)) {
return res.json(cache.get(productId));
}
const response = await fetch(`https://mystore.vtexcommercestable.com.br/api/catalog_system/pub/products/search?fq=productId:${productId}`);
const data = await response.json();
cache.set(productId, data); // 无TTL!无失效策略!数据永久过期!
res.json(data);
});Implementation Pattern
实现模式
The canonical, recommended way to implement this feature or pattern.
推荐的标准化实现方式或模式。
Step 1: Set Up CDN Cache Headers for Intelligent Search
步骤1:为Intelligent Search配置CDN缓存头
Since Intelligent Search is called directly from the frontend, use a CDN (e.g., Cloudflare, CloudFront, Fastly) to cache responses at the edge. Configure your CDN to respect headers or set custom caching rules for the search API path.
Cache-Controltypescript
// If you're using a CDN worker/edge function to add cache headers:
// cloudflare-worker.ts or similar edge function
async function handleSearchRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
// Only cache GET requests to Intelligent Search
if (request.method !== "GET") {
return fetch(request);
}
// Check CDN cache first
const cacheKey = new Request(url.toString(), request);
const cachedResponse = await caches.default.match(cacheKey);
if (cachedResponse) {
return cachedResponse;
}
// Fetch from VTEX
const response = await fetch(request);
const responseClone = response.clone();
// Add cache headers
const cachedRes = new Response(responseClone.body, responseClone);
cachedRes.headers.set(
"Cache-Control",
"public, max-age=120, stale-while-revalidate=60"
);
// Store in CDN cache
await caches.default.put(cacheKey, cachedRes.clone());
return cachedRes;
}由于Intelligent Search直接从前端调用,可使用CDN(如Cloudflare、CloudFront、Fastly)在边缘节点缓存响应。配置CDN以遵循头或为搜索API路径设置自定义缓存规则。
Cache-Controltypescript
// 若使用CDN Worker/边缘函数添加缓存头:
// cloudflare-worker.ts或类似边缘函数
async function handleSearchRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
// 仅缓存Intelligent Search的GET请求
if (request.method !== "GET") {
return fetch(request);
}
// 先检查CDN缓存
const cacheKey = new Request(url.toString(), request);
const cachedResponse = await caches.default.match(cacheKey);
if (cachedResponse) {
return cachedResponse;
}
// 从VTEX拉取数据
const response = await fetch(request);
const responseClone = response.clone();
// 添加缓存头
const cachedRes = new Response(responseClone.body, responseClone);
cachedRes.headers.set(
"Cache-Control",
"public, max-age=120, stale-while-revalidate=60"
);
// 存储到CDN缓存
await caches.default.put(cacheKey, cachedRes.clone());
return cachedRes;
}Step 2: Implement BFF Cache Layer with Redis
步骤2:基于Redis实现BFF缓存层
For catalog data proxied through the BFF, use Redis as a shared cache that persists across BFF restarts and is shared across multiple instances.
typescript
// server/cache/redis-cache.ts
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);
interface CacheOptions {
ttlSeconds: number;
swrSeconds?: number;
}
export async function getCachedOrFetch<T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions
): Promise<{ data: T; cacheStatus: "HIT" | "STALE" | "MISS" }> {
const { ttlSeconds, swrSeconds = 0 } = options;
// Try to get from cache
const cached = await redis.get(key);
if (cached) {
const ttl = await redis.ttl(key);
const isStale = ttl <= swrSeconds;
if (isStale && swrSeconds > 0) {
// Serve stale, refresh in background
fetcher()
.then((freshData) =>
redis.setex(key, ttlSeconds + swrSeconds, JSON.stringify(freshData))
)
.catch((err) => console.error(`Background refresh failed for ${key}:`, err));
return { data: JSON.parse(cached) as T, cacheStatus: "STALE" };
}
return { data: JSON.parse(cached) as T, cacheStatus: "HIT" };
}
// Cache miss — fetch and store
const data = await fetcher();
await redis.setex(key, ttlSeconds + swrSeconds, JSON.stringify(data));
return { data, cacheStatus: "MISS" };
}
export async function invalidateCache(pattern: string): Promise<number> {
const keys = await redis.keys(pattern);
if (keys.length === 0) return 0;
return redis.del(...keys);
}对于通过BFF代理的商品目录数据,使用Redis作为跨BFF实例共享的持久化缓存。
typescript
// server/cache/redis-cache.ts
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);
interface CacheOptions {
ttlSeconds: number;
swrSeconds?: number;
}
export async function getCachedOrFetch<T>(
key: string,
fetcher: () => Promise<T>,
options: CacheOptions
): Promise<{ data: T; cacheStatus: "HIT" | "STALE" | "MISS" }> {
const { ttlSeconds, swrSeconds = 0 } = options;
// 尝试从缓存获取
const cached = await redis.get(key);
if (cached) {
const ttl = await redis.ttl(key);
const isStale = ttl <= swrSeconds;
if (isStale && swrSeconds > 0) {
// 返回过期数据,后台刷新缓存
fetcher()
.then((freshData) =>
redis.setex(key, ttlSeconds + swrSeconds, JSON.stringify(freshData))
)
.catch((err) => console.error(`后台刷新缓存${key}失败:`, err));
return { data: JSON.parse(cached) as T, cacheStatus: "STALE" };
}
return { data: JSON.parse(cached) as T, cacheStatus: "HIT" };
}
// 缓存未命中 — 拉取数据并存储
const data = await fetcher();
await redis.setex(key, ttlSeconds + swrSeconds, JSON.stringify(data));
return { data, cacheStatus: "MISS" };
}
export async function invalidateCache(pattern: string): Promise<number> {
const keys = await redis.keys(pattern);
if (keys.length === 0) return 0;
return redis.del(...keys);
}Step 3: Apply Caching to BFF Routes Selectively
步骤3:为BFF路由选择性应用缓存
Only cache public, read-only data. Never cache checkout, profile, or order data.
typescript
// server/routes/catalog.ts
import { Router, Request, Response } from "express";
import { getCachedOrFetch, invalidateCache } from "../cache/redis-cache";
const router = Router();
const VTEX_ACCOUNT = process.env.VTEX_ACCOUNT!;
const VTEX_BASE = `https://${VTEX_ACCOUNT}.vtexcommercestable.com.br`;
// Category tree — long cache, changes rarely
router.get("/categories", async (_req: Request, res: Response) => {
try {
const result = await getCachedOrFetch(
"catalog:categories",
async () => {
const response = await fetch(`${VTEX_BASE}/api/catalog_system/pub/category/tree/3`);
return response.json();
},
{ ttlSeconds: 900, swrSeconds: 300 } // 15 min cache, 5 min SWR
);
res.set("X-Cache", result.cacheStatus);
res.json(result.data);
} catch (error) {
console.error("Error fetching categories:", error);
res.status(500).json({ error: "Failed to fetch categories" });
}
});
// Product details — moderate cache
router.get("/products/:productId", async (req: Request, res: Response) => {
const { productId } = req.params;
if (!/^\d+$/.test(productId)) {
return res.status(400).json({ error: "Invalid product ID" });
}
try {
const result = await getCachedOrFetch(
`catalog:product:${productId}`,
async () => {
const response = await fetch(
`${VTEX_BASE}/api/catalog_system/pub/products/search?fq=productId:${productId}`
);
return response.json();
},
{ ttlSeconds: 300, swrSeconds: 60 } // 5 min cache, 1 min SWR
);
res.set("X-Cache", result.cacheStatus);
res.json(result.data);
} catch (error) {
console.error("Error fetching product:", error);
res.status(500).json({ error: "Failed to fetch product" });
}
});
// Cart simulation — very short cache (same cart config may be checked by many users)
router.post("/simulation", async (req: Request, res: Response) => {
const cacheKey = `catalog:simulation:${JSON.stringify(req.body)}`;
try {
const result = await getCachedOrFetch(
cacheKey,
async () => {
const response = await fetch(
`${VTEX_BASE}/api/checkout/pub/orderForms/simulation`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req.body),
}
);
return response.json();
},
{ ttlSeconds: 30, swrSeconds: 10 } // 30 sec cache, 10 sec SWR
);
res.set("X-Cache", result.cacheStatus);
res.json(result.data);
} catch (error) {
console.error("Error simulating cart:", error);
res.status(500).json({ error: "Failed to simulate cart" });
}
});
// Webhook for catalog changes — invalidates affected cache
router.post("/webhooks/catalog", async (req: Request, res: Response) => {
const { productId } = req.body as { productId?: string };
if (productId) {
await invalidateCache(`catalog:product:${productId}`);
}
// Invalidate category tree on any catalog change
await invalidateCache("catalog:categories");
res.status(200).json({ received: true });
});
export default router;仅缓存公开的只读数据,绝不缓存结账、用户档案或订单数据。
typescript
// server/routes/catalog.ts
import { Router, Request, Response } from "express";
import { getCachedOrFetch, invalidateCache } from "../cache/redis-cache";
const router = Router();
const VTEX_ACCOUNT = process.env.VTEX_ACCOUNT!;
const VTEX_BASE = `https://${VTEX_ACCOUNT}.vtexcommercestable.com.br`;
// 类目树 — 缓存时间长,变更频率低
router.get("/categories", async (_req: Request, res: Response) => {
try {
const result = await getCachedOrFetch(
"catalog:categories",
async () => {
const response = await fetch(`${VTEX_BASE}/api/catalog_system/pub/category/tree/3`);
return response.json();
},
{ ttlSeconds: 900, swrSeconds: 300 } // 15分钟缓存,5分钟SWR
);
res.set("X-Cache", result.cacheStatus);
res.json(result.data);
} catch (error) {
console.error("拉取类目数据失败:", error);
res.status(500).json({ error: "拉取类目数据失败" });
}
});
// 商品详情 — 中等缓存时间
router.get("/products/:productId", async (req: Request, res: Response) => {
const { productId } = req.params;
if (!/^\d+$/.test(productId)) {
return res.status(400).json({ error: "无效的商品ID" });
}
try {
const result = await getCachedOrFetch(
`catalog:product:${productId}`,
async () => {
const response = await fetch(
`${VTEX_BASE}/api/catalog_system/pub/products/search?fq=productId:${productId}`
);
return response.json();
},
{ ttlSeconds: 300, swrSeconds: 60 } // 5分钟缓存,1分钟SWR
);
res.set("X-Cache", result.cacheStatus);
res.json(result.data);
} catch (error) {
console.error("拉取商品数据失败:", error);
res.status(500).json({ error: "拉取商品数据失败" });
}
});
// 购物车模拟 — 缓存时间极短(相同购物车配置可能被多个用户查询)
router.post("/simulation", async (req: Request, res: Response) => {
const cacheKey = `catalog:simulation:${JSON.stringify(req.body)}`;
try {
const result = await getCachedOrFetch(
cacheKey,
async () => {
const response = await fetch(
`${VTEX_BASE}/api/checkout/pub/orderForms/simulation`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req.body),
}
);
return response.json();
},
{ ttlSeconds: 30, swrSeconds: 10 } // 30秒缓存,10秒SWR
);
res.set("X-Cache", result.cacheStatus);
res.json(result.data);
} catch (error) {
console.error("模拟购物车失败:", error);
res.status(500).json({ error: "模拟购物车失败" });
}
});
// 商品目录变更Webhook — 失效受影响的缓存
router.post("/webhooks/catalog", async (req: Request, res: Response) => {
const { productId } = req.body as { productId?: string };
if (productId) {
await invalidateCache(`catalog:product:${productId}`);
}
// 任何商品目录变更时,失效类目树缓存
await invalidateCache("catalog:categories");
res.status(200).json({ received: true });
});
export default router;Complete Example
完整示例
Full caching setup with CDN headers, BFF cache, and no-cache enforcement for transactional routes:
typescript
// server/middleware/cache-headers.ts
import { Request, Response, NextFunction } from "express";
// Middleware to set appropriate cache headers based on route type
export function cacheHeaders(type: "public" | "private" | "no-cache") {
return (_req: Request, res: Response, next: NextFunction) => {
switch (type) {
case "public":
res.set({
"Cache-Control": "public, max-age=120, stale-while-revalidate=60",
Vary: "Accept-Encoding",
});
break;
case "private":
res.set({
"Cache-Control": "private, max-age=60",
Vary: "Accept-Encoding, Cookie",
});
break;
case "no-cache":
res.set({
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
Pragma: "no-cache",
Expires: "0",
"Surrogate-Control": "no-store",
});
break;
}
next();
};
}
// server/index.ts — apply cache strategies per route group
import express from "express";
import { cacheHeaders } from "./middleware/cache-headers";
import catalogRoutes from "./routes/catalog";
import { checkoutRoutes } from "./routes/checkout";
import { orderRoutes } from "./routes/orders";
import { profileRoutes } from "./routes/profile";
const app = express();
app.use(express.json());
// Cacheable routes — public catalog data
app.use("/api/bff/catalog", cacheHeaders("public"), catalogRoutes);
// Non-cacheable routes — transactional and personal data
app.use("/api/bff/checkout", cacheHeaders("no-cache"), checkoutRoutes);
app.use("/api/bff/orders", cacheHeaders("no-cache"), orderRoutes);
app.use("/api/bff/profile", cacheHeaders("no-cache"), profileRoutes);
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`BFF server running on port ${PORT}`);
});包含CDN头、BFF缓存和事务性路由禁止缓存的完整缓存配置:
typescript
// server/middleware/cache-headers.ts
import { Request, Response, NextFunction } from "express";
// 根据路由类型设置合适缓存头的中间件
export function cacheHeaders(type: "public" | "private" | "no-cache") {
return (_req: Request, res: Response, next: NextFunction) => {
switch (type) {
case "public":
res.set({
"Cache-Control": "public, max-age=120, stale-while-revalidate=60",
Vary: "Accept-Encoding",
});
break;
case "private":
res.set({
"Cache-Control": "private, max-age=60",
Vary: "Accept-Encoding, Cookie",
});
break;
case "no-cache":
res.set({
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
Pragma: "no-cache",
Expires: "0",
"Surrogate-Control": "no-store",
});
break;
}
next();
};
}
// server/index.ts — 为不同路由组应用缓存策略
import express from "express";
import { cacheHeaders } from "./middleware/cache-headers";
import catalogRoutes from "./routes/catalog";
import { checkoutRoutes } from "./routes/checkout";
import { orderRoutes } from "./routes/orders";
import { profileRoutes } from "./routes/profile";
const app = express();
app.use(express.json());
// 可缓存路由 — 公开商品目录数据
app.use("/api/bff/catalog", cacheHeaders("public"), catalogRoutes);
// 不可缓存路由 — 事务性和个性化数据
app.use("/api/bff/checkout", cacheHeaders("no-cache"), checkoutRoutes);
app.use("/api/bff/orders", cacheHeaders("no-cache"), orderRoutes);
app.use("/api/bff/profile", cacheHeaders("no-cache"), profileRoutes);
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`BFF服务器运行在端口${PORT}`);
});Anti-Patterns
反模式
Common mistakes developers make and how to fix them.
开发者常犯的错误及修复方案。
Anti-Pattern: Caching Based on Session or User Identity
反模式:基于会话或用户身份缓存
What happens: Developers create per-user caches for catalog data (e.g., caching product search results keyed by user ID).
Why it fails: Catalog data is the same for all anonymous users in the same trade policy. Creating per-user cache entries multiplies storage requirements by the number of users and eliminates the primary benefit of caching (serving the same response to many users). A store with 50,000 users and 1,000 unique searches would create 50 million cache entries instead of 1,000.
Fix: Cache public API responses by request URL/params only, not by user. Only skip cache or add user context for personalized pricing scenarios tied to specific trade policies.
typescript
// Cache key based on request parameters only — not user identity
function buildCacheKey(path: string, params: Record<string, string>): string {
const sortedParams = Object.entries(params)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}=${v}`)
.join("&");
return `search:${path}:${sortedParams}`;
}
// For trade-policy-specific pricing, include trade policy (not user ID)
function buildTradePolicyCacheKey(path: string, params: Record<string, string>, tradePolicy: string): string {
return `search:tp${tradePolicy}:${path}:${new URLSearchParams(params).toString()}`;
}问题表现:开发者为商品目录数据创建基于用户ID的缓存(如按用户ID缓存商品搜索结果)。
失败原因:商品目录数据对于同一贸易政策下的所有匿名用户是相同的。基于用户身份创建缓存条目会将存储需求乘以用户数量,完全抵消缓存的核心优势(为多用户提供相同响应)。一个拥有50000用户和1000个唯一搜索请求的店铺,会生成5000万条缓存条目,而非1000条。
修复方案:仅按请求URL/参数缓存公开API响应,不按用户身份缓存。仅在与特定贸易政策绑定的个性化定价场景下,才添加贸易政策信息(而非用户ID)到缓存键中。
typescript
// 仅基于请求参数构建缓存键 — 不包含用户身份
function buildCacheKey(path: string, params: Record<string, string>): string {
const sortedParams = Object.entries(params)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}=${v}`)
.join("&");
return `search:${path}:${sortedParams}`;
}
// 针对特定贸易政策的定价,缓存键包含贸易政策(而非用户ID)
function buildTradePolicyCacheKey(path: string, params: Record<string, string>, tradePolicy: string): string {
return `search:tp${tradePolicy}:${path}:${new URLSearchParams(params).toString()}`;
}Anti-Pattern: Setting Extremely Long Cache TTLs Without Invalidation
反模式:设置极长缓存TTL且无失效机制
What happens: Developers set cache TTLs of hours or days to maximize cache hit rates, but provide no invalidation mechanism.
Why it fails: Long TTLs mean that price changes, stock updates, and new product launches are invisible to shoppers for hours or days. A product that sells out continues to appear available. A flash sale price doesn't take effect until the cache expires. This leads to failed orders, customer frustration, and potential legal issues with displayed pricing.
Fix: Use moderate TTLs (2-15 minutes for search, 5-15 minutes for catalog) combined with event-driven invalidation. The pattern allows instant responses while still checking for fresh data regularly.
stale-while-revalidatetypescript
// Moderate TTL with stale-while-revalidate — balances freshness and performance
const CACHE_CONFIG = {
search: { ttlSeconds: 120, swrSeconds: 60 }, // 2 min + 1 min SWR
categories: { ttlSeconds: 900, swrSeconds: 300 }, // 15 min + 5 min SWR
product: { ttlSeconds: 300, swrSeconds: 60 }, // 5 min + 1 min SWR
topSearches: { ttlSeconds: 600, swrSeconds: 120 }, // 10 min + 2 min SWR
} as const;问题表现:开发者为最大化缓存命中率,将TTL设置为数小时或数天,但未提供任何失效机制。
失败原因:长TTL意味着价格变更、库存更新和新品上架需数小时或数天才能展示给用户。售罄的商品仍显示为有货,闪购价需等到缓存过期才生效。这会导致订单失败、用户不满,甚至因定价展示错误引发法律问题。
修复方案:使用中等长度的TTL(搜索API为2-15分钟,商品目录为5-15分钟),并结合事件驱动的失效机制。模式可在提供即时响应的同时,定期检查最新数据。
stale-while-revalidatetypescript
// 中等TTL + stale-while-revalidate — 平衡数据新鲜度与性能
const CACHE_CONFIG = {
search: { ttlSeconds: 120, swrSeconds: 60 }, // 2分钟缓存 + 1分钟SWR
categories: { ttlSeconds: 900, swrSeconds: 300 }, // 15分钟缓存 + 5分钟SWR
product: { ttlSeconds: 300, swrSeconds: 60 }, // 5分钟缓存 + 1分钟SWR
topSearches: { ttlSeconds: 600, swrSeconds: 120 }, // 10分钟缓存 + 2分钟SWR
} as const;Anti-Pattern: No Cache Monitoring or Observability
反模式:无缓存监控或可观测性
What happens: Developers implement caching but have no way to measure cache hit rates, miss rates, or stale-serve rates.
Why it fails: Without monitoring, you cannot tell if caching is effective, if TTLs are appropriate, or if cache invalidation is working. A cache with a 5% hit rate provides almost no benefit while adding complexity. A cache that never invalidates may be serving stale data without anyone noticing.
Fix: Add cache status headers and logging to track hit/miss/stale rates. Monitor these metrics in your observability platform.
typescript
// Add cache observability to every cached response
import { Request, Response, NextFunction } from "express";
interface CacheMetrics {
hits: number;
misses: number;
stale: number;
}
const metrics: CacheMetrics = { hits: 0, misses: 0, stale: 0 };
export function trackCacheMetrics(req: Request, res: Response, next: NextFunction): void {
const originalJson = res.json.bind(res);
res.json = function (body: unknown) {
const cacheStatus = res.getHeader("X-Cache") as string;
if (cacheStatus === "HIT") metrics.hits++;
else if (cacheStatus === "MISS") metrics.misses++;
else if (cacheStatus === "STALE") metrics.stale++;
return originalJson(body);
};
next();
}
// Expose metrics endpoint for monitoring
export function getCacheMetrics(): CacheMetrics & { hitRate: string } {
const total = metrics.hits + metrics.misses + metrics.stale;
const hitRate = total > 0 ? ((metrics.hits / total) * 100).toFixed(1) + "%" : "N/A";
return { ...metrics, hitRate };
}问题表现:开发者实现了缓存,但无法衡量缓存命中率、未命中率或过期数据返回率。
失败原因:无监控的情况下,无法判断缓存是否有效、TTL设置是否合理,或缓存失效机制是否正常工作。命中率仅5%的缓存几乎无任何收益,却增加了复杂度;永久不失效的缓存可能在无人察觉的情况下返回过期数据。
修复方案:添加缓存状态头和日志,以追踪命中/未命中/过期数据返回的频次。在可观测平台中监控这些指标。
typescript
// 为所有缓存响应添加可观测性
import { Request, Response, NextFunction } from "express";
interface CacheMetrics {
hits: number;
misses: number;
stale: number;
}
const metrics: CacheMetrics = { hits: 0, misses: 0, stale: 0 };
export function trackCacheMetrics(req: Request, res: Response, next: NextFunction): void {
const originalJson = res.json.bind(res);
res.json = function (body: unknown) {
const cacheStatus = res.getHeader("X-Cache") as string;
if (cacheStatus === "HIT") metrics.hits++;
else if (cacheStatus === "MISS") metrics.misses++;
else if (cacheStatus === "STALE") metrics.stale++;
return originalJson(body);
};
next();
}
// 暴露指标端点供监控使用
export function getCacheMetrics(): CacheMetrics & { hitRate: string } {
const total = metrics.hits + metrics.misses + metrics.stale;
const hitRate = total > 0 ? ((metrics.hits / total) * 100).toFixed(1) + "%" : "N/A";
return { ...metrics, hitRate };
}Reference
参考资料
Links to VTEX documentation and related resources.
- How the cache works — VTEX native caching behavior and cache layer architecture
- Cloud infrastructure — VTEX CDN, router, and caching infrastructure overview
- Best practices for avoiding rate limit errors — Caching as a strategy to avoid API rate limits
- Implementing cache in GraphQL APIs for IO apps — Cache patterns for VTEX IO (useful reference for cache scope concepts)
- Intelligent Search API — The primary cacheable API for headless storefronts
- Headless commerce overview — General architecture for headless VTEX stores
VTEX官方文档及相关资源链接。
- 缓存工作原理 — VTEX原生缓存行为及缓存层架构
- 云基础设施 — VTEX CDN、路由及缓存基础设施概述
- 避免频次限制错误的最佳实践 — 缓存作为避免API频次限制的策略
- 为IO应用的GraphQL API实现缓存 — 缓存范围概念的参考资料
- Intelligent Search API — 无头店铺前端的核心可缓存API
- 无头电商概述 — 无头VTEX店铺的通用架构