LCP Issues on Commercetools | Blue Frog Docs

LCP Issues on Commercetools

Fixing Largest Contentful Paint issues on Commercetools headless commerce storefronts

LCP Issues on Commercetools

General Guide: See Global LCP Guide for universal concepts and fixes.

Commercetools is a headless platform, so LCP performance depends entirely on your frontend implementation and API optimization.

Commercetools-Specific Causes

1. API Response Latency

The Commercetools API can add latency before content renders:

  • Product data fetched client-side delays LCP
  • Large API responses slow initial render
  • Multiple sequential API calls block rendering

2. Unoptimized Product Images

Product images from Commercetools CDN may not be optimized:

  • Missing responsive image sizes
  • No modern format (WebP/AVIF) support
  • Large hero images loaded eagerly

3. Client-Side Data Fetching

SPA patterns often delay content rendering:

4. Heavy JavaScript Bundles

Frontend frameworks add significant JavaScript:

  • Commercetools SDK bundle size
  • React/Vue/Angular framework overhead
  • Third-party tracking scripts

Commercetools-Specific Fixes

Fix 1: Implement Server-Side Rendering

Pre-render product data on the server to eliminate API latency from LCP:

Next.js App Router (Server Components):

// app/products/[id]/page.tsx
import { fetchProduct } from '@/lib/commercetools';

// Server Component - fetches data on server
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetchProduct(params.id);

  return (
    <main>
      <h1>{product.masterData.current.name['en-US']}</h1>
      <ProductImage
        src={product.masterData.current.masterVariant.images?.[0]?.url}
        alt={product.masterData.current.name['en-US']}
        priority // Marks as LCP element
      />
      <ProductDetails product={product} />
    </main>
  );
}

Nuxt 3:

<!-- pages/products/[id].vue -->
<script setup>
const route = useRoute();
const { data: product } = await useFetch(`/api/products/${route.params.id}`);
</script>

<template>
  <main>
    <h1>{{ product.masterData.current.name['en-US'] }}</h1>
    <NuxtImg
      :src="product.masterData.current.masterVariant.images?.[0]?.url"
      :alt="product.masterData.current.name['en-US']"
      preload
    />
  </main>
</template>

Fix 2: Optimize API Queries

Reduce payload size and response time:

// Use GraphQL for minimal payloads
const query = `
  query GetProduct($id: String!) {
    product(id: $id) {
      id
      masterData {
        current {
          name(locale: "en-US")
          masterVariant {
            images {
              url
            }
            prices {
              value {
                centAmount
                currencyCode
              }
            }
          }
        }
      }
    }
  }
`;

// Or use REST with specific projections
const product = await apiRoot
  .productProjections()
  .withId({ ID: productId })
  .get({
    queryArgs: {
      priceCurrency: 'USD',
      priceCountry: 'US',
      // Limit expanded data
    }
  })
  .execute();

Fix 3: Optimize Product Images

Implement responsive images with modern formats:

// components/ProductImage.tsx
import Image from 'next/image';

interface ProductImageProps {
  src: string;
  alt: string;
  priority?: boolean;
}

export function ProductImage({ src, alt, priority = false }: ProductImageProps) {
  // Transform Commercetools image URL for optimization
  const optimizedSrc = transformImageUrl(src, {
    width: 800,
    format: 'webp',
    quality: 80
  });

  return (
    <Image
      src={optimizedSrc}
      alt={alt}
      width={800}
      height={800}
      priority={priority}
      sizes="(max-width: 768px) 100vw, 50vw"
      placeholder="blur"
      blurDataURL={getBlurDataURL(src)}
    />
  );
}

// Image transformation (use your CDN or image service)
function transformImageUrl(url: string, options: ImageOptions): string {
  // If using Cloudinary, imgix, or similar
  const baseUrl = new URL(url);
  baseUrl.searchParams.set('w', options.width.toString());
  baseUrl.searchParams.set('f', options.format);
  baseUrl.searchParams.set('q', options.quality.toString());
  return baseUrl.toString();
}

Fix 4: Preload Critical Resources

Add preload hints for LCP elements:

// app/layout.tsx
import { headers } from 'next/headers';

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        {/* Preconnect to Commercetools API and CDN */}
        <link rel="preconnect" href="https://api.commercetools.com" />
        <link rel="preconnect" href="https://your-cdn.commercetools.com" />

        {/* DNS prefetch for image CDN */}
        <link rel="dns-prefetch" href="https://images.commercetools.com" />
      </head>
      <body>{children}</body>
    </html>
  );
}

// For specific product pages, preload the hero image
export async function generateMetadata({ params }) {
  const product = await fetchProduct(params.id);
  const heroImage = product.masterData.current.masterVariant.images?.[0]?.url;

  return {
    other: {
      'link': heroImage ? `<${heroImage}>; rel=preload; as=image` : undefined
    }
  };
}

Pre-render high-traffic product pages at build time:

// app/products/[id]/page.tsx
import { fetchPopularProducts, fetchProduct } from '@/lib/commercetools';

// Generate static pages for popular products
export async function generateStaticParams() {
  const popularProducts = await fetchPopularProducts(100);

  return popularProducts.map(product => ({
    id: product.id
  }));
}

// Revalidate every hour
export const revalidate = 3600;

export default async function ProductPage({ params }) {
  const product = await fetchProduct(params.id);
  // ... render product
}

Fix 6: Optimize JavaScript Loading

Reduce and defer non-critical JavaScript:

// app/layout.tsx
import dynamic from 'next/dynamic';

// Lazy load non-critical components
const ProductRecommendations = dynamic(
  () => import('@/components/ProductRecommendations'),
  { ssr: false }
);

const ReviewsWidget = dynamic(
  () => import('@/components/ReviewsWidget'),
  { ssr: false }
);

// Defer analytics until after LCP
const Analytics = dynamic(
  () => import('@/components/Analytics'),
  {
    ssr: false,
    loading: () => null
  }
);
// Delay non-critical tracking
if (typeof window !== 'undefined') {
  window.addEventListener('load', () => {
    setTimeout(() => {
      // Load analytics after page is interactive
      import('./analytics').then(module => module.init());
    }, 0);
  });
}

Measuring LCP

Using Web Vitals Library

// lib/webVitals.ts
import { onLCP, onFID, onCLS } from 'web-vitals';

export function reportWebVitals() {
  onLCP((metric) => {
    console.log('LCP:', metric.value, 'ms');
    console.log('LCP Element:', metric.entries[0]?.element);

    // Send to analytics
    gtag('event', 'web_vitals', {
      metric_name: 'LCP',
      metric_value: metric.value,
      metric_delta: metric.delta,
    });
  });
}

Chrome DevTools

  1. Open DevTools → Performance tab
  2. Check "Web Vitals" checkbox
  3. Record page load
  4. Look for LCP marker in timeline
  5. Identify the LCP element

Verification

After implementing fixes:

  1. Test with PageSpeed Insights - Should show LCP < 2.5s
  2. Check Core Web Vitals in GA4 - Monitor real user data
  3. Use Chrome UX Report - 28-day rolling average
  4. Test on slow connections - Use DevTools throttling

Common Pitfalls

Client-Side Fetching on Product Pages

// BAD - fetches data client-side
export default function ProductPage({ params }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    fetchProduct(params.id).then(setProduct);
  }, [params.id]);

  if (!product) return <LoadingSpinner />;
  // ...
}

// GOOD - fetch on server
export default async function ProductPage({ params }) {
  const product = await fetchProduct(params.id);
  return <ProductDisplay product={product} />;
}

Lazy Loading LCP Images

// BAD - lazy loads hero image
<Image src={heroImage} loading="lazy" />

// GOOD - prioritize hero image
<Image src={heroImage} priority />
// SYS.FOOTER