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.
In Next.js 15, we have three main image loading strategies available:
Each of these strategies has its use case and impact on page performance.
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.
loading="lazy" is used, with additional optimizations1import 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}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.
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}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).
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:loading="eager" dla danego obrazu<link rel="preload"> dla tego obrazupriority sparingly - only for the most important imagespriority only for LCP (Largest Contentful Paint) imagespriority can paradoxically decrease performance| 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 |
Beyond the basic strategies, Next.js 15 allows for more advanced approaches to image loading:
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}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}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}Loading strategies can be combined with techniques for adapting images to different devices:
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}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}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}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:
LCP measures the loading time of the largest visible element in the viewport, which is often an image.
Impact of loading strategies:
CLS measures page layout instability during loading.
Impact of loading strategies:
placeholder="blur" provides the best CLS as it reserves space immediatelywidth and height or use fill with a container of defined dimensionsFID measures the time from the user's first interaction to the moment when the browser can respond to it.
Impact of loading strategies:
To ensure that the chosen loading strategies work optimally, it is worth measuring their impact:
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}Use
only for above-the-fold images - Google recommends a maximum of 3 images with priority
priority per pageUse lazy loading by default - this saves resources and speeds up initial page loading
Monitor Web Vitals metrics - regularly check how loading strategies affect performance
Dostosuj strategie do rodzaju strony:
priority for the main product photolazy for most images, eager for the first fewpriority for hero image, lazy for the restConsider user devices:
lazy and smaller imageseager for a better scrolling experienceUse placeholders for a better loading experience:
priority - show a blur or empty placeholderlazy - use blur for a smooth transition1export 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}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}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:
Priority loading - for the most important "above the fold" images that have a direct impact on LCP and the user's first impression
Eager loading - for images that are less critical but still important, and will likely be needed soon after page loading
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:
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.