Fix Storyblok LCP Issues | OpsBlu Docs

Fix Storyblok LCP Issues

Speed up Storyblok LCP by using Image Service transforms, preloading hero component assets, and enabling ISR on your frontend.

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 images
  • provider="storyblok" - Uses Storyblok Image Service
  • modifiers - Optimizes quality, format, and fit
  • sizes - 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 dimensions
  • filters:quality(80) - Set quality (1-100)
  • filters:format(webp) - Force WebP format
  • filters:fill(crop) - Crop to fill dimensions
  • filters: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" or priority on 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

Next Steps