Profile and optimize a Spree application — N+1 queries with bullet/scout, database indexing strategy for Spree's polymorphic associations, Rails fragment + Russian doll caching, ActiveStorage variant pre-generation, Sidekiq queue tuning, MeiliSearch vs Postgres FTS tradeoffs, Puma worker/thread sizing, CDN strategy for catalog pages, asset precompile time, and load testing. Use when Spree is slow, the database is hot, or you're preparing for a traffic spike (Black Friday, launch).
Scanned 9/7/2026
Install to Claude Code
npx -y skills add OrcaQubits/agentic-commerce-skills-plugins --skill spree-performance --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Spree Performance?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/orcaqubits-spree-performance-agentic-commerce-skills-plugin)More formats (shields.io, HTML) on the badges page.
---
name: spree-performance
description: Profile and optimize a Spree application — N+1 queries with bullet/scout, database indexing strategy for Spree's polymorphic associations, Rails fragment + Russian doll caching, ActiveStorage variant pre-generation, Sidekiq queue tuning, MeiliSearch vs Postgres FTS tradeoffs, Puma worker/thread sizing, CDN strategy for catalog pages, asset precompile time, and load testing. Use when Spree is slow, the database is hot, or you're preparing for a traffic spike (Black Friday, launch).
allowed-tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch
---
# Spree Performance
## Before writing code
**Fetch live docs**:
1. Search the Spree blog at https://spreecommerce.org/blog for performance posts (Black Friday case studies, scaling guides).
2. Inspect `Spree::OrderUpdater` and other "hot path" service objects in the live source for known bottlenecks.
3. Cross-reference Rails 7+ performance guides for caching primitives.
4. For Sidekiq tuning, fetch https://github.com/sidekiq/sidekiq/wiki/Best-Practices.
5. Check the v5.4 announcement (https://spreecommerce.org/announcing-spree-commerce-5-4/) for the API v3 "10x faster than v2" claim and what changed.
## Conceptual Architecture
### Where Spree Spends Time
Profiling typical Spree requests shows hotspots at:
1. **N+1 queries** in catalog views (variants, prices, images)
2. **Order recalculation** on cart updates (`OrderUpdater` traverses line items, adjustments, taxes)
3. **Tax calculation** (`Spree::TaxCalculator` per-line)
4. **Promotion eligibility** (rules evaluated per applicable promotion per order)
5. **Image processing** (ActiveStorage variants generated on first request)
6. **Search** (Postgres FTS or MeiliSearch query)
7. **Adapter overhead** on heavy use cases (JSON:API in v2 was a serializer bottleneck — fixed in v3 flat-JSON)
### v3 API is ~10× Faster than v2
The v5.4 v3 API rewrite cites 10× perf vs v2:
- Flat JSON serialization beats JSON:API
- Prefixed IDs lookup uses index
- Fewer DB round-trips per response
- Better Russian-doll caching support
If migrating from v2 → v3, expect significant catalog-page speedups.
### Common N+1 Sources
```ruby
# BAD — N+1 on variants, prices, images
Spree::Product.all.each { |p| puts p.master.price }
# GOOD
Spree::Product.includes(master: :prices).each { |p| puts p.master.price }
# Better for views — preload images and stores too
Spree::Product.includes(:images, :stores, master: [:prices, :stock_items])
```
Use **bullet** in dev/test:
```ruby
# Gemfile
group :development, :test do
gem 'bullet'
end
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.rails_logger = true
Bullet.add_footer = true
end
```
### Fragment Caching
```erb
<% cache product do %>
<%= render product %>
<% end %>
```
Cache key auto-busts on `product.updated_at`. Combine with `touch: true` on associations for Russian-doll caching:
```ruby
class Spree::Variant < ApplicationRecord
belongs_to :product, touch: true
end
```
### Catalog Page Caching
For PLPs that don't change per user:
```ruby
class CatalogController < ApplicationController
def show
expires_in 5.minutes, public: true # CDN-cacheable
@products = current_store.products.active.includes(:taxons, master: :prices)
end
end
```
For per-user PDP (price varies):
- Cache the immutable bits as fragments
- Render user-specific bits without cache
### Database Indexing
Spree ships indexes for most common access patterns. Verify your queries hit them — `EXPLAIN ANALYZE` in psql. Common missing indexes after schema changes:
```sql
CREATE INDEX index_spree_orders_on_user_id_and_state ON spree_orders(user_id, state);
CREATE INDEX index_spree_adjustments_on_adjustable ON spree_adjustments(adjustable_type, adjustable_id);
```
For multi-store, scope indexes to `store_id` columns.
### ActiveStorage Variant Pre-Generation
```ruby
# Generate variants in background after upload
class Spree::Image < ApplicationRecord
has_one_attached :attachment do |attachable|
attachable.variant :small, resize_to_limit: [120, 120], preprocessed: true
attachable.variant :large, resize_to_limit: [1200, 1200], preprocessed: true
end
end
```
Without `preprocessed: true`, the first user request generates the variant — slow.
### Sidekiq Queue Strategy
```yaml
# config/sidekiq.yml
:concurrency: 25
:queues:
- [critical, 5]
- [webhooks, 3]
- [mailers, 2]
- [default, 1]
```
Tune `:concurrency` against DB pool size — each Sidekiq thread takes one DB connection.
### Puma Sizing
```ruby
# config/puma.rb
workers ENV.fetch('WEB_CONCURRENCY') { 2 }
threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }
threads threads_count, threads_count
preload_app!
```
Math: `workers × threads_count ≤ DB connection pool`. Set `pool` in `database.yml` to `threads_count`.
### Reporting Queries
Reports over `Spree::Order` can scan millions of rows. Strategies:
- Materialized views for daily/monthly aggregates
- Replica DB for reporting (avoid contention on primary)
- Pre-computed via Sidekiq jobs cached in Redis
- Pagination with cursors (`starting_after`) not OFFSET
### Image CDN
Front ActiveStorage with a CDN:
```ruby
# config/storage.yml
amazon:
service: S3
access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
bucket: my-spree-bucket
region: us-east-1
public: true # if you front with CDN
```
Set CloudFront / Cloudflare in front. URLs become `https://cdn.example.com/...`.
### MeiliSearch vs Postgres FTS
| Backend | Latency | Faceting | Typo tolerance |
|---------|---------|----------|----------------|
| MeiliSearch | <50ms typical | Yes | Yes (built-in) |
| Algolia | <50ms typical | Yes | Yes |
| Postgres FTS | 100-500ms | Limited | Trigram-based, manual |
MeiliSearch wins for catalog search above ~10k products.
### Order Recalc Cost
`Spree::OrderUpdater` runs on every line-item change. If profiling shows it dominant:
- Batch updates (`Spree::OrderMutations.batch { |order| ... }` — verify API)
- Defer to a Sidekiq job if not needed synchronously
- Swap `Spree::Dependencies.order_updater_class` with a leaner implementation if you don't use all features
## Implementation Guidance
### Profiling Setup
```ruby
# Gemfile
group :development do
gem 'bullet'
gem 'rack-mini-profiler'
gem 'flamegraph'
gem 'stackprof'
gem 'memory_profiler'
end
```
Hit a slow page with `?pp=help` to see profiler menu.
### Identifying Slow Queries
Enable `pg_stat_statements`:
```sql
CREATE EXTENSION pg_stat_statements;
SELECT calls, mean_exec_time, query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
```
### Cache Key Versioning
After a Spree minor upgrade, bust caches:
```ruby
# config/initializers/cache_version.rb
Rails.application.config.cache_classes_version = 'spree_5.4.2_v1'
```
(Add as fragment cache version suffix.)
### Black Friday Prep Playbook
1. **Capacity test 4 weeks before** — synthetic load = 5× expected peak
2. **DB read replica** for reporting queries
3. **Cache warming** — pre-generate ActiveStorage variants for all live products
4. **CDN cache TTLs raised** on catalog pages
5. **Sidekiq scaling test** — webhook backlog handling at burst
6. **Database connection pool** raised on primary
7. **Read-only mode plan** in case of overload
8. **War room runbook** — who is on call for what
9. **Disable expensive features** during peak (e.g., live carrier rate calls — fall back to flat rate)
### Common Pitfalls
- **`Spree::Order.all` in a controller** — full table scan; always scope.
- **`includes` not matching what the view uses** — N+1 sneaks back in.
- **Fragment cache without `touch: true`** — stale cards on inventory change.
- **Sidekiq concurrency exceeding DB pool** — connection-pool errors under load.
- **Image processing on web tier** — blocks request thread; do in Sidekiq.
- **OFFSET pagination over large tables** — slow; use cursors.
- **MeiliSearch index out of sync** — events not firing or job failures; monitor lag.
- **Ransack filters scanning unindexed columns** — admin order search slow over millions of rows.
- **Logging full response bodies** — log volume explodes; filter.
- **`spree:install` rake task in CI on every build** — slow; cache the test app.
Always profile before optimizing — assumptions about where the time goes are usually wrong. Spree's hot paths are well-known; community blog posts and Spree's own benchmarks are the best starting point.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!