W Metropolis Quantum, system transportowy miasta jest zorganizowany według różnych priorytetów. Pojazdy ratunkowe mają najwyższy priorytet i zawsze mają pierwszeństwo przejazdu. Transport publiczny ma średni priorytet, a pojazdy prywatne najniższy. To inteligentne zarządzanie ruchem zapewnia, że najważniejsze zasoby zawsze docierają do celu najszybciej, podczas gdy mniej krytyczne mogą poczekać.
Podobnie w aplikacjach internetowych, różne obrazy mają różne poziomy ważności. Obraz główny (hero image) strony powinien załadować się natychmiast, podczas gdy obrazy umieszczone niżej na stronie mogą być ładowane tylko wtedy, gdy użytkownik przewinie do nich. Next.js 15 oferuje różne strategie ładowania obrazów, które pozwalają dostosować sposób ładowania obrazów do ich ważności i miejsca na stronie.
W Next.js 15 mamy do dyspozycji trzy główne strategie ładowania obrazów:
Każda z tych strategii ma swoje zastosowanie i wpływ na wydajność strony.
Leniwe ładowanie to domyślna strategia dla komponentu
Image w Next.js 15. Działa ona zgodnie z prostą zasadą:Ładuj tylko to, co użytkownik aktualnie widzi lub wkrótce zobaczy.
loading="lazy", z dodatkowymi optymalizacjami1import 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 - zdjęcie ${index + 1}`}
11 fill
12 style={{ objectFit: 'cover' }}
13 // Nie trzeba dodawać loading="lazy" - jest to wartość domyślna
14 />
15 </div>
16 ))}
17 </div>
18 );
19}Strategia eager loading jest przeciwieństwem leniwego ładowania. Zamiast czekać, aż obrazy zbliżą się do widocznego obszaru, obrazy są ładowane natychmiast podczas ładowania strony.
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 to specjalna strategia dla najważniejszych obrazów na stronie, szczególnie tych widocznych "above the fold" (w górnej części strony, widocznej bez przewijania).
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}Gdy używasz atrybutu
priority:loading="eager" dla danego obrazu<link rel="preload"> dla tego obrazupriority oszczędnie - tylko dla najważniejszych obrazówpriority tylko dla obrazów LCP (Largest Contentful Paint)priority może paradoksalnie zmniejszyć wydajność| Strategia | Atrybut | Przypadki użycia | Wpływ na wydajność | |-----------|---------|------------------|-------------------| | Lazy | loading="lazy" (domyślne) | Obrazy poniżej "fold", galerie, listy | Szybsze ładowanie początkowe, oszczędność danych | | Eager | loading="eager" | Małe strony, wszystkie obrazy ważne | Wolniejsze ładowanie początkowe, lepsza responsywność podczas przewijania | | Priority | priority | Obrazy "above the fold", hero images, główna zawartość | Lepsze LCP, szybsze wyświetlanie kluczowych obrazów |
Poza podstawowymi strategiami, Next.js 15 pozwala na bardziej zaawansowane podejścia do ładowania obrazów:
Czasami chcemy wstępnie załadować obrazy dla strony, na którą użytkownik prawdopodobnie przejdzie:
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 obrazu szczegółowego produktu po najechaniu myszą
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}Możemy dynamicznie nadawać priorytet obrazom, które są aktualnie widoczne:
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}Można łączyć strategie, aby najpierw wyświetlić obraz niskiej jakości, a następnie zastąpić go wersją wysokiej jakości:
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 ładowany natychmiast
22 />
23
24 {/* Obraz wysokiej jakości */}
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}Strategie ładowania można łączyć z technikami dostosowywania obrazów do różnych urządzeń:
1import Image from 'next/image';
2
3export default function ResponsiveHero() {
4 return (
5 <>
6 {/* Obraz dla urządzeń mobilnych */}
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 // Priorytet dla głównego obrazu
14 />
15 </div>
16
17 {/* Obraz dla tabletów */}
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 {/* Obraz dla desktopów */}
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 // Wykryj jakość połączenia
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 // Nasłuchuj zmian w jakości połączenia
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 {/* Główny obraz produktu - 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 {/* Galeria dodatkowych zdjęć - lazy loading (domyślne) */}
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} - zdjęcie ${index + 1}`}
47 fill
48 style={{ objectFit: 'cover' }}
49 sizes="(max-width: 768px) 50vw, 25vw"
50 // Domyślne 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 to zestaw metryk Google, które mierzą doświadczenie użytkownika na stronach internetowych. Wybór właściwej strategii ładowania obrazów ma bezpośredni wpływ na te metryki:
LCP mierzy czas ładowania największego elementu widocznego w viewport, którym często jest obraz.
Wpływ strategii ładowania:
CLS mierzy niestabilność układu strony podczas ładowania.
Wpływ strategii ładowania:
placeholder="blur" zapewnia najlepsze CLS, ponieważ rezerwuje przestrzeń natychmiastwidth i height lub używaj fill z kontenerem o określonych wymiarachFID mierzy czas, jaki mija od pierwszej interakcji użytkownika do momentu, gdy przeglądarka może na nią zareagować.
Wpływ strategii ładowania:
Aby upewnić się, że wybrane strategie ładowania działają optymalnie, warto mierzyć ich wpływ:
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 // Przypisz użytkownika do grupy A lub B
11 const group = Math.random() > 0.5 ? 'A' : 'B';
12 setTestGroup(group);
13
14 // Zapisz dane telemetryczne
15 const trackImageLoad = (time) => {
16 // Wysyłanie danych do systemu analitycznego
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 // Nasłuchuj na zdarzenie load obrazu
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'} // Grupa A używa priority, grupa B nie
47 />
48 </div>
49 );
50}Używaj
tylko dla obrazów above-the-fold - Google zaleca maksymalnie 3 obrazy z priority
priority na stronieDomyślnie używaj lazy loading - pozwala to oszczędzić zasoby i przyspieszyć początkowe ładowanie strony
Monitoruj metryki Web Vitals - regularnie sprawdzaj, jak strategie ładowania wpływają na wydajność
Dostosuj strategie do rodzaju strony:
priority dla głównego zdjęcia produktulazy dla większości obrazów, eager dla pierwszych kilkupriority dla hero image, lazy dla pozostałychUwzględnij urządzenia użytkowników:
lazy i mniejszych obrazóweager dla lepszego doświadczenia przewijaniaUżywaj placeholderów dla lepszego doświadczenia podczas ładowania:
priority - pokazuj placeholder typu blur lub emptylazy - używaj blur dla płynnego przejścia1export 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 {/* Promocje - eager dla pierwszych dwóch */}
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 {/* Kategorie produktów - lazy (domyślne) */}
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 // lazy loading domyślne
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 {/* Główny obraz artykułu - 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 // lazy loading domyślne dla obrazów w treści
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 {/* Powiązane artykuły - wszystkie lazy */}
44 <div className="mt-12">
45 <h2 className="text-2xl font-bold mb-6">Powiązane artykuły</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 // Wszystkie lazy (domyślne)
55 />
56 </div>
57 ))}
58 </div>
59 </div>
60 </article>
61 );
62}Wybór odpowiedniej strategii ładowania obrazów jest kluczowy dla optymalizacji wydajności i doświadczenia użytkownika. Podobnie jak w systemie transportowym Metropolis Quantum, gdzie pojazdy o różnym priorytecie poruszają się z różną pilnością, tak w aplikacji webowej obrazy mogą być ładowane z różnymi strategiami w zależności od ich ważności:
Priority loading - dla najważniejszych obrazów "above the fold", które mają bezpośredni wpływ na LCP i pierwsze wrażenie użytkownika
Eager loading - dla obrazów, które są mniej krytyczne, ale wciąż ważne, i prawdopodobnie będą potrzebne wkrótce po załadowaniu strony
Lazy loading - dla większości obrazów, szczególnie tych poniżej "fold", które będą ładowane tylko wtedy, gdy użytkownik do nich dotrze
Wybierając strategię ładowania dla swoich obrazów, uwzględnij:
Dzięki odpowiedniemu zastosowaniu strategii ładowania obrazów, możesz stworzyć aplikację, która ładuje się szybko, efektywnie zarządza zasobami i zapewnia płynne doświadczenie użytkownika - podobnie jak inteligentny system transportowy Metropolis Quantum, który zapewnia płynny przepływ pojazdów o różnych priorytetach.
W następnym rozdziale zajmiemy się optymalizacją czcionek i komponentem Font, co pozwoli nam jeszcze bardziej poprawić wydajność i estetykę naszej aplikacji Next.js 15.