Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Code splitting i tree shaking

W świecie nowoczesnego web development, 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 bundle, 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 potrzebny

Rodzaje code splitting

1. Route-based code splitting

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-dom';
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. Component-based code splitting

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. Dynamic imports

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. Vendor code splitting

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

  1. 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');
  1. 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});
  1. 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
5import 'bootstrap/scss/functions';
6import 'bootstrap/scss/variables';
7import 'bootstrap/scss/utilities/display';
8import 'bootstrap/scss/utilities/spacing';

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// ❌ Importowanie całej biblioteki Material-UI
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 exporty
6    sideEffects: false, // Brak side effects - bezpieczne usuwanie
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
2import { defineConfig } from 'vite';
3
4export default defineConfig({
5  build: {
6    rollupOptions: {
7      output: {
8        manualChunks: {
9          vendor: ['react', 'react-dom'],
10          utils: ['lodash-es', 'date-fns']
11        }
12      }
13    }
14  }
15});

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 - wysokiy 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);

Bundle analysis

1// Analiza bundle 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};

Custom loading strategies

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}

Side effects i package.json

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}
1// Oznaczanie konkretnych plików jako mające side effects
2{
3  "sideEffects": [
4    "src/polyfills.js",
5    "src/global-styles.css",
6    "**/*.css"
7  ]
8}

Praktyczne wskazówki

1. Monitorowanie rozmiaru bundle

1# Używanie bundlephobia do sprawdzania rozmiaru bibliotek
2npm install -g bundlephobia-cli
3bundlephobia lodash
4bundlephobia lodash-es

2. Webpack Bundle Analyzer

1npm install --save-dev webpack-bundle-analyzer
2npx webpack-bundle-analyzer dist/static/js/*.js

3. Lighthouse CI dla monitorowania

1# .github/workflows/lighthouse.yml
2- name: Lighthouse CI
3  run: |
4    npm install -g @lhci/cli
5    lhci autorun

Przykł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 bundle

  • 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:

  1. Zmniejszyć inicial bundle size - użytkownicy pobierają tylko kod potrzebny do pierwszego renderu
  2. Przyspieszyć czas ładowania - mniejsze pliki = szybsze pobieranie
  3. Poprawić User Experience - aplikacja działa szybciej i płynniej
  4. Zaoszczędzić transfer danych - szczególnie ważne na mobilnych urządzeniach
  5. Umożliwić lepsze cachowanie - vendor libraries się rzadko zmieniają

Kluczem do sukcesu jest systematyczne podejście: regularny audyt bundle, monitoring rozmiaru bibliotek i świadome decyzje o tym, jakie zależności dodajemy do projektu.

Ir a CodeWorlds