Skip to content
Back to skills

Spree Performance

ASecurity

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).

  • 39 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 29, 2026
ai-agentsgorubysqlrailsawstestinggitapidatabasebackend

Works with

  • cursor
  • api

Security analysis

A100/100

Scanned May 29, 2026

npx -y skills add OrcaQubits/agentic-commerce-skills-plugins --skill spree-performance --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Spree Performance?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Spree Performance
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/orcaqubits-spree-performance/badge)](https://www.skillsdirectory.com/skills/orcaqubits-spree-performance)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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).
---

# 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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…