General Guide: See Global LCP Guide for universal concepts and fixes.
What is LCP?
Largest Contentful Paint measures when the largest content element becomes visible. Google recommends LCP under 2.5 seconds. Spree Commerce is a Ruby on Rails e-commerce framework with ERB/Slim templates, ActiveStorage or Paperclip for image handling, and Deface for storefront customization. LCP depends on Rails caching configuration, image variant processing, and the overhead of Deface template overrides.
Spree Commerce-Specific LCP Causes
- No fragment caching by default -- Spree views render full product listings and detail pages without Rails fragment caching unless explicitly configured
- Unprocessed product images -- original uploaded images serve without server-side resizing unless ActiveStorage variants or Paperclip styles are configured
- Deface override overhead -- Deface intercepts and modifies views at runtime, adding processing time to every request
- N+1 queries on listings -- product listing pages often trigger N+1 queries for variants, prices, images, and taxons
- Asset pipeline bloat -- Spree's default asset pipeline includes JavaScript and CSS for all storefront features, even unused ones
Fixes
1. Enable Rails Fragment Caching
# config/environments/production.rb
config.action_controller.perform_caching = true
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
expires_in: 1.hour
}
<%# In your product listing partial (_product.html.erb) %>
<% cache [product, product.updated_at] do %>
<div class="product-card">
<%= image_tag product.images.first&.url(:small),
width: 400, height: 400,
loading: 'lazy',
style: 'aspect-ratio: 1/1; width: 100%; height: auto; object-fit: cover;' %>
<h3><%= product.name %></h3>
<p><%= product.display_price %></p>
</div>
<% end %>
2. Configure Image Variants
For ActiveStorage (Spree 4+):
# app/models/spree/image_decorator.rb
module Spree
module ImageDecorator
def self.prepended(base)
base.attachment_definitions[:attachment][:styles] = {
mini: '48x48>',
small: '400x400>',
product: '800x800>',
large: '1200x1200>'
}
end
end
end
Spree::Image.prepend(Spree::ImageDecorator)
In your product detail template:
<%# Product hero image with proper sizing %>
<% if @product.images.any? %>
<% hero = @product.images.first %>
<img
src="<%= hero.url(:large) %>"
width="1200" height="1200"
alt="<%= @product.name %>"
loading="eager"
fetchpriority="high"
style="aspect-ratio: 1/1; width: 100%; height: auto; object-fit: contain;"
>
<% end %>
3. Optimize Database Queries
# In your products controller or decorator
# Fix N+1 queries on listing pages
def index
@products = Spree::Product
.includes(:images, :variants_including_master, :taxons)
.includes(variants_including_master: [:prices, :images])
.where(available_on: ..Time.current)
.page(params[:page])
.per(12)
end
4. Inline Critical CSS
<%# In your application layout (app/views/spree/layouts/spree_application.html.erb) %>
<head>
<style>
/* Critical above-fold CSS */
body { font-family: system-ui, sans-serif; margin: 0; }
.header { display: flex; height: 64px; align-items: center; }
.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; }
.product-card img { aspect-ratio: 1/1; width: 100%; object-fit: cover; }
</style>
<%# Defer full storefront CSS %>
<link rel="preload" href="<%= asset_path('storefront/all.css') %>"
as="style"
<noscript>
<link rel="stylesheet" href="<%= asset_path('storefront/all.css') %>">
</noscript>
</head>
5. Reduce Deface Override Impact
# Audit active Deface overrides
# In Rails console:
Deface::Override.all.each { |k, v| puts "#{k}: #{v.count} overrides" }
# Convert heavy Deface overrides to proper view overrides
# Instead of Deface:
# app/overrides/add_hero_image.rb
# Use a direct view override:
# app/views/spree/products/show.html.erb (copy from gem, modify directly)
Measuring LCP on Spree
- Rails server logs -- check render times:
Completed 200 OK in 450ms (Views: 200ms | ActiveRecord: 50ms) - Bullet gem -- install
gem 'bullet'to detect N+1 queries on product pages - PageSpeed Insights -- test homepage, product listings, and product detail pages
- rack-mini-profiler -- add
gem 'rack-mini-profiler'for per-request performance breakdown in development
Analytics Script Impact
- Place analytics scripts at the bottom of the layout before
</body>withasync/defer - Spree has built-in analytics event hooks (
spree:cart:add,spree:checkout:complete) -- use these for conversion tracking instead of DOM scraping - Avoid synchronous e-commerce tracking scripts in the
<head>