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.
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.
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 potrzebnyNajprostszy 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}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}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);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};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).
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));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 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 - 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};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';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);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';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};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});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};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);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};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}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}1# Używanie bundlephobia do sprawdzania rozmiaru bibliotek
2npm install -g bundlephobia-cli
3bundlephobia lodash
4bundlephobia lodash-es1npm install --save-dev webpack-bundle-analyzer
2npx webpack-bundle-analyzer dist/static/js/*.js1# .github/workflows/lighthouse.yml
2- name: Lighthouse CI
3 run: |
4 npm install -g @lhci/cli
5 lhci autorun1// 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}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};Code splitting i tree shaking to fundamentalne techniki optymalizacji nowoczesnych aplikacji web. Pozwalają one:
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.