We use cookies to enhance your experience on the site
CodeWorlds

Image Loading Strategies (lazy, eager, priority) in Next.js 15

In Metropolis Quantum, the city's transportation system is organized according to different priorities. Emergency vehicles have the highest priority and always have the right of way. Public transport has medium priority, and private vehicles have the lowest. This intelligent traffic management ensures that the most important resources always reach their destination fastest, while less critical ones can wait.

Similarly in web applications, different images have different levels of importance. The hero image of the page should load immediately, while images placed lower on the page can be loaded only when the user scrolls to them. Next.js 15 offers different image loading strategies that allow you to customize how images are loaded based on their importance and position on the page.

Three Main Image Loading Strategies

In Next.js 15, we have three main image loading strategies available:

  1. Lazy - default strategy, images are loaded only when they approach the visible area (viewport)
  2. Eager - images are loaded immediately, regardless of their position on the page
  3. Priority - images are loaded with the highest priority, ideal for "above the fold" images

Each of these strategies has its use case and impact on page performance.

Lazy Loading

Lazy loading is the default strategy for the

Image
component in Next.js 15. It works according to a simple principle:

Load only what the user currently sees or will see soon.

How Does Lazy Loading Work?

  1. Next.js automatically delays loading images that are outside the visible area (viewport)
  2. Images are loaded only when the user scrolls the page and they approach the visible area
  3. Behind the scenes, the modern browser feature
    loading="lazy"
    is used, with additional optimizations

Lazy Loading Usage Example:

1import Image from 'next/image';
2
3export default function QuantumGallery() {
4  return (
5    <div className="gallery">
6      {Array.from({ length: 20 }).map((_, index) => (
7        <div key={index} className="relative h-64 w-full">
8          <Image
9            src={`/images/quantum-city-${index + 1}.jpg`}
10            alt={`Metropolis Quantum - photo ${index + 1}`}
11            fill
12            style={{ objectFit: 'cover' }}
13            // No need to add loading="lazy" - it is the default value
14          />
15        </div>
16      ))}
17    </div>
18  );
19}

Zalety lazy loading:

  1. Faster initial page loading - only critical images are loaded immediately
  2. Less data consumption - images that the user never reaches are not loaded
  3. Better performance - the browser can focus resources on elements that are currently important

Scenarios Where Lazy Loading is Worth Using:

  • Long pages with many images
  • Photo galleries
  • Product lists
  • Image carousels
  • Infinite scroll pages

Eager Loading

The eager loading strategy is the opposite of lazy loading. Instead of waiting for images to approach the visible area, images are loaded immediately during page loading.

How to Set Eager Loading:

1import Image from 'next/image';
2
3export default function QuantumLogo() {
4  return (
5    <Image
6      src="/images/quantum-logo.png"
7      alt="Logo Metropolis Quantum"
8      width={200}
9      height={100}
10      loading="eager"
11    />
12  );
13}

Zalety eager loading:

  1. Immediate availability - all images are available right after page loading
  2. No delays - the user does not have to wait for images to load while scrolling
  3. More predictable behavior - all resources are loaded in one cycle

Scenarios Where Eager Loading is Worth Using:

  • Short pages with a small number of images
  • Pages where all images are important for context
  • Applications where smooth scrolling without delays is crucial
  • Offline-first applications that want to load all resources at once

Priority Loading

Priority loading is a special strategy for the most important images on the page, especially those visible "above the fold" (in the upper part of the page, visible without scrolling).

How to Set Priority Loading:

1import Image from 'next/image';
2
3export default function QuantumHero() {
4  return (
5    <Image
6      src="/images/quantum-hero.jpg"
7      alt="Witaj w Metropolis Quantum"
8      width={1920}
9      height={1080}
10      priority
11    />
12  );
13}

When you use the

priority
attribute:

  1. Next.js automatycznie ustawia
    loading="eager"
    dla danego obrazu
  2. Dodaje specjalny znacznik
    <link rel="preload">
    dla tego obrazu
  3. The image receives higher priority in the browser's loading queue

Zalety priority loading:

  1. Lower LCP (Largest Contentful Paint) - a key Core Web Vitals metric
  2. Faster display of the most important images - better perceived performance
  3. Core Web Vitals optimization - positive impact on SEO

Scenarios Where Priority is Worth Using:

  • Main page hero images
  • Logo marki
  • Product images on detail pages
  • User avatars in social applications
  • Any images that constitute the main content of the page

Key Notes About the Priority Attribute:

  • Use
    priority
    sparingly - only for the most important images
  • Google recommends using
    priority
    only for LCP (Largest Contentful Paint) images
  • Too many images with
    priority
    can paradoxically decrease performance

Loading Strategy Comparison

| Strategy | Attribute | Use Cases | Performance Impact | |-----------|---------|------------------|-------------------| | Lazy | loading="lazy" (default) | Images below the fold, galleries, lists | Faster initial loading, data savings | | Eager | loading="eager" | Small pages, all images important | Slower initial loading, better responsiveness while scrolling | | Priority | priority | "Above the fold" images, hero images, main content | Better LCP, faster display of key images |

Advanced Image Loading Strategies

Beyond the basic strategies, Next.js 15 allows for more advanced approaches to image loading:

1. Preloading Important Images with useRouter

Sometimes we want to preload images for a page the user will likely navigate to:

1'use client';
2
3import { useEffect } from 'react';
4import { useRouter } from 'next/navigation';
5import Image from 'next/image';
6
7export default function ProductCard({ product, isPriority }) {
8  const router = useRouter();
9  
10  useEffect(() => {
11    // Preload product detail image on mouse hover
12    const preloadDetailImage = () => {
13      router.prefetch(`/products/${product.id}`);
14    };
15    
16    const cardElement = document.getElementById(`product-card-${product.id}`);
17    if (cardElement) {
18      cardElement.addEventListener('mouseenter', preloadDetailImage);
19      return () => {
20        cardElement.removeEventListener('mouseenter', preloadDetailImage);
21      };
22    }
23  }, [product.id, router]);
24  
25  return (
26    <div 
27      id={`product-card-${product.id}`}
28      className="relative h-64 overflow-hidden rounded-lg"
29      onClick={() => router.push(`/products/${product.id}`)}
30    >
31      <Image
32        src={product.thumbnail}
33        alt={product.title}
34        fill
35        style={{ objectFit: 'cover' }}
36        priority={isPriority}
37      />
38      <div className="absolute bottom-0 w-full bg-black bg-opacity-60 p-4">
39        <h3 className="text-white font-bold">{product.title}</h3>
40      </div>
41    </div>
42  );
43}

2. Dynamic Priority Switching Based on Visibility

We can dynamically assign priority to images that are currently visible:

1'use client';
2
3import { useState, useEffect, useRef } from 'react';
4import Image from 'next/image';
5
6export default function DynamicPriorityImage({ src, alt }) {
7  const [isPriority, setIsPriority] = useState(false);
8  const imageRef = useRef(null);
9  
10  useEffect(() => {
11    const observer = new IntersectionObserver(
12      ([entry]) => {
13        if (entry.isIntersecting) {
14          setIsPriority(true);
15          observer.disconnect();
16        }
17      },
18      { threshold: 0.1 }
19    );
20    
21    if (imageRef.current) {
22      observer.observe(imageRef.current);
23    }
24    
25    return () => {
26      observer.disconnect();
27    };
28  }, []);
29  
30  return (
31    <div ref={imageRef} className="relative h-64 w-full">
32      <Image
33        src={src}
34        alt={alt}
35        fill
36        style={{ objectFit: 'cover' }}
37        priority={isPriority}
38      />
39    </div>
40  );
41}

3. Progressive Image Loading

You can combine strategies to first display a low-quality image and then replace it with a high-quality version:

1'use client';
2
3import { useState } from 'react';
4import Image from 'next/image';
5
6export default function ProgressiveImage() {
7  const [isLoaded, setIsLoaded] = useState(false);
8
9  return (
10    <div className="relative h-96 w-full">
11      {/* Niskokwalitywowy placeholder */}
12      <Image
13        src="/images/quantum-city-low.jpg"
14        alt="Metropolis Quantum"
15        fill
16        style={{
17          objectFit: 'cover',
18          opacity: isLoaded ? 0 : 1,
19          transition: 'opacity 0.5s ease-in-out'
20        }}
21        priority // Placeholder loaded immediately
22      />
23      
24      {/* High quality image */}
25      <Image
26        src="/images/quantum-city-high.jpg"
27        alt="Metropolis Quantum"
28        fill
29        style={{ 
30          objectFit: 'cover',
31          opacity: isLoaded ? 1 : 0,
32          transition: 'opacity 0.5s ease-in-out'
33        }}
34        onLoadingComplete={() => setIsLoaded(true)}
35      />
36    </div>
37  );
38}

Image Optimization for Different Devices

Loading strategies can be combined with techniques for adapting images to different devices:

1. Responsive Images with Different Sources

1import Image from 'next/image';
2
3export default function ResponsiveHero() {
4  return (
5    <>
6      {/* Image for mobile devices */}
7      <div className="md:hidden">
8        <Image
9          src="/images/hero-mobile.jpg"
10          alt="Metropolis Quantum"
11          width={640}
12          height={960}
13          priority // Priority for the main image
14        />
15      </div>
16      
17      {/* Image for tablets */}
18      <div className="hidden md:block lg:hidden">
19        <Image
20          src="/images/hero-tablet.jpg"
21          alt="Metropolis Quantum"
22          width={1024}
23          height={768}
24          priority
25        />
26      </div>
27      
28      {/* Image for desktops */}
29      <div className="hidden lg:block">
30        <Image
31          src="/images/hero-desktop.jpg"
32          alt="Metropolis Quantum"
33          width={1920}
34          height={1080}
35          priority
36        />
37      </div>
38    </>
39  );
40}

2. Adaptive Loading Based on Connection

1'use client';
2
3import { useState, useEffect } from 'react';
4import Image from 'next/image';
5
6export default function AdaptiveLoadingImage() {
7  const [connectionType, setConnectionType] = useState('high');
8  
9  useEffect(() => {
10    // Detect connection quality
11    const connection = navigator.connection || 
12                      navigator.mozConnection || 
13                      navigator.webkitConnection;
14    
15    if (connection) {
16      if (connection.effectiveType === '4g' && !connection.saveData) {
17        setConnectionType('high');
18      } else {
19        setConnectionType('low');
20      }
21      
22      // Listen for changes in connection quality
23      const updateConnectionStatus = () => {
24        if (connection.effectiveType === '4g' && !connection.saveData) {
25          setConnectionType('high');
26        } else {
27          setConnectionType('low');
28        }
29      };
30      
31      connection.addEventListener('change', updateConnectionStatus);
32      return () => connection.removeEventListener('change', updateConnectionStatus);
33    }
34  }, []);
35  
36  return (
37    <div className="relative h-64 w-full">
38      <Image
39        src={connectionType === 'high' ? '/images/high-quality.jpg' : '/images/low-quality.jpg'}
40        alt="Adaptacyjny obraz"
41        fill
42        style={{ objectFit: 'cover' }}
43        loading={connectionType === 'high' ? 'eager' : 'lazy'}
44        quality={connectionType === 'high' ? 90 : 50}
45      />
46    </div>
47  );
48}

Implementing Advanced Loading Strategies in a Real Application

Example: Product Page with Different Loading Strategies

1// app/products/[id]/page.tsx
2import Image from 'next/image';
3import { getProduct, getRelatedProducts } from '@/lib/api';
4import RelatedProducts from '@/components/RelatedProducts';
5
6export default async function ProductPage({ params }) {
7  const product = await getProduct((await params).id);
8  const relatedProducts = await getRelatedProducts((await params).id);
9  
10  return (
11    <div className="container mx-auto px-4 py-8">
12      <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
13        {/* Main product image - priority loading */}
14        <div className="relative aspect-square">
15          <Image
16            src={product.mainImage}
17            alt={product.name}
18            fill
19            style={{ objectFit: 'contain' }}
20            priority
21            sizes="(max-width: 768px) 100vw, 50vw"
22          />
23        </div>
24        
25        <div>
26          <h1 className="text-3xl font-bold mb-4">{product.name}</h1>
27          <p className="text-xl mb-4">{product.price}</p>
28          <div className="prose max-w-none">
29            <p>{product.description}</p>
30          </div>
31          
32          <button className="mt-6 bg-blue-600 text-white px-6 py-3 rounded-md">
33            Dodaj do koszyka
34          </button>
35        </div>
36      </div>
37      
38      {/* Additional photos gallery - lazy loading (default) */}
39      <div className="mt-12">
40        <h2 className="text-2xl font-bold mb-6">Galeria produktu</h2>
41        <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
42          {product.gallery.map((image, index) => (
43            <div key={index} className="relative aspect-square">
44              <Image
45                src={image.url}
46                alt={`${product.name} - photo ${index + 1}`}
47                fill
48                style={{ objectFit: 'cover' }}
49                sizes="(max-width: 768px) 50vw, 25vw"
50                // Default lazy loading
51              />
52            </div>
53          ))}
54        </div>
55      </div>
56      
57      {/* Podobne produkty - eager loading dla pierwszego elementu */}
58      <div className="mt-12">
59        <h2 className="text-2xl font-bold mb-6">Podobne produkty</h2>
60        <RelatedProducts products={relatedProducts} />
61      </div>
62    </div>
63  );
64}
1// components/RelatedProducts.tsx
2'use client';
3
4import Image from 'next/image';
5import Link from 'next/link';
6import { useRef, useEffect } from 'react';
7
8export default function RelatedProducts({ products }) {
9  const containerRef = useRef(null);
10  
11  useEffect(() => {
12    // Preload images when hovering over the section
13    const preloadImages = () => {
14      products.forEach(product => {
15        const img = new Image();
16        img.src = product.image;
17      });
18    };
19    
20    const container = containerRef.current;
21    if (container) {
22      container.addEventListener('mouseenter', preloadImages, { once: true });
23      return () => {
24        container.removeEventListener('mouseenter', preloadImages);
25      };
26    }
27  }, [products]);
28  
29  return (
30    <div ref={containerRef} className="grid grid-cols-2 md:grid-cols-4 gap-4">
31      {products.map((product, index) => (
32        <Link key={product.id} href={`/products/${product.id}`}>
33          <div className="relative aspect-square rounded-lg overflow-hidden">
34            <Image
35              src={product.image}
36              alt={product.name}
37              fill
38              style={{ objectFit: 'cover' }}
39              // Eager loading tylko dla pierwszego produktu
40              loading={index === 0 ? 'eager' : 'lazy'}
41              sizes="(max-width: 768px) 50vw, 25vw"
42            />
43            <div className="absolute bottom-0 w-full bg-black bg-opacity-60 p-2">
44              <p className="text-white text-sm font-medium truncate">
45                {product.name}
46              </p>
47            </div>
48          </div>
49        </Link>
50      ))}
51    </div>
52  );
53}

Impact of Loading Strategies on Core Web Vitals

Core Web Vitals is a set of Google metrics that measure user experience on websites. Choosing the right image loading strategy has a direct impact on these metrics:

1. LCP (Largest Contentful Paint)

LCP measures the loading time of the largest visible element in the viewport, which is often an image.

Impact of loading strategies:

  • Priority loading for the main image can significantly improve LCP
  • Eager loading for all images can worsen LCP by dispersing resources
  • Lazy loading for images below the fold does not affect LCP but improves overall performance

2. CLS (Cumulative Layout Shift)

CLS measures page layout instability during loading.

Impact of loading strategies:

  • All strategies can minimize CLS if images have specified dimensions
  • Priority loading with
    placeholder="blur"
    provides the best CLS as it reserves space immediately
  • Always specify
    width
    and
    height
    or use
    fill
    with a container of defined dimensions

3. FID (First Input Delay)

FID measures the time from the user's first interaction to the moment when the browser can respond to it.

Impact of loading strategies:

  • Lazy loading can improve FID by reducing browser work during loading
  • Eager loading can worsen FID by increasing main thread load
  • Priority loading for key images can be a compromise

Measuring and Optimizing Loading Strategies

To ensure that the chosen loading strategies work optimally, it is worth measuring their impact:

1. Performance Measurement Tools

  • Lighthouse - comprehensive performance analysis tool
  • Chrome DevTools Performance - detailed resource loading analysis
  • WebPageTest - performance testing under various conditions
  • Core Web Vitals w Google Search Console

2. A/B Testing of Loading Strategies

1'use client';
2
3import { useState, useEffect } from 'react';
4import Image from 'next/image';
5
6export default function ABTestImage({ src, alt }) {
7  const [testGroup, setTestGroup] = useState('');
8  
9  useEffect(() => {
10    // Assign user to group A or B
11    const group = Math.random() > 0.5 ? 'A' : 'B';
12    setTestGroup(group);
13    
14    // Save dane telemetryczne
15    const trackImageLoad = (time) => {
16      // Sending data to the analytics system
17      console.log(`Image loaded in ${time}ms (Group ${group})`);
18    };
19    
20    const startTime = performance.now();
21    const onLoadComplete = () => {
22      const loadTime = performance.now() - startTime;
23      trackImageLoad(loadTime);
24    };
25    
26    // Listen for the image load event
27    const imageEl = document.getElementById('test-image');
28    if (imageEl) {
29      imageEl.addEventListener('load', onLoadComplete);
30      return () => {
31        imageEl.removeEventListener('load', onLoadComplete);
32      };
33    }
34  }, []);
35  
36  if (!testGroup) return null;
37  
38  return (
39    <div className="relative h-96 w-full">
40      <Image
41        id="test-image"
42        src={src}
43        alt={alt}
44        fill
45        style={{ objectFit: 'cover' }}
46        priority={testGroup === 'A'} // Group A uses priority, group B does not
47      />
48    </div>
49  );
50}

Best Practices for Image Loading Strategies

  1. Use

    priority
    only for above-the-fold images - Google recommends a maximum of 3 images with
    priority
    per page

  2. Use lazy loading by default - this saves resources and speeds up initial page loading

  3. Monitor Web Vitals metrics - regularly check how loading strategies affect performance

  4. Dostosuj strategie do rodzaju strony:

    • Product pages:
      priority
      for the main product photo
    • Category pages:
      lazy
      for most images,
      eager
      for the first few
    • Homepage:
      priority
      for hero image,
      lazy
      for the rest
  5. Consider user devices:

    • Mobile users often have slower connections - it is even more important to use
      lazy
      and smaller images
    • Desktop users can benefit from
      eager
      for a better scrolling experience
  6. Use placeholders for a better loading experience:

    • For images with
      priority
      - show a
      blur
      or
      empty
      placeholder
    • For images with
      lazy
      - use
      blur
      for a smooth transition

Scenarios for Different Strategies

1. E-commerce Homepage

1export default function Homepage() {
2  return (
3    <div>
4      {/* Hero section - priority */}
5      <div className="relative h-[80vh]">
6        <Image
7          src="/images/hero.jpg"
8          alt="Witaj w Quantum Shop"
9          fill
10          style={{ objectFit: 'cover' }}
11          priority
12        />
13      </div>
14      
15      {/* Promotions - eager for the first two */}
16      <div className="grid grid-cols-2 md:grid-cols-4 gap-4 my-8">
17        {promotions.map((promo, index) => (
18          <div key={promo.id} className="relative aspect-square">
19            <Image
20              src={promo.image}
21              alt={promo.title}
22              fill
23              style={{ objectFit: 'cover' }}
24              loading={index < 2 ? 'eager' : 'lazy'}
25            />
26          </div>
27        ))}
28      </div>
29      
30      {/* Product categories - lazy (default) */}
31      <div className="grid grid-cols-2 md:grid-cols-3 gap-6 my-12">
32        {categories.map(category => (
33          <div key={category.id} className="relative aspect-video">
34            <Image
35              src={category.image}
36              alt={category.name}
37              fill
38              style={{ objectFit: 'cover' }}
39              // default lazy loading
40            />
41          </div>
42        ))}
43      </div>
44    </div>
45  );
46}

2. Blog with Long Articles

1export default function BlogPost({ post }) {
2  return (
3    <article className="max-w-3xl mx-auto py-8">
4      {/* Main article image - priority */}
5      <div className="relative aspect-video mb-8">
6        <Image
7          src={post.featuredImage}
8          alt={post.title}
9          fill
10          style={{ objectFit: 'cover' }}
11          priority
12        />
13      </div>
14      
15      <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
16      <div className="prose max-w-none">
17        {post.content.map((block, index) => {
18          if (block.type === 'paragraph') {
19            return <p key={index}>{block.content}</p>;
20          }
21          if (block.type === 'image') {
22            return (
23              <div key={index} className="relative aspect-video my-8">
24                <Image
25                  src={block.url}
26                  alt={block.caption || ''}
27                  fill
28                  style={{ objectFit: 'contain' }}
29                  // default lazy loading for images in content
30                />
31                {block.caption && (
32                  <p className="text-sm text-gray-500 mt-2 text-center">
33                    {block.caption}
34                  </p>
35                )}
36              </div>
37            );
38          }
39          return null;
40        })}
41      </div>
42      
43      {/* Related articles - all lazy */}
44      <div className="mt-12">
45        <h2 className="text-2xl font-bold mb-6">Related Articles</h2>
46        <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
47          {post.relatedPosts.map(relatedPost => (
48            <div key={relatedPost.id} className="relative aspect-video">
49              <Image
50                src={relatedPost.thumbnail}
51                alt={relatedPost.title}
52                fill
53                style={{ objectFit: 'cover' }}
54                // All lazy (default)
55              />
56            </div>
57          ))}
58        </div>
59      </div>
60    </article>
61  );
62}

Summary

Choosing the right image loading strategy is crucial for optimizing performance and user experience. Just like in the transportation system of Metropolis Quantum, where vehicles with different priorities move with varying urgency, in a web application images can be loaded with different strategies depending on their importance:

  1. Priority loading - for the most important "above the fold" images that have a direct impact on LCP and the user's first impression

  2. Eager loading - for images that are less critical but still important, and will likely be needed soon after page loading

  3. Lazy loading - for most images, especially those below the fold, which will be loaded only when the user reaches them

When choosing a loading strategy for your images, consider:

  • Image visibility without scrolling
  • Impact on key performance metrics
  • Content type and its importance
  • Your users' devices and internet connections

By properly applying image loading strategies, you can create an application that loads quickly, efficiently manages resources, and provides a smooth user experience - just like the intelligent transportation system of Metropolis Quantum, which ensures smooth flow of vehicles with different priorities.

In the next chapter, we will focus on font optimization and the Font component, which will allow us to further improve the performance and aesthetics of our Next.js 15 application.

Go to CodeWorlds