Fix LCP Issues on Spreecommerce (Loading Speed) | OpsBlu Docs

Fix LCP Issues on Spreecommerce (Loading Speed)

Speed up Spree Commerce LCP by enabling Rails fragment caching, resizing product variant images, and optimizing Deface overrides.

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

  1. Rails server logs -- check render times: Completed 200 OK in 450ms (Views: 200ms | ActiveRecord: 50ms)
  2. Bullet gem -- install gem 'bullet' to detect N+1 queries on product pages
  3. PageSpeed Insights -- test homepage, product listings, and product detail pages
  4. 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> with async/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>