Largest Contentful Paint (LCP) measures how quickly the largest visible element loads on your page. For Storyblok sites, this is often an image from the Storyblok Image Service. This guide shows how to optimize Storyblok images for optimal LCP performance.
Understanding LCP on Storyblok Sites
Common LCP Elements
- Hero Images - Large header images in hero components
- Featured Images - Blog post or article featured images
- Banner Images - Promotional banners
- Background Images - Component background images
LCP Performance Targets
- Good: LCP ≤ 2.5 seconds
- Needs Improvement: LCP 2.5-4.0 seconds
- Poor: LCP > 4.0 seconds
Method 1: Nuxt.js with Storyblok
Using Nuxt Image with Storyblok Image Service
npm install @nuxt/image
Configure nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@storyblok/nuxt',
'@nuxt/image',
],
image: {
storyblok: {
baseURL: 'https://a.storyblok.com',
},
screens: {
xs: 320,
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
xxl: 1536,
},
},
});
Optimize Hero Component:
<!-- components/HeroSection.vue -->
<template>
<section v-editable="blok" class="hero">
<NuxtImg
:src="blok.background_image.filename"
:alt="blok.background_image.alt"
provider="storyblok"
width="1920"
height="1080"
loading="eager"
:modifiers="{ quality: 80, format: 'webp', fit: 'crop' }"
sizes="100vw"
/>
<div class="hero-content">
<h1>{{ blok.title }}</h1>
</div>
</section>
</template>
<script setup>
const { blok } = defineProps(['blok']);
</script>
Key optimizations:
loading="eager"- Prevents lazy loading for LCP imagesprovider="storyblok"- Uses Storyblok Image Servicemodifiers- Optimizes quality, format, and fitsizes- Provides responsive sizing hints
Method 2: Next.js with Storyblok
Using Next.js Image Component
Configure next.config.js:
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'a.storyblok.com',
pathname: '/**',
},
],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
formats: ['image/webp'],
},
};
Optimize Hero Component:
// components/HeroSection.js
import Image from 'next/image';
import { storyblokEditable } from '@storyblok/react';
export default function HeroSection({ blok }) {
// Transform Storyblok image URL
const imageUrl = blok.background_image.filename;
return (
<section {...storyblokEditable(blok)}>
<div className="hero-image">
<Image
src={imageUrl}
alt={blok.background_image.alt || ''}
fill
priority
quality={80}
sizes="100vw"
style={{ objectFit: 'cover' }}
/>
</div>
<div className="hero-content">
<h1>{blok.title}</h1>
</div>
</section>
);
}
Storyblok Image Service Optimization
Storyblok uses a powerful Image Service. Leverage URL parameters:
Key Image Service Parameters
// Optimized image URL
const optimizedUrl = `${imageUrl}/m/1920x1080/filters:quality(80):format(webp)`;
Common parameters:
/m/WIDTHxHEIGHT- Resize to dimensionsfilters:quality(80)- Set quality (1-100)filters:format(webp)- Force WebP formatfilters:fill(crop)- Crop to fill dimensionsfilters:blur(10)- Apply blur (for placeholders)
Responsive Image Helper
// utils/storyblokImage.ts
export const getStoryblokImage = (
filename: string,
options: {
width?: number;
height?: number;
quality?: number;
format?: 'webp' | 'jpg' | 'png';
fit?: 'crop' | 'contain';
} = {}
) => {
const {
width = 1920,
height,
quality = 80,
format = 'webp',
fit = 'crop',
} = options;
const size = height ? `${width}x${height}` : `${width}x0`;
const filters = [
`quality(${quality})`,
`format(${format})`,
fit && `fill(${fit})`,
].filter(Boolean).join(':');
return `${filename}/m/${size}/filters:${filters}`;
};
Usage:
<template>
<img
:src="optimizedImage"
:alt="blok.image.alt"
loading="eager"
/>
</template>
<script setup>
const { blok } = defineProps(['blok']);
const optimizedImage = getStoryblokImage(blok.image.filename, {
width: 1920,
height: 1080,
quality: 80,
format: 'webp',
});
</script>
Preload Critical Images
Nuxt 3
In app.vue or page-specific:
<script setup>
useHead({
link: [
{
rel: 'preload',
as: 'image',
href: getStoryblokImage(heroImage, { width: 1920, quality: 80 }),
type: 'image/webp',
},
],
});
</script>
Next.js
// app/layout.js or page
export const metadata = {
other: {
'link-preload': `<${heroImageUrl}/m/1920x0/filters:quality(80):format(webp)>; rel=preload; as=image; type=image/webp`,
},
};
Measure LCP Performance
Using Web Vitals Library
npm install web-vitals
// plugins/web-vitals.client.ts (Nuxt 3)
import { onLCP } from 'web-vitals';
export default defineNuxtPlugin(() => {
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_rating: metric.rating,
});
}
});
});
Common LCP Issues & Solutions
Issue: Large Unoptimized Hero Images
Symptoms:
- LCP > 4 seconds
- Large file sizes (> 500KB)
Solution:
// Force WebP and reduce quality
const optimized = getStoryblokImage(image.filename, {
width: 1920,
height: 1080,
quality: 75, // Reduce for faster load
format: 'webp',
fit: 'crop',
});
Issue: Images Not Preloaded
Symptoms:
- LCP element loads late in waterfall
Solution: Add preload link (see Preload section)
Issue: Lazy Loading LCP Image
Symptoms:
- LCP delayed until scroll
Solution:
<NuxtImg
:src="image"
loading="eager" <!-- Critical! -->
provider="storyblok"
/>
Advanced Techniques
Blur Placeholder
<template>
<div class="image-container">
<img
v-if="!imageLoaded"
:src="blurPlaceholder"
class="blur-placeholder"
alt=""
/>
<img
:src="fullImage"
:alt="blok.image.alt"
@load="imageLoaded = true"
loading="eager"
/>
</div>
</template>
<script setup>
const { blok } = defineProps(['blok']);
const imageLoaded = ref(false);
const blurPlaceholder = getStoryblokImage(blok.image.filename, {
width: 20,
quality: 10,
});
const fullImage = getStoryblokImage(blok.image.filename, {
width: 1920,
quality: 80,
format: 'webp',
});
</script>
Checklist for Optimal LCP
- Use Nuxt Image or Next.js Image for optimization
- Set
loading="eager"orpriorityon LCP images - Add preload link for hero/LCP images
- Use Storyblok Image Service parameters:
quality(75-85),format(webp) - Specify image dimensions
- Use responsive images with sizes attribute
- Avoid lazy loading LCP images
- Compress images before uploading to Storyblok
- Measure LCP with Lighthouse and Web Vitals
- Monitor real user LCP with analytics