Kurs JavaScript i TypeScript · Moduł 9: Wzorce projektowe i architektura
Code splitting i tree shaking
W tej lekcji11
W świecie nowoczesnego web developmentu, gdzie aplikacje stają się coraz bardziej złożone, zarządzanie wielkością bundli JavaScript jest kluczowe dla wydajności. Dwie fundamentalne techniki optymalizacji - code splitting i tree shaking - pozwalają nam drastycznie zmniejszyć rozmiar kodu trafiającego do użytkowników.
Czym jest code splitting?
Code splitting to technika dzielenia kodu na mniejsze fragmenty (chunki), które mogą być ładowane na żądanie (lazy loading) lub równolegle. Zamiast wysyłać cały kod aplikacji w jednym wielkim bundlu, możemy podzielić go na logiczne części i ładować tylko to, czego aktualnie potrzebujemy.
Problemy rozwiązywane przez code splitting
1// Problem: Jeden wielki bundle
2// main.bundle.js (2.5MB)
3// - Kod strony głównej (200KB)
4// - Kod panelu administracyjnego (800KB)
5// - Kod sklepu internetowego (1.5MB)
6// Użytkownik widząc tylko stronę główną pobiera niepotrzebnie 2.3MB kodu!
7
8// Rozwiązanie: Code splitting
9// main.bundle.js (200KB) - tylko kod strony głównej
10// admin.chunk.js (800KB) - ładowany tylko gdy potrzebny
11// shop.chunk.js (1.5MB) - ładowany tylko gdy potrzebnyRodzaje code splitting
1. Code splitting według tras
Najprostszy i najczęściej używany rodzaj - dzielenie kodu według tras aplikacji:
1// React Router z lazy loading
2import { lazy, Suspense } from 'react';
3import { Routes, Route } from 'react-router';
4
5// Każdy komponent to osobny chunk
6const HomePage = lazy(() => import('./pages/HomePage'));
7const ProductPage = lazy(() => import('./pages/ProductPage'));
8const AdminPanel = lazy(() => import('./pages/AdminPanel'));
9
10function App() {
11 return (
12 <Suspense fallback={<div>Ładowanie...</div>}>
13 <Routes>
14 <Route path="/" element={<HomePage />} />
15 <Route path="/product/:id" element={<ProductPage />} />
16 <Route path="/admin" element={<AdminPanel />} />
17 </Routes>
18 </Suspense>
19 );
20}2. Code splitting na poziomie komponentów
Dzielenie kodu na poziomie komponentów:
1// Lazy loading pojedynczych komponentów
2const ExpensiveChart = lazy(() => import('./components/ExpensiveChart'));
3const HeavyDataTable = lazy(() => import('./components/HeavyDataTable'));
4
5function Dashboard({ showChart, showTable }) {
6 return (
7 <div>
8 <h1>Panel główny</h1>
9
10 {showChart && (
11 <Suspense fallback={<div>Ładowanie wykresu...</div>}>
12 <ExpensiveChart />
13 </Suspense>
14 )}
15
16 {showTable && (
17 <Suspense fallback={<div>Ładowanie tabeli...</div>}>
18 <HeavyDataTable />
19 </Suspense>
20 )}
21 </div>
22 );
23}3. Dynamiczne importy
Importowanie kodu w odpowiedzi na akcje użytkownika:
1// Ładowanie biblioteki tylko gdy potrzebna
2async function showAdvancedEditor() {
3 // CodeMirror jest ładowany tylko gdy użytkownik kliknie przycisk
4 const { EditorView, basicSetup } = await import('codemirror');
5 const { javascript } = await import('@codemirror/lang-javascript');
6
7 const editor = new EditorView({
8 extensions: [basicSetup, javascript()],
9 parent: document.getElementById('editor-container')
10 });
11}
12
13// Event listener na przycisku
14document.getElementById('show-editor').addEventListener('click', showAdvancedEditor);4. Wydzielanie bibliotek zewnętrznych (vendor)
Separowanie bibliotek zewnętrznych od kodu aplikacji:
1// webpack.config.js
2module.exports = {
3 optimization: {
4 splitChunks: {
5 chunks: 'all',
6 cacheGroups: {
7 // Biblioteki zewnętrzne w osobnym chunk
8 vendor: {
9 test: /[\\/]node_modules[\\/]/,
10 name: 'vendors',
11 chunks: 'all',
12 },
13 // Wspólny kod aplikacji
14 common: {
15 name: 'common',
16 minChunks: 2,
17 chunks: 'all',
18 enforce: true
19 }
20 }
21 }
22 }
23};Czym jest tree shaking?
Tree shaking to proces usuwania "martwego kodu" (dead code) - czyli kodu, który nie jest używany w aplikacji. Nazwa pochodzi od metafory "potrząsania drzewem", gdzie opadają tylko niepotrzebne liście (nieużywany kod).
Jak działa tree shaking?
1// utils.js - eksportujemy wiele funkcji
2export function formatDate(date) {
3 return date.toLocaleDateString();
4}
5
6export function formatTime(date) {
7 return date.toLocaleTimeString();
8}
9
10export function formatCurrency(amount) {
11 return new Intl.NumberFormat('pl-PL', {
12 style: 'currency',
13 currency: 'PLN'
14 }).format(amount);
15}
16
17export function validateEmail(email) {
18 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
19}
20
21// main.js - importujemy tylko to czego potrzebujemy
22import { formatDate, formatCurrency } from './utils.js';
23
24// Tree shaking usunie formatTime i validateEmail z finalnego bundle
25console.log(formatDate(new Date()));
26console.log(formatCurrency(100));Warunki konieczne dla tree shaking
- ES Modules (ESM) - tree shaking działa tylko z ES6 modules:
1// Działa z tree shaking (ES modules)
2import { specificFunction } from './module.js';
3
4// Nie działa z tree shaking (CommonJS)
5const { specificFunction } = require('./module.js');- Static imports - importy muszą być statyczne:
1// Static import - tree shaking działa
2import { debounce } from 'lodash-es';
3
4// Dynamic import - tree shaking nie działa
5const moduleName = 'lodash-es';
6import(moduleName).then(module => {
7 const { debounce } = module;
8});- Named exports - preferujemy named exports nad default exports:
1// Named exports - lepsze dla tree shaking
2export const add = (a, b) => a + b;
3export const subtract = (a, b) => a - b;
4
5// Default export z obiektem - gorsze dla tree shaking
6export default {
7 add: (a, b) => a + b,
8 subtract: (a, b) => a - b
9};Praktyczne przykłady optymalizacji
Optymalizacja bibliotek CSS
1// Importowanie całej biblioteki Bootstrap
2import 'bootstrap/dist/css/bootstrap.css';
3
4// Importowanie tylko potrzebnych modułów (własny plik SCSS)
5import './bootstrap-custom.scss';1// bootstrap-custom.scss (Bootstrap 5): pliki częściowe Sass dołączasz z SCSS, nie z JavaScriptu
2@import 'bootstrap/scss/functions';
3@import 'bootstrap/scss/variables';
4@import 'bootstrap/scss/variables-dark';
5@import 'bootstrap/scss/maps';
6@import 'bootstrap/scss/mixins';
7@import 'bootstrap/scss/utilities';
8
9// Tylko klasy display (d-*), margin (m-*) i padding (p-*)
10$utilities: map-get-multiple($utilities, ('display', 'margin', 'padding'));
11
12@import 'bootstrap/scss/utilities/api';Optymalizacja bibliotek JavaScript
1// Importowanie całej biblioteki Lodash (>70KB)
2import _ from 'lodash';
3const result = _.debounce(handleSearch, 300);
4
5// Importowanie tylko potrzebnej funkcji z lodash-es
6import { debounce } from 'lodash-es';
7const result = debounce(handleSearch, 300);
8
9// Lub jeszcze lepiej - pojedyncza funkcja
10import debounce from 'lodash-es/debounce';
11const result = debounce(handleSearch, 300);Tree shaking w React
1// Import przestrzeni nazw - przy statycznym dostępie (MUI.Button) bundler i tak usunie nieużywane komponenty
2import * as MUI from '@mui/material';
3
4// Importowanie tylko potrzebnych komponentów
5import { Button, TextField, Card } from '@mui/material';
6
7// Lub indywidualne importy
8import Button from '@mui/material/Button';
9import TextField from '@mui/material/TextField';
10import Card from '@mui/material/Card';Konfiguracja bundlerów
Webpack
1// webpack.config.js
2module.exports = {
3 mode: 'production', // Włącza tree shaking automatycznie
4 optimization: {
5 usedExports: true, // Oznacza używane eksporty
6 sideEffects: true, // Respektuje pole "sideEffects" z package.json (domyślne w trybie production)
7 splitChunks: {
8 chunks: 'all',
9 cacheGroups: {
10 vendor: {
11 test: /[\\/]node_modules[\\/]/,
12 name: 'vendors',
13 chunks: 'all',
14 }
15 }
16 }
17 }
18};Vite
1// vite.config.js - Vite 8+ (Rolldown); w Vite 7 i starszych: build.rollupOptions.output.manualChunks
2import { defineConfig } from 'vite';
3
4export default defineConfig({
5 build: {
6 rolldownOptions: {
7 output: {
8 codeSplitting: {
9 groups: [
10 { name: 'vendor', test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/ },
11 { name: 'utils', test: /[\\/]node_modules[\\/](lodash-es|date-fns)[\\/]/ }
12 ]
13 }
14 }
15 }
16 }
17});Rollup
1// rollup.config.js
2export default {
3 input: 'src/main.js',
4 output: {
5 dir: 'dist',
6 format: 'es',
7 manualChunks: (id) => {
8 if (id.includes('node_modules')) {
9 return 'vendor';
10 }
11 }
12 },
13 external: ['react', 'react-dom']
14};Zaawansowane techniki
Preloading i prefetching
1// Preload - wysoki priorytet, potrzebne od razu
2const AdminPanel = lazy(() =>
3 import(/* webpackPreload: true */ './AdminPanel')
4);
5
6// Prefetch - niski priorytet, prawdopodobnie potrzebne później
7const UserProfile = lazy(() =>
8 import(/* webpackPrefetch: true */ './UserProfile')
9);Analiza bundla
Plugin webpack-bundle-analyzer zapisuje przy każdym buildzie raport HTML z mapą bundla: każdy moduł jest prostokątem o polu proporcjonalnym do rozmiaru, więc od razu widać, co zajmuje najwięcej miejsca:
1// Analiza bundla za pomocą webpack-bundle-analyzer
2const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
3
4module.exports = {
5 plugins: [
6 new BundleAnalyzerPlugin({
7 analyzerMode: 'static',
8 openAnalyzer: false,
9 generateStatsFile: true
10 })
11 ]
12};Własne strategie ładowania
Możesz też dobierać wersję komponentu do jakości połączenia, którą podaje Network Information API (navigator.connection). Działa ono tylko w przeglądarkach opartych na Chromium, więc w pozostałych kod wybierze lżejszą wersję:
1// Strategia ładowania na podstawie połączenia internetowego
2async function loadComponentBasedOnConnection() {
3 const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
4
5 if (connection && connection.effectiveType === '4g') {
6 // Szybkie połączenie - ładujemy pełną wersję
7 return import('./components/FullFeaturedComponent');
8 } else {
9 // Wolne połączenie - ładujemy uproszczoną wersję
10 return import('./components/LightweightComponent');
11 }
12}Efekty uboczne (side effects) i package.json
Pole sideEffects w package.json biblioteki mówi bundlerowi, czy jej moduły mają efekty uboczne. Wartość false oznacza, że nie ma ich żaden plik, więc moduł, z którego nic nie importujesz, bundler może całkowicie pominąć:
1{
2 "name": "my-library",
3 "version": "1.0.0",
4 "sideEffects": false,
5 "main": "dist/index.js",
6 "module": "dist/index.esm.js"
7}Pliki z efektami ubocznymi, których bundler nie może pominąć, wymieniasz w tablicy:
1{
2 "sideEffects": [
3 "src/polyfills.js",
4 "src/global-styles.css",
5 "**/*.css"
6 ]
7}Praktyczne wskazówki
1. Monitorowanie rozmiaru bundla
Zanim dodasz bibliotekę do projektu, sprawdź, ile waży po minifikacji i po kompresji gzip:
1# Używanie bundlephobia do sprawdzania rozmiaru bibliotek
2npm install -g bundle-phobia-cli
3bundle-phobia lodash
4bundle-phobia lodash-es2. Webpack Bundle Analyzer
1npm install --save-dev webpack-bundle-analyzer
2npx webpack --profile --json=stats.json
3npx webpack-bundle-analyzer stats.json3. Lighthouse CI dla monitorowania
1# .github/workflows/lighthouse.yml
2- name: Lighthouse CI
3 run: |
4 npm install -g @lhci/cli
5 lhci autorunPrzykład pełnej optymalizacji
1// Przed optymalizacją
2import React from 'react';
3import * as MUI from '@mui/material';
4import _ from 'lodash';
5import moment from 'moment';
6import './styles.css';
7
8function App() {
9 const [users, setUsers] = React.useState([]);
10
11 const debouncedSearch = _.debounce((query) => {
12 // Wyszukiwanie użytkowników
13 }, 300);
14
15 return (
16 <MUI.Container>
17 <MUI.TextField onChange={(e) => debouncedSearch(e.target.value)} />
18 {users.map(user => (
19 <MUI.Card key={user.id}>
20 <MUI.Typography>
21 {user.name} - {moment(user.createdAt).format('DD/MM/YYYY')}
22 </MUI.Typography>
23 </MUI.Card>
24 ))}
25 </MUI.Container>
26 );
27}1// Po optymalizacji
2import React, { useState, lazy, Suspense } from 'react';
3import { Container, TextField } from '@mui/material';
4import { debounce } from 'lodash-es';
5import { format } from 'date-fns';
6import { pl } from 'date-fns/locale';
7
8// Lazy loading dla rzadko używanych komponentów
9const UserCard = lazy(() => import('./components/UserCard'));
10
11function App() {
12 const [users, setUsers] = useState([]);
13
14 const debouncedSearch = debounce((query) => {
15 // Wyszukiwanie użytkowników
16 }, 300);
17
18 return (
19 <Container>
20 <TextField onChange={(e) => debouncedSearch(e.target.value)} />
21 <Suspense fallback={<div>Ładowanie...</div>}>
22 {users.map(user => (
23 <UserCard
24 key={user.id}
25 name={user.name}
26 date={format(new Date(user.createdAt), 'dd/MM/yyyy', { locale: pl })}
27 />
28 ))}
29 </Suspense>
30 </Container>
31 );
32}Korzyści i rezultaty
Oszczędności w rozmiarze bundla
- Lodash: 70KB → 2KB (pojedyncza funkcja)
- Moment.js: 230KB → 11KB (date-fns)
- Material-UI: 1.2MB → 200KB (tylko używane komponenty)
- Bootstrap: 150KB → 30KB (tylko potrzebne moduły)
Wpływ na wydajność
1// Metryki wydajności
2const performanceMetrics = {
3 beforeOptimization: {
4 bundleSize: '2.5MB',
5 firstContentfulPaint: '3.2s',
6 largestContentfulPaint: '4.8s',
7 timeToInteractive: '5.2s'
8 },
9 afterOptimization: {
10 bundleSize: '450KB',
11 firstContentfulPaint: '1.1s',
12 largestContentfulPaint: '1.8s',
13 timeToInteractive: '2.1s'
14 }
15};Podsumowanie
Code splitting i tree shaking to fundamentalne techniki optymalizacji nowoczesnych aplikacji web. Pozwalają one:
- Zmniejszyć początkowy rozmiar bundla - użytkownicy pobierają tylko kod potrzebny do pierwszego renderu
- Przyspieszyć czas ładowania - mniejsze pliki = szybsze pobieranie
- Poprawić User Experience - aplikacja działa szybciej i płynniej
- Zaoszczędzić transfer danych - szczególnie ważne na mobilnych urządzeniach
- Umożliwić lepsze cachowanie - biblioteki zewnętrzne (vendor) rzadko się zmieniają
Kluczem do sukcesu jest systematyczne podejście: regularny audyt bundla, monitoring rozmiaru bibliotek i świadome decyzje o tym, jakie zależności dodajemy do projektu.
W edytorze poniżej policzysz na przykładowych danych, ile kilobajtów zwiedzający oszczędza na starcie dzięki code splittingowi i tree shakingowi.
Kod do tej lekcji: index.js
1// Code splitting i tree shaking w liczbach: symulacja bundla Parku Jurajskiego
2// Zmieniaj dane poniżej i obserwuj, ile kilobajtów zwiedzający pobiera na starcie.
3
4// static = zwykły import (moduł trafia do pakietu startowego)
5// dynamic = import('./plik.js') (bundler wydziela osobny chunk pobierany na żądanie)
6const modules = [
7 { file: 'app.js', kb: 45, load: 'static' },
8 { file: 'park-map.js', kb: 120, load: 'static' },
9 { file: 'admin-panel.js', kb: 310, load: 'dynamic' },
10 { file: 'genetics-charts.js', kb: 180, load: 'dynamic' },
11];
12
13const totalKb = (list) => list.reduce((sum, m) => sum + m.kb, 0);
14const initial = modules.filter((m) => m.load === 'static');
15const chunks = modules.filter((m) => m.load === 'dynamic');
16
17console.log('Bez code splittingu na starcie:', totalKb(modules), 'kB');
18console.log('Z code splittingiem na starcie:', totalKb(initial), 'kB');
19console.log('Chunki pobierane na żądanie:', chunks.map((m) => m.file + ' (' + m.kb + ' kB)').join(', '));
20
21// Tree shaking: do bundla trafiają tylko eksporty, które importujesz (rozmiary w kB)
22const parkUtils = { formatDate: 2, debounce: 3, deepClone: 6, chartEngine: 95 };
23const imported = ['formatDate', 'debounce'];
24const libraryKb = Object.values(parkUtils).reduce((sum, kb) => sum + kb, 0);
25const shakenKb = imported.reduce((sum, name) => sum + parkUtils[name], 0);
26console.log('Cała biblioteka:', libraryKb, 'kB');
27console.log('Po tree shakingu:', shakenKb, 'kB');
28
29// Spróbuj: zmień load modułu park-map.js na 'dynamic' albo dopisz 'chartEngine' do imported.Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Czym charakteryzuje się pure function (czysta funkcja)?
2. Co to jest immutable state (niemutowalny stan)?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj kroki implementacji niemutowalnego stanu:
- Edytor kodu
W pliku index.html jest kontener #park-app i skrypt z klasą ParkInterface, która buduje panel sterowania parku z danych parkData (wygląd paneli jest w style.css). Uzupełnij luki: ___BLANK1___ to metoda tablicy, która z każdego dinozaura robi napis z elementem <li>, ___BLANK2___ to metoda tablicy, która skleja te napisy w jeden (bez przecinków między nimi), a ___BLANK3___ to właściwość elementu, przez którą render() wstawia gotowy HTML do kontenera. Po uruchomieniu w #park-app mają być trzy panele .panel z nagłówkami h2, a w pierwszym lista wszystkich dinozaurów; render() ma działać też dla innego kontenera i innych danych.
- Klikanie w kolejności
Ułóż elementy wzorca Factory:
- Edytor kodu
W pliku index.js jest klasa TimeSimulation. Uzupełnij luki: ___BLANK1___ to liczba godzin w dobie, po której licznik godzin wraca do 0 (tick() po 23:00 daje 0:00 i kolejny dzień), ___BLANK2___ to operator logiczny w isNight(), bo noc to godziny od 20 do 23 oraz od 0 do 5, a ___BLANK3___ to funkcja przeglądarki, która wywołuje tick() co intervalMs milisekund aż do clearInterval. Drugie wywołanie start() nie może uruchomić drugiej pętli, a stop() ją zatrzymuje.