AgentStack
SKILL verified MIT Self-run

Spree Performance

skill-spree-agent-skills-spree-performance · by spree

Use when the user is investigating or improving Spree performance — slow product listings, slow cart updates, search latency, image processing bottlenecks, Sidekiq queue tuning, N+1 in admin pages, cache invalidation strategies. Common phrasings include "slow PDP", "slow cart", "N+1 queries in Spree", "Sidekiq queue backlog", "search slow", "Meilisearch tuning", "image processing slow", "Spree ca…

No reviews yet
0 installs
8 views
0.0% view→install

Install

$ agentstack add skill-spree-agent-skills-spree-performance

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Spree Performance? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Spree Performance

Most Spree performance work has more leverage than generic Rails tuning because the bottleneck is usually in one of a few known hotspots. This skill covers those.

The biggest leverage areas

In rough order of impact for typical Spree stores:

  1. Cart pipeline cost on every cart change. Spree::Cart::Recalculate is the most-run service in the app — every line-item add, remove, and quantity change fires it. A slow recalculate makes the storefront feel sluggish.
  2. Catalog rendering N+1s. Product listing pages load Products, then prices, then images, then variants, then categories — easy to hit dozens of queries per product.
  3. Search provider latency. Database search degrades past ~10K products. Meilisearch's network round-trip + result deserialization adds up if not bounded.
  4. Image processing. Generating image variants is slow and CPU-bound — variants are pre-generated in background transform jobs at upload time, and bulk uploads can flood the queue.
  5. Sidekiq queue backlog. Per-queue weights matter — image processing flooding the default queue blocks event subscribers from firing in time.
  6. Admin product table. The N+1 problem with 100+ products and all-columns-visible is real.

The cart pipeline

Every cart change runs Spree.cart_recalculate_service (default: Spree::Cart::Recalculate). The chain reads line items, prices, adjustments, shipments, promotions, computes totals, and writes the order back.

Common cart-pipeline N+1s

Each line item lazily loads its variant, then the variant's price, then the variant's images for the cart UI. Eager-load before iterating:

order.line_items.includes(variant: [:prices, :images, product: :categories]).each do |li|
  # ...
end

In a custom recalculate step, prefer batch operations over per-item loops. If you must loop, eager-load the associations the loop touches.

Sidekiq for slow recalculate work

If you have a recalculate step that's slow (external service call, complex computation), make it async via Sidekiq instead of inline. The customer doesn't need to wait for an analytics push; fire-and-forget via a subscriber on order.updated (see the spree-events-webhooks skill).

Profiling the recalculate

Sample any cart in the Rails console:

order = Spree::Order.find(123)
ActiveRecord::Base.logger.level = Logger::DEBUG
Spree::Cart::Recalculate.call(order: order, line_item: order.line_items.first)

Read the query log. Anything above ~50 queries for a 5-item cart is high. Anything that issues per-line-item queries is fixable with eager loading.

Catalog rendering

The classic Spree catalog page (PLP) hits N+1s by default. Spree includes ar_lazy_preload to mitigate, but only for paths that use it.

preloadassociationslazily

preload_associations_lazily (from the arlazypreload gem) is available on any relation — it auto-preloads whichever associations the iteration touches, with no per-model list. Spree's own controllers chain it on collections (see Spree::Api::V3::ResourceController#collection). Use it on custom catalog queries:

@products = Spree::Product.for_store(current_store)
                          .active(Spree::Current.currency)
                          .includes(
                            primary_media: [attachment_attachment: :blob],
                            master: [:prices, stock_items: [:stock_location, :active_stock_reservations]],
                            variants: [:prices, stock_items: [:stock_location, :active_stock_reservations]]
                          )
                          .preload_associations_lazily
@pagy, @products = pagy(@products, page: params[:page], limit: params[:per_page])

Pagination is Pagy (Spree's only pagination dependency) — the pagy(...) call mirrors Spree::Api::V3::ResourceController#collection. There is no Kaminari-style .page scope on Spree models.

For the most common path (default-variant-only listing), the API's ProductsController#scope already does the right thing. If you're building a custom catalog endpoint, copy that pattern.

cache_key_with_version

Every Spree model that's Spree.base_class-derived has a cache_key_with_version instance method (from ActiveRecord) — it folds in the model's updated_at. Use it for HTTP caching and fragment caching:

def show
  product = scope.find_by_prefix_id!(params[:id])
  fresh_when(etag: product.cache_key_with_version, last_modified: product.updated_at)
  # render serializer
end

  

product.touch (or touching any has_many child that the model belongs_to :product, touch: true on) bumps the version and invalidates the cache.

Search provider performance

Database provider (default)

Fine for catalogs


Updates to the product (or any `touch:`-linked association) automatically bust the cache.

### Rails.cache for expensive computations

For per-store computed values (active promo banner, configured currencies, available payment methods):

```ruby
Rails.cache.fetch(['store', current_store.cache_key_with_version, 'banner'], expires_in: 5.minutes) do
  ActiveBannerService.call(current_store)
end

Don't cache anything tied to the customer (cart, account) — it varies per session and pollutes the cache.

HTTP caching on the Store API

Only the Store API catalog controllers (products, categories, countries, currencies, markets, locales, policies) opt into Spree::Api::V3::HttpCaching — the base v3 ResourceController ships no-op caching hooks. For guest (unauthenticated) requests it sets a public Cache-Control (5-minute TTL by default): show actions use Rails stale? on the record (ETag from cache_key_with_version, Last-Modified from updated_at), while index actions get a digest ETag built from the collection's latest updated_at, count, and query params. Authenticated requests are sent Cache-Control: private, no-store, so CDNs (Cloudflare, Fastly, CloudFront) can only cache guest /api/v3/store/products-style traffic with conditional revalidation; guest responses Vary on Accept, x-spree-currency, and x-spree-locale.

Profiling tools

  • rack-mini-profiler — add it to the Gemfile first (it is not in spree-starter by default). Look for the badge on every page; click for the query waterfall.
  • bullet — detects N+1s in development. Add to the Gemfile and configure to notify on N+1.
  • Skylight / Scout / New Relic — production APM. All work fine with Spree out of the box.
  • ActiveSupport::Notifications instrumentation — Spree (via Rails) fires sql.active_record, process_action.action_controller, cache.read, cache.write. Hook into them for custom dashboards: ActiveSupport::Notifications.subscribe('sql.active_record') { |...| ... }.

Where to read further

  • Cart pipeline: Spree::Cart::Recalculate and its dependencies in spree_core/app/services/spree/cart/.
  • Search provider: Spree::SearchProvider::Base and Spree::SearchProvider::Meilisearch in the installed spree_core gem.
  • Deployment caching: node_modules/@spree/docs/dist/developer/deployment/caching.md.
  • Search + filtering: node_modules/@spree/docs/dist/developer/core-concepts/search-filtering.md.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

  • Author: spree
  • Source: spree/agent-skills
  • License: MIT
  • Homepage: https://spreecommerce.org/docs/developer/agentic/overview

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.