Fix Prismic LCP Issues | OpsBlu Docs

Fix Prismic LCP Issues

Reduce Prismic LCP by using Imgix-powered image transforms, preloading hero Slice images, and enabling static generation in Next.js.

Largest Contentful Paint (LCP) measures how quickly the largest visible element loads on your page. For Prismic sites, this is often an image from the Prismic Media Library. This guide shows how to optimize Prismic images for optimal LCP performance.


Understanding LCP on Prismic Sites

Common LCP Elements on Prismic Sites

  • Hero Images - Large header images in Hero Slices
  • Featured Images - Blog post or article featured images
  • Banner Images - Promotional or announcement banners
  • Product Images - E-commerce product photos
  • Background Images - Large background images in Slices

LCP Performance Targets

  • Good: LCP ≤ 2.5 seconds
  • Needs Improvement: LCP 2.5-4.0 seconds
  • Poor: LCP > 4.0 seconds

Using Next.js Image Component

Next.js provides automatic image optimization for Prismic images.

Basic Implementation

// components/PrismicImage.js
import Image from 'next/image';

export function PrismicImage({ field, priority = false, fill = false }) {
  if (!field || !field.url) return null;

  const { url, alt, dimensions } = field;

  if (fill) {
    return (
      <Image
        src={url}
        alt={alt || ''}
        fill
        priority={priority}
        sizes="100vw"
        style={{ objectFit: 'cover' }}
      />
    );
  }

  return (
    <Image
      src={url}
      alt={alt || ''}
      width={dimensions.width}
      height={dimensions.height}
      priority={priority}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
  );
}

Optimize Hero Slice LCP

// slices/HeroSlice/index.js
import { PrismicImage } from '@/components/PrismicImage';

export default function HeroSlice({ slice }) {
  return (
    <section className="hero">
      <div className="hero-image">
        <PrismicImage
          field={slice.primary.background_image}
          priority={true} // Critical for LCP
          fill={true}
        />
      </div>
      <div className="hero-content">
        <h1>{slice.primary.title}</h1>
        <p>{slice.primary.description}</p>
      </div>
    </section>
  );
}

Key optimizations:

  • priority={true} - Preloads image, prevents lazy loading
  • fill={true} - For responsive background images
  • sizes attribute - Provides responsive image sizing hints

Configure Next.js Image Loader for Prismic

In next.config.js:

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.prismic.io',
        pathname: '/**',
      },
    ],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    formats: ['image/webp'],
  },
};

Advanced: Responsive Images with Prismic

// components/ResponsivePrismicImage.js
import Image from 'next/image';
import { asImageSrc } from '@prismicio/client';

export function ResponsivePrismicImage({ field, priority = false }) {
  if (!field || !field.url) return null;

  // Generate responsive image URLs using Prismic Imgix
  const mobileSrc = asImageSrc(field, {
    w: 768,
    q: 80,
    auto: 'format,compress',
  });

  const tabletSrc = asImageSrc(field, {
    w: 1024,
    q: 80,
    auto: 'format,compress',
  });

  const desktopSrc = asImageSrc(field, {
    w: 1920,
    q: 85,
    auto: 'format,compress',
  });

  return (
    <picture>
      <source media="(max-width: 767px)" srcSet={mobileSrc} />
      <source media="(max-width: 1023px)" srcSet={tabletSrc} />
      <source media="(min-width: 1024px)" srcSet={desktopSrc} />
      <Image
        src={field.url}
        alt={field.alt || ''}
        width={field.dimensions.width}
        height={field.dimensions.height}
        priority={priority}
      />
    </picture>
  );
}

Method 2: Gatsby with Prismic

Using Gatsby Image Plugin

Install Dependencies

npm install gatsby-plugin-image gatsby-source-prismic

Configure gatsby-config.js

module.exports = {
  plugins: [
    {
      resolve: 'gatsby-source-prismic',
      options: {
        repositoryName: 'your-repo-name',
        accessToken: process.env.PRISMIC_ACCESS_TOKEN,
        imageImgixParams: {
          auto: 'compress,format',
          fit: 'max',
          q: 80,
        },
        imagePlaceholderImgixParams: {
          w: 100,
          blur: 15,
        },
      },
    },
    'gatsby-plugin-image',
  ],
};

GraphQL Query for Optimized Images

query BlogPostQuery($uid: String!) {
  prismicBlogPost(uid: { eq: $uid }) {
    data {
      title {
        text
      }
      featured_image {
        gatsbyImageData(
          layout: FULL_WIDTH
          placeholder: BLURRED
          imgixParams: {
            auto: "compress,format"
            q: 80
          }
        )
        alt
      }
    }
  }
}

Render Optimized Image

// src/templates/blog-post.js
import { GatsbyImage, getImage } from 'gatsby-plugin-image';

export default function BlogPost({ data }) {
  const { prismicBlogPost } = data;
  const image = getImage(prismicBlogPost.data.featured_image);

  return (
    <article>
      {image && (
        <GatsbyImage
          image={image}
          alt={prismicBlogPost.data.featured_image.alt || ''}
          loading="eager" // For LCP images
        />
      )}
      <h1>{prismicBlogPost.data.title.text}</h1>
    </article>
  );
}

Prismic Imgix Optimization

Prismic uses Imgix for image delivery. Leverage Imgix parameters for optimization:

Key Imgix Parameters

import { asImageSrc } from '@prismicio/client';

// Optimized image URL
const optimizedUrl = asImageSrc(imageField, {
  // Automatic format selection (WebP when supported)
  auto: 'format,compress',

  // Quality (lower = smaller file, faster load)
  q: 80, // 80 is good balance

  // Width constraints
  w: 1920,
  h: 1080,

  // Fit mode
  fit: 'crop',
  crop: 'faces,entropy',

  // Sharpening
  sharp: 10,

  // DPR (device pixel ratio)
  dpr: 2,
});

Responsive Image Srcset

function generateSrcSet(imageField) {
  const widths = [320, 640, 768, 1024, 1280, 1920];

  return widths
    .map((width) => {
      const url = asImageSrc(imageField, {
        w: width,
        auto: 'format,compress',
        q: 80,
      });
      return `${url} ${width}w`;
    })
    .join(', ');
}

// Usage
<img
  src={imageField.url}
  srcSet={generateSrcSet(imageField)}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  alt={imageField.alt}
  loading="eager" // For LCP images
/>

Preload Critical Images

Next.js App Router

In app/layout.js or page-specific layout:

export default function Layout({ children }) {
  return (
    <html>
      <head>
        {/* Preload hero image */}
        <link
          rel="preload"
          as="image"
          href="https://images.prismic.io/your-repo/hero-image.jpg?auto=format,compress&q=80&w=1920"
          imageSrcSet="
            https://images.prismic.io/your-repo/hero-image.jpg?w=640&auto=format,compress&q=80 640w,
            https://images.prismic.io/your-repo/hero-image.jpg?w=1024&auto=format,compress&q=80 1024w,
            https://images.prismic.io/your-repo/hero-image.jpg?w=1920&auto=format,compress&q=80 1920w
          "
          imageSizes="100vw"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Next.js Pages Router

In pages/_document.js:

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <link
            rel="preload"
            as="image"
            href="https://images.prismic.io/your-repo/hero.jpg?auto=format,compress&q=80&w=1920"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

Slice-Specific Optimizations

Determine LCP Slice Dynamically

// utils/isLCPSlice.js
export function isLCPSlice(slice, index) {
  // First Hero slice is likely LCP
  if (slice.slice_type === 'hero' && index === 0) {
    return true;
  }

  // First large image slice
  if (slice.slice_type === 'image_banner' && index === 0) {
    return true;
  }

  return false;
}

Usage in Slice Zone:

import { SliceZone } from '@prismicio/react';
import { isLCPSlice } from '@/utils/isLCPSlice';

export default function Page({ document }) {
  const slicesWithPriority = document.data.slices.map((slice, index) => ({
    ...slice,
    isLCP: isLCPSlice(slice, index),
  }));

  return (
    <SliceZone
      slices={slicesWithPriority}
      components={{
        hero: ({ slice }) => (
          <HeroSlice slice={slice} priority={slice.isLCP} />
        ),
      }}
    />
  );
}

Measure LCP Performance

Using Chrome DevTools

  1. Open DevTools (F12)
  2. Go to Lighthouse tab
  3. Select Performance category
  4. Click Analyze page load
  5. Check Largest Contentful Paint metric

Using Web Vitals Library

npm install web-vitals
// app/layout.js
'use client';

import { useEffect } from 'react';
import { onLCP } from 'web-vitals';

export function WebVitals() {
  useEffect(() => {
    onLCP((metric) => {
      console.log('LCP:', metric);

      // Send to analytics
      if (typeof window.gtag !== 'undefined') {
        window.gtag('event', 'web_vitals', {
          event_category: 'Web Vitals',
          event_label: 'LCP',
          value: Math.round(metric.value),
          metric_id: metric.id,
          metric_rating: metric.rating,
        });
      }
    });
  }, []);

  return null;
}

Real User Monitoring

Send LCP data to GA4:

import { onLCP } from 'web-vitals';

onLCP((metric) => {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'web_vitals',
    metric_name: 'LCP',
    metric_value: metric.value,
    metric_rating: metric.rating,
    metric_element: metric.attribution?.element || 'unknown',
  });
});

Common LCP Issues & Solutions

Issue: Large Unoptimized Hero Images

Symptoms:

  • LCP > 4 seconds
  • Large image file sizes (> 500KB)
  • Hero image is LCP element

Solution:

// Optimize hero image with Imgix parameters
const optimizedHero = asImageSrc(slice.primary.hero_image, {
  w: 1920,
  h: 1080,
  fit: 'crop',
  auto: 'format,compress',
  q: 75, // Reduce quality for faster load
});

<Image
  src={optimizedHero}
  alt={slice.primary.hero_image.alt}
  width={1920}
  height={1080}
  priority={true}
  quality={75}
/>

Issue: Images Not Preloaded

Symptoms:

  • LCP element loads late in waterfall
  • No preload for hero image

Solution: Add preload link (see Preload section above)

Issue: Wrong Image Dimensions

Symptoms:

  • Browser must resize large images
  • Layout shift occurs

Solution:

// Always specify dimensions
<Image
  src={imageField.url}
  alt={imageField.alt}
  width={imageField.dimensions.width}
  height={imageField.dimensions.height}
  priority={true}
/>

Issue: Lazy Loading LCP Image

Symptoms:

  • LCP image has loading="lazy"
  • LCP delayed until scroll

Solution:

// Don't lazy load LCP images
<Image
  src={imageField.url}
  alt={imageField.alt}
  width={imageField.dimensions.width}
  height={imageField.dimensions.height}
  priority={true} // Prevents lazy loading
  loading="eager" // Explicit eager loading
/>

Advanced Techniques

Adaptive Loading Based on Connection

'use client';

import { useEffect, useState } from 'react';
import Image from 'next/image';
import { asImageSrc } from '@prismicio/client';

export function AdaptivePrismicImage({ field, priority }) {
  const [quality, setQuality] = useState(80);

  useEffect(() => {
    if ('connection' in navigator) {
      const connection = navigator.connection;

      if (connection.effectiveType === '4g') {
        setQuality(90);
      } else if (connection.effectiveType === '3g') {
        setQuality(70);
      } else {
        setQuality(50);
      }
    }
  }, []);

  const optimizedSrc = asImageSrc(field, {
    auto: 'format,compress',
    q: quality,
    w: 1920,
  });

  return (
    <Image
      src={optimizedSrc}
      alt={field.alt || ''}
      width={field.dimensions.width}
      height={field.dimensions.height}
      priority={priority}
    />
  );
}

Blur Placeholder

// Generate blur placeholder
async function getBlurDataURL(imageUrl) {
  const blurUrl = `${imageUrl}?w=10&blur=50&q=10`;
  const response = await fetch(blurUrl);
  const buffer = await response.arrayBuffer();
  const base64 = Buffer.from(buffer).toString('base64');
  return `data:image/jpeg;base64,${base64}`;
}

// Usage
export default async function Page() {
  const document = await client.getSingle('homepage');
  const blurDataURL = await getBlurDataURL(document.data.hero_image.url);

  return (
    <Image
      src={document.data.hero_image.url}
      alt={document.data.hero_image.alt}
      width={document.data.hero_image.dimensions.width}
      height={document.data.hero_image.dimensions.height}
      priority
      placeholder="blur"
      blurDataURL={blurDataURL}
    />
  );
}

Checklist for Optimal LCP

  • Use Next.js Image or Gatsby Image for automatic optimization
  • Set priority={true} on LCP images
  • Add preload link for hero/LCP images
  • Use Imgix parameters: auto=format,compress, q=75-85
  • Specify image dimensions to prevent layout shift
  • Use responsive images with srcset and sizes
  • Avoid lazy loading LCP images
  • Compress images before uploading to Prismic
  • Use WebP format (via Imgix auto format)
  • Measure LCP with Lighthouse and Web Vitals
  • Monitor real user LCP with analytics

Next Steps