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

Zarządzanie zasobami statycznymi w katalogu public w Next.js 16

11 min czytania ·
W tej lekcji12

W Metropolis Quantum, każda dzielnica ma swoją centralną bazę zasobów - miejsce, gdzie przechowywane są elementy dostępne dla wszystkich mieszkańców: mapy, broszury informacyjne, elementy identyfikacji wizualnej czy interaktywne holoekrany z planami miasta. Te zasoby są niezmienne i dostępne publicznie, zapewniając spójność i ciągłość doświadczenia w całym mieście.

W aplikacjach Next.js, rolę tej centralnej bazy pełni katalog public. To właśnie tutaj umieszczamy wszystkie zasoby statyczne, które powinny być dostępne bez przetwarzania przez system budowania Next.js. W tym rozdziale przyjrzymy się, jak efektywnie zarządzać tymi zasobami, by nasza aplikacja była zarówno wydajna, jak i dobrze zorganizowana.

Czym jest katalog public w Next.js 16?

Katalog public to specjalny folder w aplikacji Next.js, który spełnia następujące funkcje:

  1. Udostępnia pliki statyczne bezpośrednio - pliki umieszczone w public są serwowane z głównego katalogu domeny
  2. Nie przetwarza zawartości - pliki nie przechodzą przez system budowania (w przeciwieństwie do zasobów importowanych bezpośrednio w komponentach)
  3. Zapewnia niezmienne URL-e - nazwy plików nie ulegają zmianie przez proces budowania (nie są dodawane hashe do nazw)

Katalog ten jest idealny do przechowywania takich zasobów jak:

  • Obrazy logo i ikony
  • Pliki favicon i manifest.json
  • Pliki robots.txt i sitemap.xml
  • Czcionki
  • Dokumenty (PDF, DOCX)
  • Zasoby multimedialne (filmy, dźwięki)
  • Dane statyczne (JSON, CSV)

Struktura katalogu public

Dobrze zorganizowany katalog public ułatwia zarządzanie zasobami. Oto przykładowa struktura:

public/
├── favicon.ico                  # Ikona strony
├── favicon-16x16.png            # Alternatywne rozmiary favicon
├── favicon-32x32.png
├── apple-touch-icon.png         # Ikona dla urządzeń Apple
├── manifest.json                # Manifest PWA
├── robots.txt                   # Instrukcje dla botów
├── sitemap.xml                  # Mapa strony dla wyszukiwarek
├── fonts/                       # Katalog z czcionkami
│   ├── QuantumSans-Regular.woff2
│   └── QuantumSans-Bold.woff2
├── images/                      # Katalog z obrazami
│   ├── logo.png
│   ├── hero-banner.jpg
│   ├── icons/                   # Podkatalog z ikonami
│   │   ├── arrow-right.svg
│   │   └── search.svg
│   └── backgrounds/             # Podkatalog z tłami
│       ├── quantum-pattern.svg
│       └── districts/           # Dalsze podkatalogi dla organizacji
│           ├── downtown.jpg
│           └── quantum-district.jpg
├── videos/                      # Katalog z filmami
│   └── intro.mp4
└── data/                        # Katalog z danymi statycznymi
    ├── districts.json
    └── services.json

Dostęp do zasobów z katalogu public

Dostęp do zasobów z katalogu public jest prosty i intuicyjny. Pamiętaj, że:

  1. Ścieżki są względem głównego katalogu domeny
  2. Musisz poprzedzać nazwy plików ukośnikiem (/)

W komponentach React

// Przykład użycia obrazu z katalogu public
export default function Logo() {
  return (
    <img
      src="/images/logo.png"
      alt="Metropolis Quantum Logo"
      width={200}
      height={50}
    />
  );
}

W CSS

/* W pliku CSS */
.hero-section {
  background-image: url('/images/backgrounds/quantum-pattern.svg');
  background-repeat: repeat;
}

W metadanych

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Metropolis Quantum',
  description: 'Oficjalny portal miasta przyszłości',
  icons: {
    icon: [
      { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
      { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
    ],
    apple: [
      { url: '/apple-touch-icon.png' },
    ],
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="pl">
      <body>{children}</body>
    </html>
  );
}

Komponent Image a katalog public

Mimo że Next.js dostarcza zoptymalizowany komponent Image z pakietu next/image, czasem potrzebujemy użyć obrazów bezpośrednio z katalogu public. Komponent Image można używać z obrazami z katalogu public, zachowując korzyści z optymalizacji:

import Image from 'next/image';

export default function HeroBanner() {
  return (
    <div className="hero-banner">
      <Image
        src="/images/hero-banner.jpg"
        alt="Witaj w Metropolis Quantum"
        width={1200}
        height={600}
        preload
      />
    </div>
  );
}

Kiedy używać obrazów z public zamiast importów?

  1. Gdy URL obrazu musi być znany podczas budowy:

    • Obrazy w metadanych (Open Graph, Twitter)
    • Tła CSS
  2. Gdy obraz jest bardzo duży i rzadko używany:

    • Duże grafiki, które pojawiają się tylko w specyficznych kontekstach
  3. Gdy chcesz mieć bezpośredni link do pliku:

    • Zasoby do pobrania
    • Zasoby współdzielone z innymi aplikacjami

Obsługa różnych typów plików

SVG

Pliki SVG można umieszczać w katalogu public i używać ich jako zwykłych obrazów:

export default function Icon() {
  return (
    <img src="/images/icons/search.svg" alt="Szukaj" width={24} height={24} />
  );
}

Alternatywnie, SVG można importować bezpośrednio w kodzie (nie przez katalog public), co pozwala na manipulację ich właściwościami:

import SearchIcon from '@/public/images/icons/search.svg';

export default function SearchButton() {
  return (
    <button className="search-button">
      <SearchIcon className="icon" width={24} height={24} />
      Szukaj
    </button>
  );
}

Uwaga: Aby powyższy przykład działał, potrzebujesz dodatkowej konfiguracji, np. za pomocą @svgr/webpack.

Czcionki

Jak widzieliśmy w poprzednim rozdziale, pliki czcionek można przechowywać w katalogu public i używać za pomocą next/font/local:

import localFont from 'next/font/local';

export const quantumSans = localFont({
  src: [
    {
      path: '../public/fonts/QuantumSans-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: '../public/fonts/QuantumSans-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
});

Filmy i audio

Pliki multimedialne również mogą być przechowywane w katalogu public:

export default function IntroVideo() {
  return (
    <video
      controls
      width="100%"
      poster="/images/video-poster.jpg"
    >
      <source src="/videos/intro.mp4" type="video/mp4" />
      Twoja przeglądarka nie obsługuje odtwarzacza wideo.
    </video>
  );
}

Pliki JSON

JSON przechowywane w public można ładować za pomocą fetch:

// app/districts/page.tsx
export default async function DistrictsPage() {
  // Fetch danych z pliku JSON
  const response = await fetch('http://localhost:3000/data/districts.json');
  const districts = await response.json();

  return (
    <div>
      <h1>Dzielnice Metropolis Quantum</h1>
      <ul>
        {districts.map((district) => (
          <li key={district.id}>{district.name}</li>
        ))}
      </ul>
    </div>
  );
}

Wskazówka: W rzeczywistych aplikacjach lepszym rozwiązaniem jest często importowanie danych JSON bezpośrednio w kodzie (np. import districts from '@/data/districts.json') lub przechowywanie ich w bazie danych.

Organizacja i najlepsze praktyki

1. Struktura katalogów

Utrzymuj logiczną strukturę katalogów, która odzwierciedla przeznaczenie zasobów:

public/
├── branding/       # Elementy związane z marką
├── icons/          # Ikony systemowe
├── images/         # Zdjęcia i grafiki
├── videos/         # Pliki wideo
├── fonts/          # Czcionki
└── data/           # Dane statyczne (JSON, CSV)

2. Konwencje nazewnictwa

Stosuj spójne konwencje nazewnictwa:

  • Używaj kebab-case (hero-banner.jpg) zamiast spacji (hero banner.jpg)
  • Dodawaj sufiksy z rozmiarami dla różnych wariantów (logo-sm.png, logo-lg.png)
  • Używaj prefiksów dla grupowania (icon-home.svg, icon-user.svg)

3. Optymalizacja zasobów

Przed umieszczeniem zasobów w katalogu public, optymalizuj je:

  • Obrazy: Kompresuj za pomocą narzędzi jak TinyPNG, ImageOptim
  • SVG: Minimalizuj za pomocą SVGO
  • Wideo: Kompresuj do właściwych formatów i rozmiarów
  • Czcionki: Używaj WOFF2 jako preferowanego formatu

4. Cachowanie

Next.js 16 dodaje do plików z katalogu public nagłówek Cache-Control: public, max-age=0, bo takie pliki mogą się zmienić bez zmiany nazwy. Długie, niezmienne cachowanie dostają tylko pliki z /_next/static, które mają w nazwie skrót zawartości. Jeśli wiesz, że Twoje zasoby się nie zmieniają, ustaw własne nagłówki w pliku next.config.js:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/fonts/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/images/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=86400, stale-while-revalidate=31536000',
          },
        ],
      },
    ];
  },
};

Problemy i rozwiązania

Problem 1: Zbyt duży rozmiar katalogu public

Jeśli katalog public staje się zbyt duży, może to wpłynąć na czas deploymentu i zarządzanie zasobami.

Rozwiązanie:

  • Przenieś rzadko używane, duże zasoby do usług CDN (Content Delivery Network)
  • Stosuj strategie lazy loading dla zasobów
  • Optymalizuj wszystkie zasoby przed dodaniem ich do katalogu

Problem 2: Problemy z URL obrazów

Rozwiązanie:

  • Zawsze używaj ukośników na początku ścieżek (/images/logo.png, nie images/logo.png)
  • Unikaj spacji i znaków specjalnych w nazwach plików
  • Sprawdzaj wielkość liter w nazwach plików (systemy Linux są case-sensitive)

Problem 3: Niespójność w zarządzaniu zasobami

Rozwiązanie:

  • Stwórz i stosuj konwencje nazewnictwa dla całego zespołu
  • Dokumentuj strukturę katalogu public
  • Rozważ utworzenie komponentów abstrakcyjnych dla często używanych zasobów

Praktyczne przykłady

Przykład 1: System ikon z katalogu public

// components/Icon.tsx
type IconName = 'home' | 'search' | 'user' | 'settings' | 'menu';

interface IconProps {
  name: IconName;
  size?: 'sm' | 'md' | 'lg';
  color?: string;
  className?: string;
}

const sizeMap = {
  sm: { width: 16, height: 16 },
  md: { width: 24, height: 24 },
  lg: { width: 32, height: 32 },
};

export function Icon({
  name,
  size = 'md',
  color = 'currentColor',
  className = '',
}: IconProps) {
  const dimensions = sizeMap[size];

  return (
    <img
      src={`/icons/${name}.svg`}
      alt={`${name} icon`}
      className={`icon icon-${name} ${className}`}
      {...dimensions}
      style={{ filter: `${color !== 'currentColor' ? `color(${color})` : ''}` }}
    />
  );
}

Użycie:

import { Icon } from '@/components/Icon';

export default function Navbar() {
  return (
    <nav className="navbar">
      <Icon name="home" size="sm" />
      <Icon name="search" color="#3a86ff" />
      <Icon name="user" className="user-icon" />
    </nav>
  );
}

Przykład 2: System dynamicznego ładowania zdjęć z katalogu public

// components/DistrictImage.tsx
import Image from 'next/image';

interface DistrictImageProps {
  districtId: string;
  alt: string;
  className?: string;
}

export function DistrictImage({ districtId, alt, className = '' }: DistrictImageProps) {
  return (
    <div className={`district-image-container ${className}`}>
      <Image
        src={`/images/backgrounds/districts/${districtId}.jpg`}
        alt={alt}
        fill
        className="district-image"
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      />
    </div>
  );
}

Użycie:

import { DistrictImage } from '@/components/DistrictImage';

export default function DistrictCard({ district }) {
  return (
    <div className="district-card">
      <DistrictImage
        districtId={district.id}
        alt={`Dzielnica ${district.name}`}
      />
      <h3>{district.name}</h3>
      <p>{district.description}</p>
    </div>
  );
}

Przykład 3: Konfiguracja SEO z zasobami z public

// app/districts/[id]/page.tsx
import { Metadata } from 'next';

interface Props {
  params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const districtId = (await params).id;
  const district = await fetchDistrictData(districtId);

  return {
    title: `${district.name} | Metropolis Quantum`,
    description: district.description,
    openGraph: {
      images: [{
        url: `/images/backgrounds/districts/${districtId}.jpg`,
        width: 1200,
        height: 630,
        alt: district.name,
      }],
      type: 'website',
    },
    twitter: {
      card: 'summary_large_image',
      images: [`/images/backgrounds/districts/${districtId}.jpg`],
    },
  };
}

export default async function DistrictPage({ params }: Props) {
  const district = await fetchDistrictData((await params).id);

  return (
    <div className="district-page">
      <h1>{district.name}</h1>
      {/* Reszta zawartości strony */}
    </div>
  );
}

// Funkcja pobierająca dane dzielnicy
async function fetchDistrictData(id: string) {
  // W rzeczywistej aplikacji: pobieranie danych z API lub bazy
  const response = await fetch(`http://localhost:3000/data/districts/${id}.json`);
  return response.json();
}

Integracja z CDN

Dla większych aplikacji warto rozważyć przeniesienie zasobów z katalogu public do zewnętrznego CDN (Content Delivery Network), co zapewni szybsze ładowanie na całym świecie.

Konfiguracja ścieżki bazowej w Next.js

// next.config.js
module.exports = {
  assetPrefix: process.env.NODE_ENV === 'production' ? 'https://cdn.twojadomena.com' : '',
};

Komponent do zarządzania ścieżkami zasobów

// utils/assetPath.ts
export function getAssetPath(path: string): string {
  const cdnUrl = process.env.NEXT_PUBLIC_CDN_URL || '';
  return `${cdnUrl}${path}`;
}

Użycie:

import { getAssetPath } from '@/utils/assetPath';

export default function Logo() {
  return (
    <img
      src={getAssetPath('/images/logo.png')}
      alt="Metropolis Quantum Logo"
      width={200}
      height={50}
    />
  );
}

Zaawansowane techniki

1. Dynamiczne importy zasobów

Możesz dynamicznie importować zasoby z katalogu public podczas budowy:

// utils/getDistrictData.ts
import fs from 'fs';
import path from 'path';

export function getDistrictData() {
  const filePath = path.join(process.cwd(), 'public', 'data', 'districts.json');
  const fileContents = fs.readFileSync(filePath, 'utf8');
  return JSON.parse(fileContents);
}

2. Generowanie zasobów podczas budowy

Możesz generować zasoby statyczne podczas budowy aplikacji:

// scripts/generate-sitemap.js
const fs = require('fs');
const path = require('path');

// Funkcja generująca sitemap.xml
function generateSitemap(routes) {
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    ${routes.map(route => `
      <url>
        <loc>https://metropolisquantum.pl${route}</loc>
        <lastmod>${new Date().toISOString()}</lastmod>
      </url>
    `).join('')}
  </urlset>
  `;

  fs.writeFileSync(
    path.join(process.cwd(), 'public', 'sitemap.xml'),
    sitemap
  );
}

// Lista tras aplikacji
const routes = [
  '/',
  '/districts',
  '/about',
  '/contact',
  // inne trasy
];

generateSitemap(routes);

Następnie możesz dodać ten skrypt do procesu budowania w package.json:

{
  "scripts": {
    "build": "node scripts/generate-sitemap.js && next build"
  }
}

Bezpieczeństwo i uwagi dotyczące zasobów publicznych

1. Unikaj przechowywania poufnych informacji

Katalog public jest dostępny publicznie, więc nigdy nie należy przechowywać w nim:

  • Kluczy API
  • Poufnych danych
  • Plików konfiguracyjnych z wrażliwymi informacjami

2. Ochrona przed hotlinkiem

Możesz chronić swoje zasoby przed hotlinkingiem (używaniem ich na innych stronach) za pomocą nagłówków HTTP:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/images/:path*',
        headers: [
          {
            key: 'Referrer-Policy',
            value: 'same-origin',
          },
        ],
      },
    ];
  },
};

3. Content Security Policy (CSP)

Warto skonfigurować CSP, aby chronić swoją aplikację:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; img-src 'self' https://cdn.twojadomena.com; font-src 'self' https://fonts.gstatic.com",
          },
        ],
      },
    ];
  },
};

Podsumowanie

Katalog public w Next.js 16 jest potężnym narzędziem do zarządzania zasobami statycznymi w aplikacji. Odpowiednia organizacja, konwencje nazewnictwa i optymalizacja zasobów mogą znacząco poprawić wydajność i utrzymywalność projektu.

Kluczowe punkty:

  1. Struktura i organizacja - utrzymuj logiczną strukturę katalogów i spójne konwencje nazewnictwa
  2. Optymalizacja - kompresuj i optymalizuj zasoby przed umieszczeniem ich w katalogu
  3. Abstrakcja - twórz komponenty abstrakcyjne do zarządzania dostępem do zasobów
  4. Bezpieczeństwo - pamiętaj, że zawartość katalogu public jest dostępna publicznie
  5. Skalowanie - dla większych aplikacji rozważ użycie CDN

Podobnie jak w centralnej bazie zasobów Metropolis Quantum, dobrze zarządzany katalog public zapewnia sprawny dostęp do wszystkich niezbędnych elementów, utrzymując porządek i wydajność całego systemu.

W następnym rozdziale przyjrzymy się ikonom, favicon i metadanym wizualnym, które są kluczowymi elementami tożsamości wizualnej Twojej aplikacji Next.js 16.

Kod do tej lekcji: app/page.tsx
// CSS Performance Optimization - Cyberpunk Edition
'use client';

export default function PerformanceDemo() {
  return (
    <div style={{
      minHeight: '100vh',
      background: 'linear-gradient(135deg, #0f0f23, #1a1a2e)',
      color: '#ffffff',
      padding: '3rem 2rem'
    }}>
      <h1 style={{
        fontSize: '3rem',
        textAlign: 'center',
        background: 'linear-gradient(45deg, #64ffda, #7c4dff)',
        WebkitBackgroundClip: 'text',
        WebkitTextFillColor: 'transparent',
        marginBottom: '3rem'
      }}>
        CSS Performance Optimization
      </h1>

      {/* Will-change dla animowanych elementów */}
      <section style={{
        maxWidth: '1200px',
        margin: '0 auto 3rem',
        background: 'rgba(255,255,255,0.05)',
        padding: '2rem',
        borderRadius: '1rem',
        border: '1px solid rgba(100,255,218,0.2)'
      }}>
        <h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>1. will-change Property</h2>
        <div style={{
          width: '200px',
          height: '200px',
          background: 'linear-gradient(135deg, #64ffda, #7c4dff)',
          borderRadius: '1rem',
          willChange: 'transform',
          animation: 'float 3s ease-in-out infinite'
        }}></div>
        <p style={{color: '#b0bec5', marginTop: '1rem'}}>
          Ten element używa <code style={{background: 'rgba(0,0,0,0.5)', padding: '0.25rem 0.5rem', borderRadius: '0.25rem', color: '#64ffda'}}>will-change: transform</code> dla lepszej wydajności animacji
        </p>
      </section>

      {/* Transform zamiast left/top */}
      <section style={{
        maxWidth: '1200px',
        margin: '0 auto 3rem',
        background: 'rgba(255,255,255,0.05)',
        padding: '2rem',
        borderRadius: '1rem',
        border: '1px solid rgba(100,255,218,0.2)'
      }}>
        <h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>2. Transform vs Position</h2>
        <div style={{display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '2rem'}}>
          <div>
            <h3 style={{color: '#7c4dff', marginBottom: '1rem'}}>Wolne (position)</h3>
            <div style={{
              position: 'relative',
              width: '150px',
              height: '150px',
              background: '#ff0080',
              borderRadius: '0.75rem'
            }}>
              <div style={{fontSize: '0.875rem', padding: '1rem', color: '#fff'}}>
                Używa position: relative; top/left
              </div>
            </div>
          </div>

          <div>
            <h3 style={{color: '#7c4dff', marginBottom: '1rem'}}>Szybkie (transform)</h3>
            <div style={{
              width: '150px',
              height: '150px',
              background: '#00ff7f',
              borderRadius: '0.75rem',
              transform: 'translateZ(0)'
            }}>
              <div style={{fontSize: '0.875rem', padding: '1rem', color: '#0f0f23', fontWeight: 600}}>
                Używa transform: translate()
              </div>
            </div>
          </div>
        </div>
      </section>

      {/* Contain property */}
      <section style={{
        maxWidth: '1200px',
        margin: '0 auto 3rem',
        background: 'rgba(255,255,255,0.05)',
        padding: '2rem',
        borderRadius: '1rem',
        border: '1px solid rgba(100,255,218,0.2)'
      }}>
        <h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>3. CSS Containment</h2>
        <div style={{
          contain: 'layout style paint',
          background: 'rgba(124,77,255,0.2)',
          padding: '1.5rem',
          borderRadius: '0.75rem',
          border: '1px solid rgba(124,77,255,0.5)'
        }}>
          <p style={{color: '#b0bec5', lineHeight: 1.6}}>
            Ten kontener używa <code style={{background: 'rgba(0,0,0,0.5)', padding: '0.25rem 0.5rem', borderRadius: '0.25rem', color: '#7c4dff'}}>contain: layout style paint</code>
            <br/><br/>
            Izoluje zmiany layoutu tylko do tego kontenera, poprawiając wydajność renderowania.
          </p>
        </div>
      </section>

      {/* Content-visibility */}
      <section style={{
        maxWidth: '1200px',
        margin: '0 auto 3rem',
        background: 'rgba(255,255,255,0.05)',
        padding: '2rem',
        borderRadius: '1rem',
        border: '1px solid rgba(100,255,218,0.2)'
      }}>
        <h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>4. content-visibility</h2>
        {[1, 2, 3, 4, 5].map(i => (
          <div key={i} style={{
            contentVisibility: 'auto',
            containIntrinsicSize: '0 200px',
            marginBottom: '1.5rem',
            background: 'rgba(100,255,218,0.1)',
            padding: '1.5rem',
            borderRadius: '0.75rem',
            border: '1px solid rgba(100,255,218,0.3)'
          }}>
            <h3 style={{color: '#64ffda', marginBottom: '0.75rem'}}>Section {i}</h3>
            <p style={{color: '#b0bec5', lineHeight: 1.6}}>
             Ta sekcja używa content-visibility: auto. Przeglądarka renderuje ją tylko wtedy, gdy jest w viewporcie, 
              co znacząco poprawia wydajność przy dużych listach treści.
            </p>
          </div>
        ))}
      </section>

      {/* Critical CSS */}
      <section style={{
        maxWidth: '1200px',
        margin: '0 auto 3rem',
        background: 'rgba(255,255,255,0.05)',
        padding: '2rem',
        borderRadius: '1rem',
        border: '1px solid rgba(100,255,218,0.2)'
      }}>
        <h2 style={{color: '#64ffda', marginBottom: '1.5rem'}}>5. Critical CSS Best Practices</h2>
        <ul style={{color: '#b0bec5', lineHeight: 2, paddingLeft: '1.5rem'}}>
          <li>Inline krytyczne style w &lt;head&gt;</li>
          <li>Lazy-load non-critical CSS</li>
          <li>Użyj CSS-in-JS dla komponentów</li>
          <li>Minifikuj i kompresuj CSS</li>
          <li>Usuń nieużywane style (PurgeCSS)</li>
          <li>Użyj CDN dla szybszego dostarczania</li>
        </ul>
      </section>

      <style jsx global>{`
        @keyframes float {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-20px); }
        }
      `}</style>
    </div>
  );
}

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

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

  1. 1. CSS Custom Properties (variables) definiuje się używając:

  2. 2. SASS/SCSS to:

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Utwórz CSS z uwzględnieniem accessibility: focus styles, prefers-reduced-motion, high contrast

  • Klikanie w kolejności

    Ułóż deklarację CSS Grid template:

  • Układanie w pionie

    Ułóż etapy optymalizacji obrazów przez komponent next/image w prawidłowej kolejności:

Przydatne artykuły