CodeWorlds

Strategie ładowania obrazów (lazy, eager, priority) w Next.js 16

Kurs Next.js · Moduł 3: Stylowanie i optymalizacja wizualna

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 16 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.

Trzy główne strategie ładowania obrazów

W Next.js 16 mamy do dyspozycji trzy główne strategie ładowania obrazów:

  1. Lazy (leniwe) - domyślna strategia, obrazy są ładowane tylko wtedy, gdy zbliżają się do widocznego obszaru (viewport)
  2. Eager (gorliwe) - obrazy są ładowane natychmiast, niezależnie od ich pozycji na stronie
  3. Priority (priorytetowe) - obrazy są ładowane z najwyższym priorytetem, idealnie dla obrazów "above the fold". W Next.js 16 włączasz tę strategię atrybutem preload, który zastąpił przestarzały atrybut priority

Każda z tych strategii ma swoje zastosowanie i wpływ na wydajność strony.

Lazy Loading (leniwe ładowanie)

Leniwe ładowanie to domyślna strategia dla komponentu Image w Next.js 16. Działa ona zgodnie z prostą zasadą:

Ładuj tylko to, co użytkownik aktualnie widzi lub wkrótce zobaczy.

Jak działa lazy loading?

  1. Next.js automatycznie opóźnia ładowanie obrazów, które znajdują się poza widocznym obszarem (viewport)
  2. Obrazy są ładowane dopiero wtedy, gdy użytkownik przewinie stronę i zbliżą się do widocznego obszaru
  3. W tle używana jest nowoczesna funkcja przeglądarki loading="lazy", z dodatkowymi optymalizacjami

Przykład użycia lazy loading:

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 - 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}

Zalety lazy loading:

  1. Szybsze wstępne ładowanie strony - tylko krytyczne obrazy są ładowane natychmiast
  2. Mniejsze zużycie danych - obrazy, do których użytkownik nigdy nie dotrze, nie są ładowane
  3. Lepsza wydajność - przeglądarka może skupić zasoby na elementach, które są aktualnie ważne

Scenariusze, w których warto używać lazy loading:

  • Długie strony z wieloma obrazami
  • Galerie zdjęć
  • Listy produktów
  • Karuzele obrazów
  • Strony z nieskończonym przewijaniem

Eager Loading (gorliwe ładowanie)

Strategia eager loading jest przeciwieństwem leniwego ładowania. Zamiast czekać, aż obrazy zbliżą się do widocznego obszaru, obrazy są ładowane natychmiast podczas ładowania strony.

Jak ustawić 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. Natychmiastowa dostępność - wszystkie obrazy są dostępne od razu po załadowaniu strony
  2. Brak opóźnień - użytkownik nie musi czekać na ładowanie obrazów podczas przewijania
  3. Bardziej przewidywalne zachowanie - wszystkie zasoby są ładowane w jednym cyklu

Scenariusze, w których warto używać eager loading:

  • Krótkie strony z niewielką liczbą obrazów
  • Strony, gdzie wszystkie obrazy są ważne dla kontekstu
  • Aplikacje, gdzie płynne przewijanie bez opóźnień jest kluczowe
  • Aplikacje offline-first, które chcą załadować wszystkie zasoby od razu

Priority Loading (priorytetowe ładowanie)

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).

Jak ustawić 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      preload
11    />
12  );
13}

Gdy używasz atrybutu preload (w Next.js 15 i starszych nazywał się priority):

  1. Next.js dodaje do <head> znacznik <link rel="preload"> dla tego obrazu
  2. Przeglądarka zaczyna pobierać obraz od razu, zanim dotrze do niego w treści strony
  3. Największy element strony (LCP) pojawia się szybciej

Stary atrybut priority w Next.js 16 wciąż działa, ale jest przestarzały. Dokumentacja radzi też, by preload stosować tylko wtedy, gdy obraz na pewno jest elementem LCP; w pozostałych przypadkach wybierz loading="eager" albo fetchPriority="high".

Zalety priority loading:

  1. Niższe LCP (Largest Contentful Paint) - kluczowa metryka Core Web Vitals
  2. Szybsze wyświetlanie najważniejszych obrazów - lepsza percepcja wydajności
  3. Optymalizacja dla Core Web Vitals - pozytywny wpływ na SEO

Scenariusze, w których warto używać priority:

  • Główne obrazy (hero images) stron
  • Logo marki
  • Obrazy produktów na stronach szczegółowych
  • Awatary użytkowników w aplikacjach społecznościowych
  • Wszelkie obrazy, które stanowią główną zawartość strony

Kluczowe uwagi dotyczące atrybutu preload:

  • Używaj preload oszczędnie - tylko dla najważniejszych obrazów
  • Dokumentacja Next.js zaleca preload tylko dla obrazu LCP (Largest Contentful Paint), a nie dla kilku obrazów, które mogą nim być zależnie od szerokości ekranu
  • Zbyt wiele obrazów z preload może paradoksalnie zmniejszyć wydajność

Porównanie strategii ładowania

| 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 | preload (dawniej priority) | Obrazy "above the fold", hero images, główna zawartość | Lepsze LCP, szybsze wyświetlanie kluczowych obrazów |

Zaawansowane strategie ładowania obrazów

Poza podstawowymi strategiami, Next.js 16 pozwala na bardziej zaawansowane podejścia do ładowania obrazów:

1. Preload ważnych obrazów z useRouter

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        preload={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. Dynamiczne przełączanie priority w zależności od widoczności

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        preload={isPriority}
38      />
39    </div>
40  );
41}

3. Progresywne ładowanie obrazów

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        preload // 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        onLoad={() => setIsLoaded(true)}
35      />
36    </div>
37  );
38}

Optymalizacja obrazów dla różnych urządzeń

Strategie ładowania można łączyć z technikami dostosowywania obrazów do różnych urządzeń:

1. Responsywne obrazy z different źródłami

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          preload // 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          preload
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          preload
36        />
37      </div>
38    </>
39  );
40}

2. Adaptacyjne ładowanie w zależności od łącza

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}

Pamiętaj, że w Next.js 16 obie wartości quality (90 i 50) muszą znaleźć się na liście images.qualities w next.config. Domyślnie ta lista zawiera tylko 75, więc bez jej rozszerzenia oba warianty dostaną jakość 75.

Implementacja zaawansowanych strategii ładowania w realnej aplikacji

Przykład: Strona produktowa z różnymi strategiami ładowania

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            preload
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}

Wpływ strategii ładowania na Core Web Vitals

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:

1. LCP (Largest Contentful Paint)

LCP mierzy czas ładowania największego elementu widocznego w viewport, którym często jest obraz.

Wpływ strategii ładowania:

  • Priority loading dla głównego obrazu może znacząco poprawić LCP
  • Eager loading dla wszystkich obrazów może pogorszyć LCP przez rozproszenie zasobów
  • Lazy loading dla obrazów poniżej "fold" nie wpływa na LCP, ale poprawia ogólną wydajność

2. CLS (Cumulative Layout Shift)

CLS mierzy niestabilność układu strony podczas ładowania.

Wpływ strategii ładowania:

  • Wszystkie strategie mogą minimalizować CLS, jeśli obrazy mają określone wymiary
  • Priority loading z placeholder="blur" zapewnia najlepsze CLS, ponieważ rezerwuje przestrzeń natychmiast
  • Zawsze określaj width i height lub używaj fill z kontenerem o określonych wymiarach

3. FID (First Input Delay)

FID mierzy czas, jaki mija od pierwszej interakcji użytkownika do momentu, gdy przeglądarka może na nią zareagować.

Wpływ strategii ładowania:

  • Lazy loading może poprawić FID przez zmniejszenie pracy przeglądarki podczas ładowania
  • Eager loading może pogorszyć FID przez zwiększenie obciążenia głównego wątku
  • Priority loading dla kluczowych obrazów może być kompromisem

Pomiary i optymalizacja strategii ładowania

Aby upewnić się, że wybrane strategie ładowania działają optymalnie, warto mierzyć ich wpływ:

1. Narzędzia do pomiaru wydajności

  • Lighthouse - kompleksowe narzędzie do analizy wydajności
  • Chrome DevTools Performance - szczegółowa analiza ładowania zasobów
  • WebPageTest - testowanie wydajności w różnych warunkach
  • Core Web Vitals w Google Search Console

2. A/B testy strategii ładowania

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        preload={testGroup === 'A'} // Grupa A używa preload, grupa B nie
47      />
48    </div>
49  );
50}

Dobre praktyki dotyczące strategii ładowania obrazów

  1. Używaj preload tylko dla obrazów above-the-fold - najlepiej dla jednego obrazu LCP na stronie

  2. Domyślnie używaj lazy loading - pozwala to oszczędzić zasoby i przyspieszyć początkowe ładowanie strony

  3. Monitoruj metryki Web Vitals - regularnie sprawdzaj, jak strategie ładowania wpływają na wydajność

  4. Dostosuj strategie do rodzaju strony:

    • Strony produktowe: preload dla głównego zdjęcia produktu
    • Strony kategorii: lazy dla większości obrazów, eager dla pierwszych kilku
    • Strona główna: preload dla hero image, lazy dla pozostałych
  5. Uwzględnij urządzenia użytkowników:

    • Użytkownicy mobilni często mają wolniejsze połączenia - jeszcze ważniejsze jest użycie lazy i mniejszych obrazów
    • Użytkownicy desktopowi mogą skorzystać z eager dla lepszego doświadczenia przewijania
  6. Używaj placeholderów dla lepszego doświadczenia podczas ładowania:

    • Dla obrazów z preload - pokazuj placeholder typu blur lub empty
    • Dla obrazów z lazy - używaj blur dla płynnego przejścia

Scenariusze dla różnych strategii

1. Strona główna e-commerce

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          preload
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}

2. Blog z długimi artykułami

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          preload
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}

Podsumowanie

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:

  1. 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

  2. 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

  3. 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:

  • Widoczność obrazu bez przewijania
  • Wpływ na kluczowe metryki wydajności
  • Typ zawartości i jej ważność
  • Urządzenia i łącza internetowe Twoich użytkowników

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 16.

Kod do tej lekcji: app/page.tsx
1// Image Loading Strategies - lazy, eager, priority
2'use client';
3
4import { useState } from 'react';
5
6export default function LoadingStrategiesDemo() {
7  const [activeStrategy, setActiveStrategy] = useState('lazy');
8
9  const strategies = [
10    {
11      id: 'lazy',
12      name: 'Lazy Loading',
13      icon: '',
14      color: 'blue',
15      description: 'Default strategy. Images load only when approaching the viewport.',
16      code: `<Image
17  src="/photo.jpg"
18  alt="Gallery image"
19  width={800}
20  height={600}
21  loading="lazy"  // default
22/>`,
23      useCase: 'Below-the-fold images, galleries, blog content images',
24      pros: ['Saves bandwidth', 'Faster initial page load', 'Default behavior'],
25      cons: ['Slight delay when scrolling to image', 'Not ideal for hero images'],
26    },
27    {
28      id: 'eager',
29      name: 'Eager Loading',
30      icon: '',
31      color: 'green',
32      description: 'Images load immediately, regardless of position on page.',
33      code: `<Image
34  src="/photo.jpg"
35  alt="Important image"
36  width={800}
37  height={600}
38  loading="eager"
39/>`,
40      useCase: 'Images likely to be seen quickly, carousels, critical UI elements',
41      pros: ['No loading delay', 'Good for near-viewport images'],
42      cons: ['Uses more bandwidth', 'Can slow initial load'],
43    },
44    {
45      id: 'priority',
46      name: 'Priority Loading',
47      icon: '',
48      color: 'purple',
49      description: 'Highest priority loading. Preloaded in <head> for fastest display.',
50      code: `<Image
51  src="/hero.jpg"
52  alt="Hero banner"
53  width={1920}
54  height={1080}
55  priority  // preloads in <head>
56/>`,
57      useCase: 'Hero images, LCP elements, above-the-fold content',
58      pros: ['Fastest loading', 'Improves LCP score', 'Preloaded in head'],
59      cons: ['Use sparingly (1-2 per page)', 'Increases initial payload'],
60    },
61  ];
62
63  const active = strategies.find(s => s.id === activeStrategy);
64
65  return (
66    <div className="min-h-screen bg-gray-900 text-white p-8">
67      <div className="max-w-4xl mx-auto">
68        <h1 className="text-3xl font-bold mb-2">Image Loading Strategies</h1>
69        <p className="text-gray-400 mb-8">
70          Choose the right strategy for each image in your Next.js app
71        </p>
72
73        {/* Strategy Tabs */}
74        <div className="flex gap-4 mb-8">
75          {strategies.map(s => (
76            <button
77              key={s.id}
78              onClick={() => setActiveStrategy(s.id)}
79              className={`px-6 py-3 rounded-lg font-medium transition-colors ${
80                activeStrategy === s.id
81                  ? 'bg-blue-600 text-white'
82                  : 'bg-gray-800 text-gray-400 hover:bg-gray-700'
83              }`}
84            >
85              {s.icon} {s.name}
86            </button>
87          ))}
88        </div>
89
90        {/* Active Strategy Details */}
91        {active && (
92          <div className="bg-gray-800 rounded-xl p-8">
93            <h2 className="text-2xl font-bold mb-2">
94              {active.icon} {active.name}
95            </h2>
96            <p className="text-gray-400 mb-6">{active.description}</p>
97
98            {/* Code Example */}
99            <div className="bg-gray-900 rounded-lg p-4 mb-6">
100              <pre className="text-sm text-green-400 overflow-x-auto">
101                {active.code}
102              </pre>
103            </div>
104
105            {/* Use Cases */}
106            <div className="mb-6">
107              <h3 className="font-semibold mb-2">When to use:</h3>
108              <p className="text-gray-400">{active.useCase}</p>
109            </div>
110
111            {/* Pros & Cons */}
112            <div className="grid grid-cols-2 gap-4">
113              <div>
114                <h3 className="font-semibold text-green-400 mb-2">Pros</h3>
115                <ul className="space-y-1">
116                  {active.pros.map((p, i) => (
117                    <li key={i} className="text-sm text-gray-400">• {p}</li>
118                  ))}
119                </ul>
120              </div>
121              <div>
122                <h3 className="font-semibold text-red-400 mb-2">Cons</h3>
123                <ul className="space-y-1">
124                  {active.cons.map((c, i) => (
125                    <li key={i} className="text-sm text-gray-400">• {c}</li>
126                  ))}
127                </ul>
128              </div>
129            </div>
130          </div>
131        )}
132
133        {/* Priority Guide */}
134        <div className="mt-8 bg-gray-800 rounded-xl p-6">
135          <h2 className="text-xl font-bold mb-4">Strategy Map</h2>
136          <div className="space-y-3">
137            <div className="flex items-center gap-4 p-3 bg-purple-900/30 rounded-lg">
138              <span className="text-2xl"></span>
139              <div>
140                <span className="font-semibold text-purple-400">Priority</span>
141                <span className="text-gray-400 ml-2">→ Hero image, main banner (1-2 per page)</span>
142              </div>
143            </div>
144            <div className="flex items-center gap-4 p-3 bg-green-900/30 rounded-lg">
145              <span className="text-2xl"></span>
146              <div>
147                <span className="font-semibold text-green-400">Eager</span>
148                <span className="text-gray-400 ml-2">→ Near-viewport images, carousel slides</span>
149              </div>
150            </div>
151            <div className="flex items-center gap-4 p-3 bg-blue-900/30 rounded-lg">
152              <span className="text-2xl"></span>
153              <div>
154                <span className="font-semibold text-blue-400">Lazy</span>
155                <span className="text-gray-400 ml-2">→ Everything else (default)</span>
156              </div>
157            </div>
158          </div>
159        </div>
160      </div>
161    </div>
162  );
163}

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Styled-components to biblioteka dla:

  2. 2. Mobile-first design oznacza:

Zadania praktyczne w grze

  • Edytor kodu

    Utwórz plik z CSS variables definiującymi kompletny system designu (kolory, spacing, typography, shadows)

  • Układanie w pionie

    Ułóż kroki użycia CSS Modules w Next.js w prawidłowej kolejności:

  • Klikanie w kolejności

    Ułóż składnię className z klasami Tailwind CSS w JSX

  • Edytor kodu

    Utwórz keyframes animation dla fade-in efektu i hover transition dla przycisku

Przydatne artykuły