Install
$ agentstack add skill-spree-agent-skills-spree-performance ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
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:
- Cart pipeline cost on every cart change.
Spree::Cart::Recalculateis 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. - 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.
- Search provider latency. Database search degrades past ~10K products. Meilisearch's network round-trip + result deserialization adds up if not bounded.
- 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.
- Sidekiq queue backlog. Per-queue weights matter — image processing flooding the
defaultqueue blocks event subscribers from firing in time. - 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::Recalculateand its dependencies inspree_core/app/services/spree/cart/. - Search provider:
Spree::SearchProvider::BaseandSpree::SearchProvider::Meilisearchin the installedspree_coregem. - 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.
Write a review
Versions
- v0.1.0 Imported from the upstream source.