mcpField
S
spree-performance

spree-performance

github.com/spree开发10226 次安装4 周前更新
代码仓库

关于 spree-performance

spree-performance 是面向 Spree 6 开发者的性能诊断与优化技能,聚焦 Store API 热点、商品读取、缓存、搜索、图片和后台任务,并结合 Spree 工作流提供建议。

主要功能

  1. 01追踪工作流与外部调用
  2. 02减少购物车重复计算
  3. 03避免商品读取的 N+1 查询
  4. 04配置 Store API 缓存
  5. 05调整搜索、图片与任务队列

适用场景

  1. 01定位购物车或结账的慢步骤
  2. 02优化自定义商品接口
  3. 03缓存访客商品目录响应
  4. 04将批量任务与结账任务分队列

安装

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

$npx skills add https://github.com/spree/agent-skills --skill spree-performance

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

详细介绍

Spree 性能优化

Spree 6 采用 API 优先架构:性能热点主要是 Store API 请求(商品目录读取、购物车写入、结账)和 后台任务。大部分性能提升来自少数已知热点,而非通用的 Rails 调优。

先进行测量

  1. 追踪。 添加 gem 'spree_opentelemetry',并将 OTEL_EXPORTER_OTLP_ENDPOINT 指向一个收集器(本地运行 Jaeger:docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:latest)。每次 Spree::Workflow 运行中的每个步骤、钩子分发、事件订阅者分发、Webhook 投递和网关调用,都会与 SQL 和 HTTP span 一起生成各自的 span — 例如 carts.add_item、carts.complete process_payments。这样无需猜测,就能知道哪个步骤耗时较长。自定义工作流会自动追踪。
  2. 在控制台或请求 spec 中统计查询次数:设置 ActiveRecord::Base.logger = Logger.new($stdout),然后直接运行工作流。
  3. 生产环境 APM(Skylight、Scout、New Relic、Datadog)都可使用;Datadog/Honeycomb/New Relic 也可以采集 OTel 追踪数据。

购物车写入:重新计算流水线

购物车变更(Spree::Carts::AddItem、UpsertItems、行项目更新/移除以及其他购物车编辑)会运行 Spree.cart_recalculate_workflow(Spree::Carts::Recalculate),该工作流会:

  1. 通过 Spree.cart_recalculate_totals_workflow(Spree::Carts::RecalculateTotals)刷新总额,
  2. 重建配送方案(cart.ensure_updated_fulfillments — 配送费率提供方会在此处运行),
  3. 激活促销(set_promotion_context 钩子,然后是 Spree::PromotionHandler::Cart),
  4. 再次运行 RecalculateTotals,以重建折扣和税费,
  5. 运行 after_recalculate 钩子处理程序。

RecalculateTotals 会重新生成类型化行(Spree::Discount、Spree::Fee、Spree::TaxLine),并且每次在未下单的购物车上运行时,都会调用 cart.tax_provider.estimate(...)。由此会产生以下影响:

  • 每次购物车写入至少会调用两次外部税务提供方。 在提供方内部缓存估算结果(以行项目、数量、金额、收货地址、免税项为键),并将超时时间设得较短。已下单的订单金额会冻结,不会重新生成。
  • 外部配送费率提供方会在每次重建方案时运行 — 按(地址、包裹内容)缓存报价。
  • 钩子处理程序(carts.recalculate.after_recalculate、carts.recalculate.set_promotion_context、carts.recalculate_totals.set_tax_line_context)会在每次写入时运行。应尽量减少其中的查询;将非必要操作移至异步运行的事件订阅者(cart.updated)。
  • 绝不要在数据库事务中执行网络 I/O。在自己的工作流中,使用 external_step 进行网关/网络调用 — 如果在工作流事务中运行,它会抛出异常,并将该 span 标记为客户端调用。
  • 不要在 Spree::LineItem/Spree::Cart 上添加会触发重新计算的 after_save 回调 — 工作流已经会执行重新计算,而回调会让工作量成倍增加。

商品目录读取:N+1 查询

Store API 的 ResourceController#collection 已经串接 .includes(collection_includes).preload_associations_lazily(ar_lazy_preload),并通过 Pagy 分页(默认 limit 为 25,最大值为 100)。products 控制器会显式包含惰性预加载无法获取的内容:

# Spree::Api::V3::Store::ProductsController#scope_includes (abridged)
[:seller, {
  primary_media: [attachment_attachment: :blob, poster_attachment: :blob],
  default_variant: [:prices, stock_levels: [:stock_location, :active_stock_reservations]],
  variants: [:prices, :seller, stock_levels: [:stock_location, :active_stock_reservations]]
}]

构建自定义端点或序列化器时,请沿用这种结构:

  • 没有主变体。商品列表会读取 product.default_variant — 预加载它及其 :prices。
  • variant.price_in(currency) 在 prices 关联已加载时会使用它,否则会对每个变体执行一次查询。迭代前务必预加载 prices。
  • Spree::Media 有一个默认作用域,会包含 attachment 和 poster blob;商品列表应预加载 primary_media。
  • 向序列化器添加字段时 → 检查是否已预加载新增关联。在控制器中,重载 scope_includes / collection_includes,不要直接调用 .includes。
  • 客户端应仅对屏幕所需的内容使用 expand=,并使用 fields= 获取精简载荷 — 每次展开都会增加加载和序列化开销。
products = current_store.products.active
  .includes(primary_media: [], default_variant: [:prices])
  .preload_associations_lazily

Store API 上的 HTTP 缓存

Store 目录控制器(产品、分类、集合、市场、货币、区域设置、政策、卖家)包含 Spree::Api::V3::HttpCaching:

  • 访客会获得 Cache-Control: public(5 分钟,列表支持 stale-while-revalidate)以及一个 ETag;响应会根据 Accept, x-spree-currency, x-spree-locale, x-spree-channel 进行 Vary。列表 ETag 会纳入最大 updated_at、数量、expand、fields、q、页码、限制、货币、区域设置和渠道。
  • 已认证客户会获得 private, no-store。

因此,API 前面的 CDN 可以缓存访客的目录流量——请将其配置为遵循 Vary/这些标头,并确保店面发送 x-spree-* 标头,而非查询字符串变体。对于你自己的 Store API 控制器,请在 index/show 操作中调用 cache_collection(collection)/cache_resource(resource)(发送 304 后,两者都会返回 false)。购物车、结账和账户端点必须保持不缓存。

默认情况下,Rails.cache 使用 Solid Cache(由数据库提供支持)。用于缓存查找结果是合适的;如果缓存流量变得显著,请切换到 Redis/Valkey(redis_cache_store)。切勿使用共享键缓存客户专属数据。

搜索

  • 数据库提供程序(Spree::SearchProvider::Database,默认):文本搜索会对产品名称/SKU 执行前置通配符 LIKE 查询(以及定义被标记为可搜索的自定义字段),然后应用 Ransack 筛选条件。适用于小型目录;产品超过约 10K 个后性能会下降。在 PostgreSQL 上,核心迁移会启用 pg_trgm——如果添加三元组索引,请先对生成的 SQL 执行 EXPLAIN,确保索引与实际表达式匹配。
  • Meilisearch(spree_meilisearch gem,Spree.search_provider = 'SpreeMeilisearch::SearchProvider'):适合中大型目录以及分面搜索。
    • 每次产品创建/更新提交都会按店铺将 Spree::SearchProvider::IndexJob 加入队列(队列为 Spree.queues.search,失败后会退避重试)。批量导入会产生大量任务——请将 search 路由到独立的低优先级队列,或在导入后重新索引一次:bin/rails spree:search:reindex(spree task search:reindex)。
    • 更改索引设置/presenter 字段后重新索引。

图片

Spree::Media 根据 Spree::Config.product_image_variant_sizes 定义命名的 webp 变体(默认包括 mini、small、medium、large、xlarge 2000×2000、og_image 1200×630),并启用 preprocessed: true——Active Storage 会在上传时将转换任务加入队列,因此每次上传的每种尺寸都要进行一次转换(视频还需为每个封面图转换一次)。富文本的 :embed 变体会在首次使用时生成。

  • 在初始化程序中精简不使用的尺寸(必须在模型加载前设置)——尺寸越少,每次上传消耗的 CPU 越少。
  • 将转换任务路由到不会影响结账关键任务的队列(config.active_storage.queues.transform),并在 /rails/active_storage/representations/ 前放置 CDN(参见 spree-deployment、CDN_HOST)。
  • 不要从订阅器预热变体——命名变体已经预热,而临时变体的摘要不同。

后台任务

Spree 任务会从 Spree.queues 选择队列(默认值全部为 :default)。键包括:default、events、exports、images、imports、products、variants、categories、collections、stock_location_stock_levels、coupon_codes、themes、addresses、gift_cards、webhooks、payment_webhooks、api_keys、search、stock_reservations、tax_identifiers、data_requests、payouts。请在已安装的 spree_core 中核对 Spree.queues(lib/spree/core.rb)。

# server/config/initializers/spree.rb
Spree.queues.payment_webhooks   = :spree_payment_webhooks
Spree.queues.events             = :spree_events
Spree.queues.webhooks           = :spree_webhooks
Spree.queues.stock_reservations = :spree_stock_reservations
Spree.queues.imports            = :spree_imports
Spree.queues.images             = :spree_images
Spree.queues.search             = :spree_search
Spree.queues.categories         = :spree_categories
Spree.queues.collections        = :spree_collections
Spree.queues.payouts            = :spree_payouts

使用当前键名——Spree.queues.taxons= 和 stock_location_stock_items= 已弃用,而分配 Spree 不读取的键(例如 reports)会被静默忽略。

Solid Queue 调优
  • config/queue.yml 会按列出的顺序轮询队列——优先处理结账、支付和库存预留任务,将批量目录、导入和图像任务放在后面,并保留末尾的 "*"。
  • 在组合模式下,任务线程会与 Web 请求共享 Puma 进程和 GVL:提高其中的 JOB_THREADS 会以 API 延迟换取吞吐量。任务负载变得重要后,应拆分为 bin/jobs 工作进程(在 Web 端设置 SOLID_QUEUE_IN_PUMA=false),并通过 JOB_THREADS/JOB_CONCURRENCY/副本数进行扩展。
  • 如需隔离,可在 queue.yml 中定义多个工作进程——例如,一个池处理 [spree_payment_webhooks, spree_events, spree_webhooks, mailers, default],另一个池处理 [spree_imports, spree_images, active_storage_transform, spree_search]。
  • CSV 导入会通过 SPREE_IMPORT_JOB_CONCURRENCY 自行限制并发数(默认值为 JOB_THREADS 的 75%),以免大型导入占满所有线程。
  • 数据库连接池必须 ≥ RAILS_MAX_THREADS + JOB_THREADS(并留有余量)。
  • 在 Mission Control 的 /jobs 中查看积压情况。
  • Sidekiq 适用于超高负载——在 sidekiq.yml 中列出每个队列并设置权重(不设兜底队列)。参见 spree-deployment。
事件订阅者

订阅者默认异步执行(在 Spree.queues.events 上运行 Spree::Events::SubscriberJob)。只有对于必须在请求中执行且开销很低的工作,才使用 subscribes_to 'order.placed', async: false。繁重或受网络限制的副作用应交由异步订阅者处理。

仪表板(React 管理后台)

仪表板使用 TanStack Query(默认 staleTime 为 60s,retry: 1,切换焦点时不重新获取数据)。在插件中:

  • 通过 adminClient 获取数据,并且只对视图会渲染的数据传入 expand: [...];使用列表 limit 和 Ransack 筛选器,而不是加载所有数据。
  • 复用查询键,让各组件共享缓存数据;对于变化较慢的参考数据,设置更长的 staleTime。
  • 仪表板页面变慢,几乎总是因为 Admin API 端点变慢——请在服务端追踪。

常见错误

  • 遍历变体并调用 price_in,却未预加载 prices。
  • 调用外部税费/汇率 API 时不做缓存——每次购物车写入都会触发多次调用。
  • 在事务或同步钩子处理程序中执行 HTTP 调用。
  • 繁忙的商店中让所有 Spree.queues 都使用 :default——导入和图像转换会延误支付 Webhook 和库存预留过期处理。
  • 提供访客目录流量时,CDN 未遵循 Vary,或在 CDN 处剥离 x-spree-* 标头。
  • 在自定义端点中使用 Spree::Product.all——应通过 current_store 限定范围并进行分页。

延伸阅读

  • node_modules/@spree/docs/dist/developer/providers/observability.md — 跨度、采样、跨度指标
  • node_modules/@spree/docs/dist/developer/deployment/background_jobs.md、caching.md、cdn.md
  • node_modules/@spree/docs/dist/developer/core-concepts/search-filtering.md
  • 源码:Spree::Carts::Recalculate / RecalculateTotals(spree_core/app/workflows/spree/carts/)、Spree::Api::V3::HttpCaching、Spree::Api::V3::ResourceController
  • 相关技能:spree-workflows、spree-order-totals、spree-taxes、spree-deployment、spree-events-webhooks

评论

支持 Markdown