vtex-io-application-performance

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

VTEX IO application performance

VTEX IO应用性能

When this skill applies

本技能适用场景

Use this skill when you optimize VTEX IO backends (typically Node with
@vtex/api
/ Koa-style middleware, or .NET services) for performance and resilience: caching, deduplicating work, parallel I/O, and efficient configuration loading—not only “add a cache.”
  • Adding an in-memory LRU (per pod) for hot keys
  • Adding VBase persistence for shared cache across pods, optionally with stale-while-revalidate (return stale, refresh in background)
  • Loading AppSettings (or similar) once at startup or on a TTL refresh vs every request
  • Parallelizing independent client calls (
    Promise.all
    ) instead of serial waterfalls
  • Passing
    ctx.clients
    (e.g.
    vbase
    ) into client helpers or resolvers so caches are testable and explicit
Do not use this skill for:
  • Choosing
    /_v/private
    vs public paths or
    Cache-Control
    at the edge → vtex-io-service-paths-and-cdn
  • GraphQL
    @cacheControl
    field semantics only → vtex-io-graphql-api
当你优化VTEX IO后端(通常是搭载
@vtex/api
/Koa风格中间件的Node服务,或是**.NET服务)的性能与弹性时可使用本技能:涉及缓存**、工作去重并行I/O高效配置加载,而不仅仅是「添加一个缓存」这么简单。
  • 为热键添加(每个Pod独立的)内存LRU缓存
  • 引入VBase持久化实现Pod间共享缓存,可选搭配stale-while-revalidate策略(先返回过期数据,后台异步刷新)
  • 启动时一次性加载AppSettings(或同类配置)或按TTL刷新,而非每次请求都重新加载
  • Promise.all
    并行执行独立的客户端调用,而非串行瀑布式调用
  • 将**
    ctx.clients
    (例如
    vbase
    )传入
    客户端工具函数**或解析器,让缓存可测试、依赖关系明确
本技能不适用于以下场景:
  • 选择边缘层的**
    /_v/private
    路径和公开路径、或是配置边缘层
    Cache-Control
    ** → 请参考vtex-io-service-paths-and-cdn技能
  • 仅涉及GraphQL
    @cacheControl
    字段语义的场景 → 请参考vtex-io-graphql-api
    技能

Decision rules

决策规则

  • Layer 1 — LRU (in-process) — Fastest; lost on cold start and not shared across replicas. Use bounded size + TTL for hot keys (organization, cost center, small config slices).
  • Layer 2 — VBaseShared across pods; platform data is partitioned by account / workspace like other IO resources. Pair with hash or
    trySaveIfhashMatches
    when the client supports concurrency-safe updates (see Clients).
  • Stale-while-revalidate — On VBase hit with expired freshness, return stale immediately and revalidate asynchronously (fetch origin → write VBase + LRU). Reduces tail latency vs blocking on origin every time.
  • TTL-only — Simpler: cache until TTL expires, then blocking fetch. Prefer when staleness is unacceptable or origin is cheap.
  • AppSettings — If values are account-wide and rarely change, load once (or refresh on interval) and hold in module memory; if workspace-dependent or must reflect admin changes quickly, use per-request read or short TTL cache. Never cache secrets in logs or global state without guardrails.
  • Context — Use
    ctx.state
    for per-request deduplication (e.g. “already loaded org for this request”). Use global module cache only for immutable or TTL-refreshed app data; account and workspace live on
    ctx.vtex
    —always include them in in-memory cache keys when the same pod serves multiple tenants.
  • Parallel requests — When resolvers need independent upstream calls, run them in parallel; combine only when outputs depend on each other.
  • Timeouts on every outbound call — Every
    ctx.clients
    call and external HTTP request must have an explicit timeout. Use
    @vtex/api
    client options (
    timeout
    ,
    retries
    ,
    exponentialTimeoutCoefficient
    ) to tune per-client behavior. Unbounded waits are the top cause of cascading failures in distributed systems.
  • Graceful degradation — When an upstream is slow or down, fail open where the business allows (return cached/default data, skip optional enrichment) rather than blocking the response. Consider circuit breaker patterns for chronically failing dependencies.
  • Never cache real-time transactional stateOrder forms, cart simulations, payment responses, full session state, and commitment pricing must never be served from cache. They reflect live, mutable state that changes on every interaction. Caching these creates stale prices, phantom inventory, or duplicate charges.
  • Resolver chain deduplication — When a resolver chain calls the same client method multiple times (e.g.
    getCostCenter
    in the resolver and again inside a helper), deduplicate: call once, pass the result through, or stash in
    ctx.state
    . Serial waterfalls of 7+ calls that could be 3 parallel + 1 sequential are the top performance sink.
  • Phased
    Promise.all
    — Group independent calls into parallel phases. Phase 1:
    Promise.all([getOrderForm(), getCostCenter(), getSession()])
    . Phase 2 (depends on Phase 1):
    getSkuMetadata()
    . Phase 3 (depends on Phase 2):
    generatePrice()
    . Never
    await
    six calls sequentially when only two depend on each other.
  • Batch mutations — When setting multiple values (e.g.
    setManualPrice
    per cart item), use
    Promise.all
    instead of a sequential loop. Each
    await
    in a loop adds a full round-trip.
  • 第一层 — 进程内LRU缓存 — 速度最快;冷启动时会丢失数据,且不会在多个副本间共享。为热键(组织信息、成本中心、小型配置分片)设置容量上限+TTL即可。
  • 第二层 — VBase — 可在Pod间共享;平台数据和其他IO资源一样按账号/工作空间分区。当客户端支持并发安全更新时,可搭配哈希值或
    trySaveIfhashMatches
    使用(参考Clients文档)。
  • stale-while-revalidate策略 — 当VBase命中但数据新鲜度已过期时,立即返回过期数据,同时异步刷新(拉取源站数据 → 写入VBase和LRU)。和每次都阻塞等待源站响应的方案相比,可大幅降低长尾延迟。
  • 仅TTL策略 — 更简单:缓存到TTL过期后,阻塞式拉取新数据。如果业务无法接受数据过期、或是源站请求成本很低时优先使用该策略。
  • AppSettings加载规则 — 如果配置是账号全局很少变更,则一次性加载(或按间隔刷新)并保存在模块内存中;如果配置是和工作空间相关、或是必须快速响应后台变更,则每次请求都读取或使用短TTL缓存。严禁在没有防护措施的情况下将密钥缓存到日志或全局状态中。
  • 上下文使用规则 — 使用**
    ctx.state
    实现单次请求内的去重(例如「本次请求已经加载过组织信息了」)。仅对不可变或按TTL刷新的应用数据使用全局模块缓存;账号工作空间信息存储在
    ctx.vtex
    中——当同一个Pod服务多个租户时,一定要把这两个字段加入内存**缓存的键中。
  • 并行请求规则 — 当解析器需要调用多个独立的上游接口时,并行执行;只有当输出存在依赖关系时才串行执行。
  • 所有对外调用都要设置超时 — 每个
    ctx.clients
    调用和外部HTTP请求必须设置明确的超时时间。使用
    @vtex/api
    的客户端选项(
    timeout
    retries
    exponentialTimeoutCoefficient
    )为每个客户端调整行为。无限制等待是分布式系统级联故障的首要原因。
  • 优雅降级规则 — 当上游响应缓慢或宕机时,在业务允许的情况下熔断开放(返回缓存/默认数据、跳过非必要的信息补充),而非阻塞响应。对长期故障的依赖可以考虑使用断路器模式。
  • 严禁缓存实时交易状态订单表单购物车模拟结果支付响应完整会话状态承诺定价绝对不能从缓存返回,它们反映的是每次交互都会变化的实时可变状态。缓存这类数据会导致价格过期、库存幻读或是重复扣费。
  • 解析器链去重规则 — 当解析器链多次调用同一个客户端方法时(例如在解析器里调用了
    getCostCenter
    ,在工具函数里又调用了一次),去重处理:调用一次后将结果传递下去,或是暂存在
    ctx.state
    中。原本可以优化为3个并行+1个串行的调用,却写成7+次串行瀑布调用,是性能损耗的首要原因。
  • 分阶段
    Promise.all
    规则
    — 将独立的调用分组为并行阶段。第一阶段:
    Promise.all([getOrderForm(), getCostCenter(), getSession()])
    ;第二阶段(依赖第一阶段结果):
    getSkuMetadata()
    ;第三阶段(依赖第二阶段结果):
    generatePrice()
    。如果只有两个调用存在依赖关系,绝对不要串行等待六个调用完成。
  • 批量变更规则 — 当需要设置多个值时(例如为每个购物车项调用
    setManualPrice
    ),使用
    Promise.all
    而非串行循环。循环里的每个
    await
    都会增加一次完整的往返耗时。

VBase deep patterns

VBase深度模式

  • Per-entity keys, not blob keys — Cache individual entities (e.g.
    sku:{region}:{skuId}
    ) instead of composite blobs (e.g.
    allSkus:{sortedCartSkuIds}
    ). Per-entity keys dramatically increase cache hit rates when items are added/removed.
  • Minimal DTOs — Store only the fields the consumer needs (e.g.
    { skuId, mappedId, isSpecialItem }
    at ~50 bytes) instead of the full API response (~10-50 KB per product). Reduces VBase storage, serialization time, and transfer size.
  • Sibling prewarming — When a search API returns a product with 4 SKU variants, cache all 4 individual SKUs even if only 1 was requested. The next request for a sibling is a VBase hit instead of an API call.
  • Pass
    vbase
    as a parameter
    — Clients don't have direct access to other clients. Pass
    ctx.clients.vbase
    as a parameter to client methods or utilities that need it. This keeps code testable and explicit about dependencies.
  • VBase state machines — For long-running operations (scans, imports, batch processing), use VBase as a state store with
    current-operation.json
    (lock + progress), heartbeat extensions, checkpoint/resume, and TTL-based lock expiry to prevent zombie locks.
  • 按实体设置键,而非大Blob键 — 缓存单个实体(例如
    sku:{region}:{skuId}
    ),而非复合Blob(例如
    allSkus:{sortedCartSkuIds}
    )。当条目新增/删除时,按实体设置的键可以大幅提升缓存命中率。
  • 最小化DTO — 仅存储消费者需要的字段(例如仅存
    { skuId, mappedId, isSpecialItem }
    ,约50字节),而非完整的API响应(每个产品约10-50KB)。可减少VBase存储、序列化时间和传输体积。
  • 同级数据预热 — 当搜索API返回的产品包含4个SKU变体时,即使只请求了1个,也要把4个SKU都缓存起来。下次请求同级SKU时就会命中VBase,无需再调用API。
  • vbase
    作为参数传递
    — 客户端无法直接访问其他客户端,将
    ctx.clients.vbase
    作为参数传给需要使用它的客户端方法或工具函数。这能保证代码可测试,依赖关系明确。
  • VBase状态机 — 对于长时间运行的操作(扫描、导入、批量处理),用VBase作为状态存储,配合
    current-operation.json
    (锁+进度)、心跳续期、断点续跑和基于TTL的锁过期机制,防止僵尸锁。

service.json
tuning

service.json
配置调优

  • timeout
    — Maximum seconds before the platform kills a request. Set based on the longest expected operation; do not leave at the default if your resolver calls slow upstreams.
  • memory
    — MB per worker. Increase if LRU caches or large payloads cause OOM; monitor actual usage before over-provisioning.
  • workers
    — Concurrent request handlers per replica. More workers handle more concurrent requests but each shares the memory budget and in-process LRU.
  • minReplicas
    /
    maxReplicas
    — Controls horizontal scaling. For payment-critical or high-throughput apps, set
    minReplicas >= 2
    so cold starts don't hit production traffic.
  • timeout
    — 平台终止请求前的最大等待秒数。根据预期的最长操作时间设置;如果你的解析器会调用慢上游,不要使用默认值。
  • memory
    — 每个Worker的内存上限(单位MB)。如果LRU缓存或大负载导致OOM则调高该值;过度配置前先监控实际使用量。
  • workers
    — 每个副本的并发请求处理数。Worker越多能处理的并发请求越多,但所有Worker共享内存预算和进程内LRU缓存。
  • minReplicas
    /
    maxReplicas
    — 控制水平扩缩容。对于支付关键型或高吞吐量应用,设置
    minReplicas >= 2
    ,避免冷启动影响生产流量。

Tenancy and in-memory caches

多租户与进程内缓存

IO runs per app version per shard, with pods shared across accounts: every request is still resolved in
{account, workspace}
context. VBase, app buckets, and related platform stores partition data by account/workspace. In-process LRU/module
Map
does not—you must key explicitly with
ctx.vtex.account
and
ctx.vtex.workspace
(plus entity id) so two consecutive requests for different accounts on the same pod cannot read each other’s entries.
IO按「每个分片每个应用版本」运行,Pod在多个账号间共享:每个请求仍然在**
{账号, 工作空间}
上下文下解析。VBase、应用存储桶和相关平台存储会按账号/工作空间分区数据**。但进程内LRU/模块
Map
不会自动分区——你必须显式将**
ctx.vtex.account
ctx.vtex.workspace
**(加上实体ID)加入键中,避免同一个Pod上连续两个不同账号的请求读取到对方的缓存条目。

Hard constraints

硬性约束

Constraint: Do not store sensitive or tenant-specific data in module-level caches without tenant keys

约束:如果没有租户键,不要在模块级缓存中存储敏感或租户专属数据

Global or module-level maps must not store PII, tokens, or authorization-sensitive blobs keyed only by user id or email without
account
and
workspace
(and any other dimension needed for isolation).
Why this matters — Pods are multi-tenant: the same process may serve many accounts in sequence. VBase and similar APIs are scoped to the current account/workspace, but an in-memory
Map
is your responsibility. Missing
account
/
workspace
in the key risks cross-tenant reads from warm cache.
Detection — A module-scope
Map
keyed only by
userId
or
email
; or cache keys that omit
ctx.vtex.account
/
ctx.vtex.workspace
when the value is tenant-specific.
Correct — Build keys from
ctx.vtex.account
,
ctx.vtex.workspace
, and the entity id; never store app tokens in VBase/LRU as plain cache values; prefer
ctx.clients
and platform auth.
typescript
// Pseudocode: in-memory key must mirror tenant scope (same pod, many accounts)
function cacheKey(ctx: Context, subjectId: string) {
  return `${ctx.vtex.account}:${ctx.vtex.workspace}:${subjectId}`;
}
Wrong
globalUserCache.set(email, profile)
keyed only by email, with no
account
/
workspace
segment—unsafe on shared pods even though a later VBase read would be account-scoped, because this map is not partitioned by the platform.
全局模块级的Map禁止仅用用户ID邮箱作为键存储PII令牌授权敏感Blob,必须加上**
account
workspace
**(以及其他隔离所需的维度)。
重要性 — Pod是多租户的:同一个进程可能按顺序服务很多个账号。VBase和同类API会自动限定在当前账号/工作空间范围,但内存
Map
的隔离是开发者的责任。键中缺少**
account
/
workspace
**会导致热缓存出现跨租户读取的风险。
检测方式 — 模块作用域的
Map
仅用
userId
email
作为键;或是当值是租户专属时,缓存键省略了
ctx.vtex.account
/
ctx.vtex.workspace
正确写法 — 用**
ctx.vtex.account
ctx.vtex.workspace
和实体ID构建键;永远不要将应用令牌作为纯缓存值存储在VBase/LRU中;优先使用
ctx.clients
平台**鉴权。
typescript
// 伪代码:内存缓存键必须镜像租户范围(同一个Pod服务多个账号)
function cacheKey(ctx: Context, subjectId: string) {
  return `${ctx.vtex.account}:${ctx.vtex.workspace}:${subjectId}`;
}
错误写法
globalUserCache.set(email, profile)
仅用email作为键,没有
account
/
workspace
分段——即使后续的VBase读取是账号范围的,在共享Pod上也是不安全的,因为这个Map没有被平台分区。

Constraint: Do not use fire-and-forget VBase writes in financial or idempotency-critical paths

约束:在金融或幂等关键路径中,不要使用发后即忘的VBase写入

When VBase serves as an idempotency store (e.g. payment connectors storing transaction state) or a data-integrity store, writes must be awaited. Fire-and-forget writes risk silent failure: a successful upstream operation (e.g. a charge) whose VBase record is lost causes a duplicate on the next retry.
Why this matters — VTEX Gateway retries payment calls with the same
paymentId
. If VBase write fails silently after a successful authorization, the connector cannot find the previous result and sends another payment request—causing a duplicate charge.
Detection — A VBase
saveJSON
or
saveOrUpdate
call without
await
in a payment, settlement, refund, or any flow where the stored value is the only record preventing re-execution.
Correct — Await the write; accept the latency cost for correctness.
typescript
// Critical path: await guarantees the idempotency record is persisted
await ctx.clients.vbase.saveJSON<Transaction>('transactions', paymentId, transactionData)
return Authorizations.approve(authorization, { ... })
Wrong — Fire-and-forget in a payment flow.
typescript
// No await — if this fails silently, the next retry creates a duplicate charge
ctx.clients.vbase.saveJSON('transactions', paymentId, transactionData)
return Authorizations.approve(authorization, { ... })
当VBase作为幂等存储(例如支付连接器存储交易状态)或数据完整性存储时,写入必须await。发后即忘的写入有静默失败的风险:上游操作(例如扣费)成功了,但VBase记录丢失,下次重试时会导致重复操作
重要性 — VTEX网关会用同一个`paymentId重试支付调用。如果授权成功后VBase写入静默失败,连接器找不到之前的结果,就会再次发送支付请求——导致重复扣费**。
检测方式 — 在支付、结算、退款、或是存储值是防止重复执行的唯一记录的流程中,VBase的
saveJSON
saveOrUpdate
调用没有加
await
正确写法 — 等待写入完成;为了正确性接受相应的延迟成本。
typescript
// 关键路径:await保证幂等记录已持久化
await ctx.clients.vbase.saveJSON<Transaction>('transactions', paymentId, transactionData)
return Authorizations.approve(authorization, { ... })
错误写法 — 支付流程中发后即忘。
typescript
// 没有await — 如果写入静默失败,下次重试会造成重复扣费
ctx.clients.vbase.saveJSON('transactions', paymentId, transactionData)
return Authorizations.approve(authorization, { ... })

Constraint: Do not cache real-time transactional data

约束:不要缓存实时交易数据

Order forms, cart simulation responses, payment statuses, full session state, and commitment prices must never be served from LRU, VBase, or any cache layer. They reflect live mutable state.
Why this matters — Serving a cached order form shows phantom items, stale prices, or wrong quantities. Caching payment responses could return a previous transaction's status for a different payment. Caching cart simulations returns stale availability and pricing.
Detection — LRU or VBase keys like
orderForm:{id}
,
cartSim:{hash}
,
paymentResponse:{id}
, or
session:{token}
used for read-through caching. Or a resolver that caches the result of
checkout.orderForm()
.
Correct — Always call the live API for transactional data; cache reference data (org, cost center, config, seller lists) around it.
typescript
// Reference data: cached (changes rarely)
const costCenter = await getCostCenterCached(ctx, costCenterId)
const sellerList = await getSellerListCached(ctx)

// Transactional data: always live
const orderForm = await ctx.clients.checkout.orderForm()
const simulation = await ctx.clients.checkout.simulation(payload)
Wrong — Caching the order form or cart simulation.
typescript
const cacheKey = `orderForm:${orderFormId}`
const cached = orderFormCache.get(cacheKey)
if (cached) return cached // Stale cart state served to user
订单表单购物车模拟响应支付状态完整会话状态承诺定价绝对不能从LRU、VBase或任何缓存层返回,它们反映的是实时可变状态。
重要性 — 返回缓存的订单表单会显示幻读商品、过期价格或错误数量。缓存支付响应可能会将之前交易的状态返回给其他支付请求。缓存购物车模拟结果会返回过期的库存和价格。
检测方式 — LRU或VBase键如
orderForm:{id}
cartSim:{hash}
paymentResponse:{id}
session:{token}
被用于读穿缓存;或是解析器缓存了
checkout.orderForm()
的结果。
正确写法 — 交易数据永远调用实时接口;只缓存交易数据周边的参考数据(组织、成本中心、配置、卖家列表)。
typescript
// 参考数据:可缓存(很少变更)
const costCenter = await getCostCenterCached(ctx, costCenterId)
const sellerList = await getSellerListCached(ctx)

// 交易数据:永远实时读取
const orderForm = await ctx.clients.checkout.orderForm()
const simulation = await ctx.clients.checkout.simulation(payload)
错误写法 — 缓存订单表单或购物车模拟结果。
typescript
const cacheKey = `orderForm:${orderFormId}`
const cached = orderFormCache.get(cacheKey)
if (cached) return cached // 向用户返回过期的购物车状态

Constraint: Do not block the purchase path on slow or unbounded cache refresh

约束:不要在购买路径上阻塞等待缓慢或无限制的缓存刷新

Stale-while-revalidate or origin calls must not add unbounded latency to checkout-critical middleware if the platform SLA requires a fast response.
Why this matters — Blocking checkout on optional enrichment breaks conversion and reliability.
Detection — A cart or payment resolver awaits VBase refresh or external API before returning; no timeout or fallback.
Correct — Return stale or default; enqueue refresh; fail open where business rules allow.
Wrong
await fetchHeavyPartner()
in the hot path with no timeout.
如果平台SLA要求快速响应,stale-while-revalidate源站调用绝对不能结账关键中间件增加无限制的延迟。
重要性 — 阻塞结账流程等待非必要的信息补充会破坏转化率和可靠性。
检测方式购物车支付解析器在返回前awaitVBase刷新或外部API调用;没有设置超时或降级方案
正确写法 — 返回过期默认数据;异步排队执行刷新;业务规则允许的情况下熔断开放。
错误写法 — 热路径中调用
await fetchHeavyPartner()
没有超时。

Preferred pattern

推荐模式

  1. Classify data: reference data (org, cost center, config, seller lists → cacheable) vs transactional data (order form, cart sim, payment → never cache) vs user-private (never in shared cache without encryption and keying).
  2. Choose LRU only, VBase only, or LRU → VBase → origin (two-layer) for read-heavy reference data.
  3. Deduplicate within a request: set
    ctx.state
    flags when a resolver chain might call the same loader twice.
  4. Parallelize independent
    ctx.clients
    calls in phased
    Promise.all
    groups.
  5. Per-entity VBase keys with minimal DTOs for high-cardinality data (SKUs, users, org records).
  6. Document TTLs and invalidation (who writes, when refresh runs).
  1. 数据分类参考数据(组织、成本中心、配置、卖家列表 → 可缓存)vs 交易数据(订单表单、购物车模拟、支付 → 绝对不能缓存)vs 用户隐私数据(未加密和正确键控的情况下不能存入共享缓存)。
  2. 缓存层选择:对读多写少的参考数据,选择仅LRU、仅VBase,或是LRU → VBase → 源站的两层架构。
  3. 请求内去重:当解析器链可能两次调用同一个加载器时,设置**
    ctx.state
    **标记。
  4. 分阶段并行调用:将独立的**
    ctx.clients
    **调用分组为分阶段的
    Promise.all
    并行执行。
  5. VBase按实体设键:高基数数据(SKU、用户、组织记录)使用最小化DTO,按实体设键。
  6. 文档记录:记录TTL和失效规则(谁负责写入、什么时候执行刷新)。

Resolver chain optimization (before/after)

解析器链优化(前后对比)

typescript
// BEFORE: 7 sequential awaits, 2 duplicate calls
const settings = await getAppSettings(ctx)          // 1
const session = await getSessions(ctx)               // 2
const costCenter = await getCostCenter(ctx, ccId)    // 3
const orderForm = await getOrderForm(ctx)            // 4
const skus = await getSkuById(ctx, skuIds)           // 5
const price = await generatePrice(ctx, costCenter)   // 6 (calls getCostCenter AGAIN + getSession AGAIN)
for (const item of items) {
  await setManualPrice(ctx, item)                    // 7, 8, 9... (sequential loop)
}

// AFTER: 3 phases, no duplicates, parallel mutations
const settings = await getAppSettings(ctx)
const [session, costCenter, orderForm] = await Promise.all([
  getSessions(ctx),
  getCostCenter(ctx, ccId),
  getOrderForm(ctx),
])
const skus = await getSkuMetadataBatch(ctx, skuIds)  // per-entity VBase cache
const price = await generatePrice(ctx, costCenter, session)  // reuse, no re-fetch
await Promise.all(items.map(item => setManualPrice(ctx, item)))
typescript
// 优化前:7次串行await,2次重复调用
const settings = await getAppSettings(ctx)          // 1
const session = await getSessions(ctx)               // 2
const costCenter = await getCostCenter(ctx, ccId)    // 3
const orderForm = await getOrderForm(ctx)            // 4
const skus = await getSkuById(ctx, skuIds)           // 5
const price = await generatePrice(ctx, costCenter)   // 6 (再次调用getCostCenter + 再次调用getSession)
for (const item of items) {
  await setManualPrice(ctx, item)                    // 7, 8, 9... (串行循环)
}

// 优化后:3个阶段,无重复调用,并行变更
const settings = await getAppSettings(ctx)
const [session, costCenter, orderForm] = await Promise.all([
  getSessions(ctx),
  getCostCenter(ctx, ccId),
  getOrderForm(ctx),
])
const skus = await getSkuMetadataBatch(ctx, skuIds)  // 按实体的VBase缓存
const price = await generatePrice(ctx, costCenter, session)  // 复用结果,无需重新拉取
await Promise.all(items.map(item => setManualPrice(ctx, item)))

Per-entity VBase caching

按实体的VBase缓存

typescript
interface SkuMetadata {
  skuId: string
  mappedSku: string | null
  isSpecialItem: boolean
}

async function getSkuMetadataBatch(
  ctx: Context,
  skuIds: string[],
): Promise<Map<string, SkuMetadata>> {
  const { vbase, search } = ctx.clients
  const results = new Map<string, SkuMetadata>()

  // Phase 1: check VBase for each SKU in parallel
  const lookups = await Promise.allSettled(
    skuIds.map(id => vbase.getJSON<SkuMetadata>('sku-metadata', `sku:${id}`))
  )

  const missing: string[] = []
  lookups.forEach((result, i) => {
    if (result.status === 'fulfilled' && result.value) {
      results.set(skuIds[i], result.value)
    } else {
      missing.push(skuIds[i])
    }
  })

  if (missing.length === 0) return results

  // Phase 2: fetch only missing SKUs from API
  const products = await search.getSkuById(missing)

  // Phase 3: cache ALL sibling SKUs (prewarm)
  for (const product of products) {
    for (const sku of product.items) {
      const metadata: SkuMetadata = {
        skuId: sku.itemId,
        mappedSku: extractMapping(sku),
        isSpecialItem: checkSpecial(sku),
      }
      results.set(sku.itemId, metadata)
      // Fire-and-forget for prewarming (not idempotency-critical)
      vbase.saveJSON('sku-metadata', `sku:${sku.itemId}`, metadata).catch(() => {})
    }
  }

  return results
}
typescript
interface SkuMetadata {
  skuId: string
  mappedSku: string | null
  isSpecialItem: boolean
}

async function getSkuMetadataBatch(
  ctx: Context,
  skuIds: string[],
): Promise<Map<string, SkuMetadata>> {
  const { vbase, search } = ctx.clients
  const results = new Map<string, SkuMetadata>()

  // 第一阶段:并行检查每个SKU的VBase缓存
  const lookups = await Promise.allSettled(
    skuIds.map(id => vbase.getJSON<SkuMetadata>('sku-metadata', `sku:${id}`))
  )

  const missing: string[] = []
  lookups.forEach((result, i) => {
    if (result.status === 'fulfilled' && result.value) {
      results.set(skuIds[i], result.value)
    } else {
      missing.push(skuIds[i])
    }
  })

  if (missing.length === 0) return results

  // 第二阶段:仅从API拉取缺失的SKU
  const products = await search.getSkuById(missing)

  // 第三阶段:缓存所有同级SKU(预热)
  for (const product of products) {
    for (const sku of product.items) {
      const metadata: SkuMetadata = {
        skuId: sku.itemId,
        mappedSku: extractMapping(sku),
        isSpecialItem: checkSpecial(sku),
      }
      results.set(sku.itemId, metadata)
      // 预热用发后即忘(非幂等关键场景)
      vbase.saveJSON('sku-metadata', `sku:${sku.itemId}`, metadata).catch(() => {})
    }
  }

  return results
}

Common failure modes

常见故障模式

  • LRU unbounded — Memory grows without max entries; pod OOM.
  • VBase without LRU — Every request hits VBase for hot keys; latency and cost rise.
  • In-memory cache without tenant in key — Same pod serves account A then B; stale or wrong row returned from module cache.
  • Serial awaits — Three independent Janus calls awaited one after another; total latency = sum of all instead of max.
  • Duplicate calls in resolver chains
    getCostCenter
    called in the resolver and again inside a helper;
    getSession
    called twice in the same flow. Each duplicate adds a full round-trip.
  • Blob VBase keys — Keying VBase by
    sortedCartSkuIds
    means adding 1 item to a cart of 10 requires a full re-fetch instead of 1 lookup.
  • Caching transactional data — Order forms, cart simulations, payment responses served from cache; stale prices, phantom items, or duplicate charges.
  • Fire-and-forget writes in critical paths — Unawaited VBase writes for idempotency stores; silent failure causes duplicates on retry.
  • No explicit timeouts — Relying on default or infinite timeouts for upstream calls; one slow dependency stalls the whole request chain.
  • Global mutable singletons — Module-level mutable objects (e.g. token cache metadata) modified by concurrent requests cause race conditions and incorrect behavior.
  • Treating AppSettings as real-timeStale admin change until TTL expires; no notification path.
  • console.log
    in hot paths
    — Logging full response objects with template literals produces
    [object Object]
    ; use
    ctx.vtex.logger
    with
    JSON.stringify
    and redact sensitive data.
  • LRU无容量限制 — 内存无限制增长,Pod发生OOM。
  • 只用VBase不用LRU — 每次请求热键都要访问VBase,延迟和成本上升。
  • 内存缓存键不含租户信息 — 同一个Pod先服务账号A再服务账号B,从模块缓存返回过期或错误的数据。
  • 串行await — 三个独立的Janus调用串行等待,总延迟等于所有调用耗时之和,而非最长的单次调用耗时。
  • 解析器链重复调用
    getCostCenter
    在解析器里调用一次,在工具函数里又调用一次;
    getSession
    在同一个流程里调用两次。每次重复调用都会增加一次完整的往返耗时。
  • VBase大Blob键 — 用
    sortedCartSkuIds
    作为VBase键,意味着10个商品的购物车加1个商品就要全量重新拉取,而非仅查询1次。
  • 缓存交易数据 — 订单表单、购物车模拟、支付响应从缓存返回,导致价格过期、商品幻读或重复扣费。
  • 关键路径发后即忘写入 — 幂等存储的VBase写入没有等待完成,静默失败导致重试时重复操作。
  • 无显式超时 — 上游调用依赖默认或无限超时,一个慢依赖会拖垮整个请求链。
  • 全局可变单例 — 模块级可变对象(例如令牌缓存元数据)被并发请求修改,导致竞态条件和错误行为。
  • 将AppSettings当作实时数据 — 后台变更要等到TTL过期才会生效,没有通知通道。
  • 热路径使用
    console.log
    — 用模板字符串打印完整响应对象会输出
    [object Object]
    ;使用
    ctx.vtex.logger
    搭配
    JSON.stringify
    ,并脱敏敏感数据。

Review checklist

评审检查清单

  • Are in-memory cache keys built with
    account
    +
    workspace
    (and entity id) when values are tenant-specific?
  • Is LRU bounded (max entries) and TTL defined?
  • For VBase, is stale-while-revalidate or TTL-only behavior explicit?
  • Are independent upstream calls parallelized (
    Promise.all
    phases) where safe?
  • Are there no duplicate calls to the same client method within a resolver chain?
  • Is AppSettings (or similar) loaded at the right frequency (once vs per request)?
  • Is checkout or payment path free of blocking optional refreshes?
  • Does every outbound call have an explicit timeout (via
    @vtex/api
    client options or equivalent)?
  • For unreliable upstreams, is there a degradation path (cached fallback, skip, circuit breaker)?
  • Are VBase writes awaited in financial or idempotency-critical paths?
  • Is transactional data (order form, cart sim, payment) always fetched live, never from cache?
  • Are VBase keys per-entity (not blob) for high-cardinality data like SKUs?
  • Are
    service.json
    resource limits (
    timeout
    ,
    memory
    ,
    workers
    ,
    replicas
    ) tuned for the workload?
  • Is logging done via
    ctx.vtex.logger
    (not
    console.log
    ) with sensitive data redacted?
  • 当值是租户专属时,内存缓存键是否由**
    account
    +
    workspace
    **(加上实体ID)构建?
  • LRU是否设置了容量上限(最大条目数)和TTL
  • 对于VBasestale-while-revalidate仅TTL行为是否明确?
  • 安全的情况下,独立的上游调用是否用**分阶段
    Promise.all
    **并行执行?
  • 同一个解析器链中是否没有对同一个客户端方法的重复调用?
  • AppSettings(或同类配置)的加载频率是否正确(一次性加载 vs 每次请求加载)?
  • 结账支付路径中是否没有阻塞式的非必要刷新?
  • 每个对外调用是否都有显式超时(通过
    @vtex/api
    客户端选项或等效方式设置)?
  • 对于不可靠的上游,是否有降级路径(缓存降级、跳过、断路器)?
  • 金融或幂等关键路径中的VBase写入是否都加了
    await
  • 交易数据(订单表单、购物车模拟、支付)是否永远实时拉取,从不从缓存返回?
  • 高基数数据(如SKU)的VBase键是否是按实体设置(而非大Blob)?
  • service.json
    的资源限制(
    timeout
    memory
    workers
    replicas
    )是否根据工作负载调优?
  • 是否通过
    ctx.vtex.logger
    (而非
    console.log
    )打印日志,且敏感数据已脱敏?

Related skills

相关技能

  • vtex-io-service-paths-and-cdn — Edge paths, cookies, CDN
  • vtex-io-session-apps — Session transforms (caching patterns apply inside transforms)
  • vtex-io-service-apps — Clients, middleware, Service
  • vtex-io-graphql-api — GraphQL caching
  • vtex-io-app-structure — Manifest, policies
  • vtex-io-service-paths-and-cdn — 边缘路径、Cookie、CDN
  • vtex-io-session-apps — 会话转换(缓存模式适用于转换逻辑内部)
  • vtex-io-service-apps — 客户端、中间件、服务
  • vtex-io-graphql-api — GraphQL缓存
  • vtex-io-app-structure — 清单、权限策略

Reference

参考资料