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
Method 1: Next.js with Prismic (Recommended)
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 loadingfill={true}- For responsive background imagessizesattribute - 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
- Open DevTools (F12)
- Go to Lighthouse tab
- Select Performance category
- Click Analyze page load
- 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