Kurs JavaScript i TypeScript · Moduł 9: Wzorce projektowe i architektura

Optymalizacja bundla - analiza i optymalizacja pakietów

17 min czytania
W tej lekcji9

Wyobraź sobie ciężarówkę, która wiezie do parku jeden worek karmy, a ciągnie cały magazyn, bo nikt nie sprawdził paki. Optymalizacja bundla to kompleksowy proces analizy i optymalizacji pakietów JavaScript: mniejszy rozmiar, szybsze ładowanie i efektywne wykorzystanie zasobów przeglądarki. To ostatni krok po code splittingu, tree shakingu i lazy loadingu.

Dlaczego to ważne?

Poniższe liczby to hipotetyczny przykład, a nie wynik badań, ale pokazują, co mierzymy:

1// Hipotetyczny przykład: liczby ilustracyjne, nie wyniki pomiarów
2
3// Metryki wydajności przed optymalizacją
4const beforeOptimization = {
5  bundleSize: '2.8MB',
6  firstContentfulPaint: '3.4s',
7  largestContentfulPaint: '5.1s',
8  timeToInteractive: '6.2s',
9  cumulativeLayoutShift: 0.25
10};
11
12// Metryki wydajności po optymalizacji
13const afterOptimization = {
14  bundleSize: '420KB',
15  firstContentfulPaint: '0.9s',
16  largestContentfulPaint: '1.6s',
17  timeToInteractive: '2.1s',
18  cumulativeLayoutShift: 0.05
19};
20
21// Poprawa wydajności
22const improvement = {
23  bundleSizeReduction: '85%',
24  fcpImprovement: '73%',
25  lcpImprovement: '69%',
26  ttiImprovement: '66%',
27  clsImprovement: '80%'
28};
29
30// Analiza kosztów business impact
31const businessImpact = {
32  conversionRate: {
33    before: '2.1%',
34    after: '3.8%',
35    improvement: '+81%'
36  },
37  bounceRate: {
38    before: '68%',
39    after: '42%',
40    improvement: '-38%'
41  },
42  mobileUsersLost: {
43    before: '35%', // Przez wolne ładowanie
44    after: '12%',
45    improvement: '-66%'
46  },
47  revenueImpact: {
48    monthlyLoss: '$24,000', // Z powodu wolnych bundli
49    monthlyGain: '$45,000'  // Po optymalizacji
50  }
51};

Do Core Web Vitals należą z nich LCP i CLS (trzecią jest INP), FCP to metryka pomocnicza, a TTI od Lighthouse 10 nie wlicza się do wyniku. Własne liczby zawsze zmierz.

Narzędzia do analizy bundli

Code splitting wdrażasz w czterech krokach: analiza rozmiaru bundla, wybór punktów podziału (split points), konfiguracja dynamicznych importów i weryfikacja ładowania chunków.

Webpack Bundle Analyzer

Webpack Bundle Analyzer rysuje mapę modułów i pokazuje, które zajmują najwięcej miejsca:

1# Instalacja i użycie
2npm install --save-dev webpack-bundle-analyzer
3
4# Generowanie raportu (analyzer czyta plik statystyk webpacka)
5npx webpack --profile --json=stats.json
6npx webpack-bundle-analyzer stats.json
7
8# Integracja z webpack config
9const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
10
11module.exports = {
12  plugins: [
13    new BundleAnalyzerPlugin({
14      analyzerMode: 'static',
15      openAnalyzer: false,
16      generateStatsFile: true,
17      reportFilename: 'bundle-report.html'
18    })
19  ]
20};

Analyzer czyta plik statystyk (stats.json), a nie gotowe pliki .js.

Analiza w CI/CD

Workflow GitHub Actions sprawdza rozmiar przy każdym pushu i pull requeście:

1# .github/workflows/bundle-analysis.yml
2name: Bundle Analysis
3on: [push, pull_request]
4
5jobs:
6  analyze:
7    runs-on: ubuntu-latest
8    steps:
9      - uses: actions/checkout@v4
10
11      - name: Setup Node.js
12        uses: actions/setup-node@v4
13        with:
14          node-version: '22'
15
16      - name: Install dependencies
17        run: npm ci
18
19      - name: Build and analyze
20        run: |
21          npm run build
22          npx size-limit
23
24      - name: Upload bundle stats
25        uses: actions/upload-artifact@v4
26        with:
27          name: bundle-stats
28          path: dist/stats.json

npx size-limit (pakiety size-limit i @size-limit/file w devDependencies) przerywa build, gdy plik przekroczy limit z package.json.

Source Map Explorer

Source Map Explorer pokazuje, z których plików źródłowych składa się gotowy bundle:

1# Analiza source map
2npm install --global source-map-explorer
3
4# Analiza konkretnego bundle
5source-map-explorer bundle.js bundle.js.map
6
7# Raport HTML (zachowaj go, by porównać z kolejną wersją)
8source-map-explorer bundle.js --html report.html

Porównywania wersji nie ma, więc raporty HTML zachowuj między wydaniami.

Webpack Dashboard

Webpack Dashboard zamienia surowy log webpacka w czytelny panel w terminalu:

1// Interaktywny dashboard podczas development
2const DashboardPlugin = require('webpack-dashboard/plugin');
3
4module.exports = {
5  plugins: [
6    new DashboardPlugin()
7  ]
8};

Panel pojawi się, gdy uruchomisz webpacka przez CLI pakietu, np. webpack-dashboard -- webpack --watch. Rozmiary modułów widzisz wtedy przy każdej zmianie.

Zaawansowane techniki optymalizacji

Strategie podziału na chunki

splitChunks dzieli node_modules na grupy (cache groups), które przeglądarka cache'uje niezależnie od kodu aplikacji:

1// webpack.config.js - Zaawansowana konfiguracja chunks
2module.exports = {
3  optimization: {
4    splitChunks: {
5      chunks: 'all',
6      minSize: 20000,
7      maxSize: 244000,
8      cacheGroups: {
9        // Framework chunks (React, Angular, Vue)
10        framework: {
11          test: /[\\/]node_modules[\\/](react|react-dom|@angular|vue)[\\/]/,
12          name: 'framework',
13          priority: 40,
14          enforce: true
15        },
16
17        // UI Libraries chunk
18        ui: {
19          test: /[\\/]node_modules[\\/](@mui|antd|bootstrap|@chakra-ui)[\\/]/,
20          name: 'ui-libs',
21          priority: 30,
22          enforce: true
23        },
24
25        // Utility libraries chunk
26        utils: {
27          test: /[\\/]node_modules[\\/](lodash|ramda|date-fns|moment)[\\/]/,
28          name: 'utils',
29          priority: 20,
30          enforce: true
31        },
32
33        // Common application code
34        common: {
35          name: 'common',
36          minChunks: 2,
37          priority: 10,
38          reuseExistingChunk: true
39        },
40
41        // Large libraries w osobnych chunkach
42        largeDeps: {
43          test: /[\\/]node_modules[\\/](monaco-editor|three|d3)[\\/]/,
44          name(module) {
45            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
46            return `lib.${packageName.replace('@', '')}`;
47          },
48          priority: 35,
49          enforce: true
50        }
51      }
52    }
53  }
54};

Wyższy priority wygrywa przy konflikcie grup, a enforce: true pomija progi rozmiaru. Po zmianie Twojego kodu użytkownik nie pobiera ponownie Reacta.

Module Federation dla mikrofrontendów

Module Federation, wbudowane w webpack 5, pozwala niezależnym aplikacjom dzielić się kodem w trakcie działania:

1// webpack.config.js - Host application
2const { ModuleFederationPlugin } = require('webpack').container;
3
4module.exports = {
5  plugins: [
6    new ModuleFederationPlugin({
7      name: 'host',
8      filename: 'remoteEntry.js',
9      remotes: {
10        userDashboard: 'userDashboard@http://localhost:3001/remoteEntry.js',
11        productCatalog: 'productCatalog@http://localhost:3002/remoteEntry.js',
12        paymentSystem: 'paymentSystem@http://localhost:3003/remoteEntry.js'
13      },
14      shared: {
15        react: {
16          singleton: true,
17          requiredVersion: '^19.0.0',
18          eager: true
19        },
20        'react-dom': {
21          singleton: true,
22          requiredVersion: '^19.0.0',
23          eager: true
24        }
25      }
26    })
27  ]
28};
29
30// Micro-frontend - User Dashboard
31const { ModuleFederationPlugin } = require('webpack').container;
32
33module.exports = {
34  plugins: [
35    new ModuleFederationPlugin({
36      name: 'userDashboard',
37      filename: 'remoteEntry.js',
38      exposes: {
39        './UserProfile': './src/components/UserProfile',
40        './UserSettings': './src/components/UserSettings'
41      },
42      shared: {
43        react: { singleton: true },
44        'react-dom': { singleton: true }
45      }
46    })
47  ]
48};

singleton: true gwarantuje jedną kopię Reacta, a dwa module.exports to dwa osobne pliki konfiguracyjne.

Budowanie przyrostowe

Cache na dysku skraca kolejne buildy, bo webpack przebudowuje tylko to, co się zmieniło:

1// Konfiguracja cache webpacka przyspieszająca kolejne buildy
2const path = require('path');
3
4module.exports = {
5  cache: {
6    type: 'filesystem',
7    cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
8    buildDependencies: {
9      config: [__filename],
10      tsconfig: [path.resolve(__dirname, 'tsconfig.json')],
11      packagejson: [path.resolve(__dirname, 'package.json')]
12    },
13    version: '1.0',
14    store: 'pack',
15    compression: 'gzip'
16  },
17
18  // Persistent cache dla node_modules
19  snapshot: {
20    managedPaths: [path.resolve(__dirname, 'node_modules')],
21    buildDependencies: {
22      hash: true,
23      timestamp: true
24    },
25    module: {
26      timestamp: true
27    },
28    resolve: {
29      timestamp: true
30    }
31  }
32};

buildDependencies wskazuje pliki, których zmiana unieważnia cache.

Budżety i monitoring

Konfiguracja budżetu

Budżet wydajności (performance budget) to limit rozmiaru, który narzędzie size-limit czyta z package.json:

1{
2  "size-limit": [
3    {
4      "path": "./dist/js/main.*.js",
5      "limit": "170 kB",
6      "gzip": true
7    },
8    {
9      "path": "./dist/js/vendor.*.js",
10      "limit": "300 kB",
11      "gzip": true
12    },
13    {
14      "path": "./dist/css/*.css",
15      "limit": "50 kB",
16      "gzip": true
17    },
18    {
19      "path": "./dist/js/runtime.*.js",
20      "limit": "10 kB",
21      "gzip": true
22    }
23  ]
24}

Limity dotyczą rozmiaru po gzip. Pierwszą linię usuń, bo JSON nie ma komentarzy.

Automatyczne monitorowanie wydajności

Monitor zbiera czasy ładowania z Performance API u prawdziwych użytkowników i porównuje je z progami:

1// Performance monitoring service
2class BundlePerformanceMonitor {
3  constructor() {
4    this.metrics = {
5      bundleSizes: new Map(),
6      loadTimes: new Map(),
7      cacheHitRates: new Map()
8    };
9
10    this.thresholds = {
11      maxBundleSize: 250000, // 250KB
12      maxLoadTime: 3000,     // 3s
13      minCacheHitRate: 80    // 80%
14    };
15  }
16
17  async measureBundlePerformance() {
18    const performanceData = {
19      timestamp: Date.now(),
20      metrics: await this.collectMetrics(),
21      violations: this.checkViolations(),
22      recommendations: this.generateRecommendations()
23    };
24
25    await this.sendToMonitoring(performanceData);
26    return performanceData;
27  }
28
29  async collectMetrics() {
30    const bundleEntries = performance.getEntriesByType('navigation');
31    const resourceEntries = performance.getEntriesByType('resource');
32
33    return {
34      pageLoadTime: bundleEntries[0]?.loadEventEnd - bundleEntries[0]?.fetchStart,
35      resourceLoadTimes: resourceEntries
36        .filter(entry => entry.name.includes('.js') || entry.name.includes('.css'))
37        .map(entry => ({
38          name: entry.name,
39          size: entry.transferSize,
40          loadTime: entry.responseEnd - entry.fetchStart,
41          cached: entry.transferSize === 0
42        })),
43      memoryUsage: performance.memory ? {
44        used: performance.memory.usedJSHeapSize,
45        total: performance.memory.totalJSHeapSize,
46        limit: performance.memory.jsHeapSizeLimit
47      } : null
48    };
49  }
50
51  checkViolations() {
52    const violations = [];
53
54    // Sprawdzenie rozmiarów bundli (transferSize z Resource Timing)
55    performance.getEntriesByType('resource')
56      .filter(entry => entry.name.includes('.js'))
57      .forEach(({ name: bundleName, transferSize: size }) => {
58        if (size > this.thresholds.maxBundleSize) {
59          violations.push({
60            type: 'SIZE_VIOLATION',
61            bundle: bundleName,
62            current: size,
63            threshold: this.thresholds.maxBundleSize,
64            severity: 'HIGH'
65          });
66        }
67      });
68
69    return violations;
70  }
71
72  generateRecommendations() {
73    const recommendations = [];
74
75    // Analiza duplikatów w bundlach
76    const duplicatedModules = this.findDuplicatedModules();
77    if (duplicatedModules.length > 0) {
78      recommendations.push({
79        type: 'REMOVE_DUPLICATES',
80        modules: duplicatedModules,
81        estimatedSavings: this.calculateDuplicateSavings(duplicatedModules)
82      });
83    }
84
85    // Analiza nieużywanych dependencji
86    const unusedDeps = this.findUnusedDependencies();
87    if (unusedDeps.length > 0) {
88      recommendations.push({
89        type: 'REMOVE_UNUSED_DEPS',
90        dependencies: unusedDeps,
91        estimatedSavings: this.calculateUnusedDepsSavings(unusedDeps)
92      });
93    }
94
95    return recommendations;
96  }
97}
98
99// Automatyczne monitorowanie w production
100if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
101  const monitor = new BundlePerformanceMonitor();
102
103  // Pomiar po załadowaniu strony
104  window.addEventListener('load', () => {
105    setTimeout(() => {
106      monitor.measureBundlePerformance();
107    }, 2000);
108  });
109}

performance.memory istnieje tylko w Chromium, stąd sprawdzenie przed użyciem. Metody pomocnicze (sendToMonitoring, findDuplicatedModules, findUnusedDependencies i te liczące oszczędności) piszesz sam.

Real User Monitoring (RUM)

PerformanceObserver powiadamia o każdym pobranym pliku i o długich zadaniach (long tasks):

1// RUM dla bundle performance
2class RealUserBundleMonitoring {
3  constructor() {
4    this.sessionId = this.generateSessionId();
5    this.startTime = performance.now();
6    this.setupObservers();
7  }
8
9  setupObservers() {
10    // Performance Observer dla resource timing
11    if ('PerformanceObserver' in window) {
12      const observer = new PerformanceObserver((list) => {
13        list.getEntries().forEach(entry => {
14          if (entry.name.includes('.js') || entry.name.includes('.css')) {
15            this.trackResourceLoad(entry);
16          }
17        });
18      });
19
20      observer.observe({ entryTypes: ['resource'] });
21    }
22
23    // Long task observer
24    if ('PerformanceObserver' in window) {
25      const longTaskObserver = new PerformanceObserver((list) => {
26        list.getEntries().forEach(entry => {
27          this.trackLongTask(entry);
28        });
29      });
30
31      longTaskObserver.observe({ entryTypes: ['longtask'] });
32    }
33  }
34
35  trackResourceLoad(entry) {
36    const resourceData = {
37      sessionId: this.sessionId,
38      timestamp: Date.now(),
39      resourceType: entry.name.includes('.js') ? 'javascript' : 'css',
40      resourceName: entry.name,
41      loadTime: entry.responseEnd - entry.fetchStart,
42      transferSize: entry.transferSize,
43      cached: entry.transferSize === 0,
44      userAgent: navigator.userAgent,
45      connectionType: navigator.connection?.effectiveType
46    };
47
48    this.sendMetric('resource_load', resourceData);
49  }
50
51  trackLongTask(entry) {
52    const longTaskData = {
53      sessionId: this.sessionId,
54      timestamp: Date.now(),
55      duration: entry.duration,
56      startTime: entry.startTime,
57      attribution: entry.attribution
58    };
59
60    this.sendMetric('long_task', longTaskData);
61  }
62
63  async sendMetric(type, data) {
64    // Wysyłanie do systemu analytics
65    if (navigator.sendBeacon) {
66      navigator.sendBeacon('/api/metrics', JSON.stringify({
67        type,
68        data,
69        metadata: {
70          url: window.location.href,
71          timestamp: Date.now(),
72          sessionId: this.sessionId
73        }
74      }));
75    }
76  }
77}

Long task blokuje główny wątek co najmniej 50 ms, a sendBeacon działa nawet przy zamykaniu karty.

Optymalizacja specyficznych bibliotek

Optymalizacja Lodasha

Tu wraca tree shaking, czyli automatyczne usuwanie nieużywanego kodu (dead code) z finalnego bundla. Działa na modułach ES, bo import i export są statyczne, w przeciwieństwie do require z CommonJS. Pole "sideEffects": false w package.json mówi bundlerowi, że moduły paczki nie mają efektów ubocznych, więc nieużywane może pominąć. Klasyczny Lodash to CommonJS:

1# Budowa modułowej wersji Lodash w formacie modułów ES (tak powstaje lodash-es)
2npx lodash-cli modularize exports=es
3
4# Babel plugin dla automatycznej optymalizacji
5npm install --save-dev babel-plugin-lodash

Druga część to wtyczka Babel, która zamienia import całej biblioteki na importy pojedynczych funkcji:

1// babel.config.js
2module.exports = {
3  plugins: [
4    'lodash' // Automatycznie zamienia importy na cherry-picked
5  ]
6};
7
8// Przed optymalizacją (70KB)
9import _ from 'lodash';
10
11// Po optymalizacji (2KB)
12import debounce from 'lodash/debounce';
13import throttle from 'lodash/throttle';

Dziś prościej sięgnąć po lodash-es. Z tego samego powodu wzorca Module z IIFE, choć ukrywa prywatne zmienne, nie da się przyciąć, bo jest jednym obiektem.

Optymalizacja bibliotek do dat

Moment.js nie wspiera tree shakingu, a jego autorzy sami polecają nowsze alternatywy:

1// Porównanie rozmiarów bibliotek dat (orientacyjne, zależą od wersji; aktualne sprawdzisz np. w bundlephobia)
2const dateLibraryComparison = {
3  'moment': {
4    size: '230KB',
5    gzipped: '67KB',
6    treeshaking: false
7  },
8  'date-fns': {
9    size: '78KB',
10    gzipped: '13KB',
11    treeshaking: true
12  },
13  'dayjs': {
14    size: '9KB',
15    gzipped: '3KB',
16    treeshaking: true
17  },
18  'luxon': {
19    size: '67KB',
20    gzipped: '19KB',
21    treeshaking: true
22  }
23};
24
25// Migration guide z Moment.js do date-fns
26const migrationExamples = {
27  before: `
28    import moment from 'moment';
29
30    const formatted = moment().format('DD/MM/YYYY');
31    const isAfter = moment(date1).isAfter(date2);
32  `,
33  after: `
34    import { format, isAfter } from 'date-fns';
35    import { pl } from 'date-fns/locale';
36
37    const formatted = format(new Date(), 'dd/MM/yyyy', { locale: pl });
38    const isAfterResult = isAfter(date1, date2);
39  `
40};

Rozmiary są orientacyjne. Uwaga na tokeny: date-fns używa dd i yyyy, a Moment DD i YYYY.

Optymalizacja frameworków CSS

PurgeCSS skanuje pliki źródłowe i usuwa selektory CSS, których w nich nie znalazł:

1// PurgeCSS dla usuwania nieużywanych styli
2const purgecss = require('@fullhuman/postcss-purgecss');
3
4module.exports = {
5  plugins: [
6    purgecss({
7      content: [
8        './src/**/*.html',
9        './src/**/*.js',
10        './src/**/*.tsx'
11      ],
12      safelist: [
13        /^tooltip-/,
14        /^modal-/,
15        /^dropdown-/
16      ],
17      defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
18    })
19  ]
20};
21
22// Przykładowe redukcje rozmiaru (wartości ilustracyjne, zależą od projektu)
23const cssOptimizationResults = {
24  bootstrap: {
25    before: '150KB',
26    after: '23KB',
27    reduction: '85%'
28  },
29  tailwind: {
30    before: '3.2MB',
31    after: '12KB',
32    reduction: '99.6%'
33  },
34  materialUI: {
35    before: '890KB',
36    after: '145KB',
37    reduction: '84%'
38  }
39};

safelist chroni klasy dodawane dynamicznie przez JavaScript.

Zaawansowana optymalizacja webpacka

Własne pluginy webpacka

Własny plugin to klasa z metodą apply, która podpina się pod hooki kompilatora:

1// Plugin do analizy i optymalizacji bundle
2class BundleOptimizationPlugin {
3  constructor(options = {}) {
4    this.options = {
5      reportPath: 'bundle-optimization-report.json',
6      thresholds: {
7        maxChunkSize: 250000,
8        maxModuleSize: 50000
9      },
10      ...options
11    };
12  }
13
14  apply(compiler) {
15    compiler.hooks.emit.tapAsync('BundleOptimizationPlugin', (compilation, callback) => {
16      const report = this.generateOptimizationReport(compilation);
17
18      // Zapisz raport
19      compilation.assets[this.options.reportPath] = {
20        source: () => JSON.stringify(report, null, 2),
21        size: () => JSON.stringify(report, null, 2).length
22      };
23
24      // Loguj ostrzeżenia
25      this.logWarnings(compilation, report);
26
27      callback();
28    });
29  }
30
31  generateOptimizationReport(compilation) {
32    const chunks = Array.from(compilation.chunks);
33    const modules = Array.from(compilation.modules);
34
35    return {
36      timestamp: new Date().toISOString(),
37      summary: {
38        totalChunks: chunks.length,
39        totalModules: modules.length,
40        totalSize: this.calculateTotalSize(chunks)
41      },
42      chunks: chunks.map(chunk => ({
43        id: chunk.id,
44        name: chunk.name,
45        size: chunk.size,
46        modules: chunk.getModules().length,
47        parents: Array.from(chunk.groupsIterable, g => g.getParents()),
48        children: Array.from(chunk.groupsIterable, g => g.getChildren())
49      })),
50      optimizationOpportunities: this.findOptimizationOpportunities(chunks, modules),
51      recommendations: this.generateRecommendations(chunks, modules)
52    };
53  }
54
55  findOptimizationOpportunities(chunks, modules) {
56    const opportunities = [];
57
58    // Znajdź duże moduły
59    const largeModules = modules.filter(module =>
60      module.size() > this.options.thresholds.maxModuleSize
61    );
62
63    if (largeModules.length > 0) {
64      opportunities.push({
65        type: 'LARGE_MODULES',
66        modules: largeModules.map(m => ({
67          identifier: m.identifier(),
68          size: m.size(),
69          reasons: m.reasons.map(r => r.module?.identifier())
70        }))
71      });
72    }
73
74    // Znajdź duplikaty
75    const moduleDuplicates = this.findModuleDuplicates(modules);
76    if (moduleDuplicates.length > 0) {
77      opportunities.push({
78        type: 'MODULE_DUPLICATES',
79        duplicates: moduleDuplicates
80      });
81    }
82
83    return opportunities;
84  }
85}
86
87// Użycie plugin
88module.exports = {
89  plugins: [
90    new BundleOptimizationPlugin({
91      thresholds: {
92        maxChunkSize: 200000,
93        maxModuleSize: 30000
94      }
95    })
96  ]
97};

To szkic w API webpacka 4, a metody pomocnicze (calculateTotalSize, findModuleDuplicates, generateRecommendations, logWarnings) piszesz sam. W webpacku 5 zasoby dodajesz w hooku processAssets przez compilation.emitAsset, rozmiar chunka podaje compilation.chunkGraph.getChunkSize(chunk), a zamiast usuniętego module.reasons używasz compilation.moduleGraph.getIncomingConnections(module).

Optymalizacja chunka runtime

Runtime to kod webpacka ładujący chunki, a wydzielony nie unieważnia cache aplikacji. Obok minifikacja JS i CSS:

1// Optymalizacja runtime chunk
2const TerserPlugin = require('terser-webpack-plugin');
3const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
4
5module.exports = {
6  optimization: {
7    runtimeChunk: {
8      name: entrypoint => `runtime-${entrypoint.name}`
9    },
10
11    // Minimizer configuration
12    minimizer: [
13      new TerserPlugin({
14        parallel: true,
15        terserOptions: {
16          parse: {
17            ecma: 2020
18          },
19          compress: {
20            ecma: 2020,
21            comparisons: false,
22            inline: 2,
23            drop_console: process.env.NODE_ENV === 'production',
24            drop_debugger: process.env.NODE_ENV === 'production',
25            pure_funcs: ['console.log', 'console.info', 'console.debug']
26          },
27          mangle: {
28            safari10: true
29          },
30          output: {
31            ecma: 2020,
32            comments: false,
33            ascii_only: true
34          }
35        }
36      }),
37
38      new CssMinimizerPlugin({
39        parallel: true,
40        minimizerOptions: {
41          preset: [
42            'default',
43            {
44              discardComments: { removeAll: true },
45              normalizeWhitespace: true,
46              colormin: true,
47              convertValues: true,
48              discardDuplicates: true,
49              discardEmpty: true,
50              mergeRules: true,
51              minifyFontValues: true,
52              minifyGradients: true,
53              minifyParams: true,
54              minifySelectors: true,
55              normalizeCharset: true,
56              normalizeDisplayValues: true,
57              normalizePositions: true,
58              normalizeRepeatStyle: true,
59              normalizeString: true,
60              normalizeTimingFunctions: true,
61              normalizeUnicode: true,
62              normalizeUrl: true,
63              orderedValues: true,
64              reduceIdents: true,
65              reduceInitial: true,
66              reduceTransforms: true,
67              svgo: true,
68              uniqueSelectors: true
69            }
70          ]
71        }
72      })
73    ]
74  }
75};

drop_console działa tylko w produkcji, a pure_funcs wskazuje funkcje, których wywołania można wyciąć.

Testy bundla

Automatyczne testy bundla

Optymalizację łatwo zepsuć jednym importem, więc pilnują jej testy rozmiaru, duplikatów i tree shakingu:

1// Test suite dla bundle optimization
2const bundleTests = {
3  // Test rozmiaru bundle
4  async testBundleSize() {
5    const bundleStats = await getBundleStats();
6    const maxSize = 500000; // 500KB
7
8    expect(bundleStats.totalSize).toBeLessThan(maxSize);
9    expect(bundleStats.mainChunk).toBeLessThan(200000);
10    expect(bundleStats.vendorChunk).toBeLessThan(300000);
11  },
12
13  // Test duplikatów
14  async testNoDuplicateModules() {
15    const bundleAnalysis = await analyzeBundleContent();
16    const duplicates = bundleAnalysis.duplicatedModules;
17
18    expect(duplicates).toHaveLength(0);
19  },
20
21  // Test tree shaking effectiveness
22  async testTreeShakingEffectiveness() {
23    const unusedExports = await findUnusedExports();
24    const deadCode = await findDeadCode();
25
26    expect(unusedExports.length).toBeLessThan(5);
27    expect(deadCode.totalSize).toBeLessThan(10000);
28  },
29
30  // Test cache effectiveness
31  async testCacheability() {
32    const chunks = await getChunkManifest();
33
34    chunks.forEach(chunk => {
35      if (chunk.type === 'vendor') {
36        expect(chunk.hash).toMatch(/^[a-f0-9]{8}$/);
37        expect(chunk.cacheable).toBe(true);
38      }
39    });
40  }
41};
42
43// Integration z CI/CD
44describe('Bundle Optimization Tests', () => {
45  test('Bundle size within limits', bundleTests.testBundleSize);
46  test('No duplicate modules', bundleTests.testNoDuplicateModules);
47  test('Tree shaking effective', bundleTests.testTreeShakingEffectiveness);
48  test('Chunks properly cacheable', bundleTests.testCacheability);
49});

Funkcje pomocnicze, jak getBundleStats, piszesz sam na podstawie stats.json.

Wykrywanie regresji wydajności

Detektor regresji porównuje bieżące metryki z zapisanym punktem odniesienia (baseline):

1// System do wykrywania regresji wydajności
2class PerformanceRegressionDetector {
3  constructor() {
4    this.baselineMetrics = null;
5    this.threshold = 0.1; // 10% wzrost = regresja
6  }
7
8  async setBaseline() {
9    this.baselineMetrics = await this.measureCurrentMetrics();
10    await this.saveBaseline(this.baselineMetrics);
11  }
12
13  async detectRegressions() {
14    const currentMetrics = await this.measureCurrentMetrics();
15    const regressions = [];
16
17    if (!this.baselineMetrics) {
18      this.baselineMetrics = await this.loadBaseline();
19    }
20
21    // Sprawdź rozmiary bundli
22    const sizeRegression = this.detectSizeRegression(
23      this.baselineMetrics.bundleSize,
24      currentMetrics.bundleSize
25    );
26
27    if (sizeRegression) {
28      regressions.push(sizeRegression);
29    }
30
31    // Sprawdź czasy ładowania
32    const loadTimeRegression = this.detectLoadTimeRegression(
33      this.baselineMetrics.loadTime,
34      currentMetrics.loadTime
35    );
36
37    if (loadTimeRegression) {
38      regressions.push(loadTimeRegression);
39    }
40
41    return regressions;
42  }
43
44  detectSizeRegression(baseline, current) {
45    const increase = (current - baseline) / baseline;
46
47    if (increase > this.threshold) {
48      return {
49        type: 'SIZE_REGRESSION',
50        baseline: baseline,
51        current: current,
52        increase: `${(increase * 100).toFixed(1)}%`,
53        severity: increase > 0.25 ? 'HIGH' : 'MEDIUM'
54      };
55    }
56
57    return null;
58  }
59
60  async generateRegressionReport(regressions) {
61    return {
62      timestamp: new Date().toISOString(),
63      regressions: regressions,
64      recommendations: await this.generateRecommendations(regressions),
65      actionItems: this.generateActionItems(regressions)
66    };
67  }
68}

Wzrost ponad 10% oznacza regresję, a ponad 25% regresję o wysokim priorytecie.

Strategie wdrożeń produkcyjnych

Nowy bundle możesz wypuszczać stopniowo i porównywać warianty na prawdziwym ruchu:

1// Strategia progressive deployment dla bundli
2class ProgressiveBundleDeployment {
3  constructor() {
4    this.deploymentStages = [
5      { name: 'canary', percentage: 5 },
6      { name: 'blue-green', percentage: 50 },
7      { name: 'full', percentage: 100 }
8    ];
9  }
10
11  async deployNewBundle(bundleVersion) {
12    for (const stage of this.deploymentStages) {
13      console.log(`Deploying to ${stage.name} (${stage.percentage}% traffic)`);
14
15      await this.deployToStage(bundleVersion, stage);
16
17      const metrics = await this.monitorStageMetrics(stage, 300000); // 5 min
18
19      if (!this.validateStageSuccess(metrics)) {
20        await this.rollbackStage(stage);
21        throw new Error(`Deployment failed at ${stage.name} stage`);
22      }
23
24      console.log(`Stage ${stage.name} successful`);
25    }
26  }
27
28  async monitorStageMetrics(stage, duration) {
29    const startTime = Date.now();
30    const metrics = {
31      errorRate: [],
32      loadTimes: [],
33      bundleErrors: []
34    };
35
36    while (Date.now() - startTime < duration) {
37      const sample = await this.collectMetricsSample(stage);
38      metrics.errorRate.push(sample.errorRate);
39      metrics.loadTimes.push(sample.avgLoadTime);
40      metrics.bundleErrors.push(sample.bundleErrors);
41
42      await new Promise(resolve => setTimeout(resolve, 30000)); // 30s intervals
43    }
44
45    return metrics;
46  }
47
48  validateStageSuccess(metrics) {
49    const avgErrorRate = metrics.errorRate.reduce((a, b) => a + b) / metrics.errorRate.length;
50    const avgLoadTime = metrics.loadTimes.reduce((a, b) => a + b) / metrics.loadTimes.length;
51    const totalBundleErrors = metrics.bundleErrors.reduce((a, b) => a + b);
52
53    return avgErrorRate < 0.01 && // < 1% error rate
54           avgLoadTime < 3000 &&  // < 3s load time
55           totalBundleErrors < 5; // < 5 bundle errors
56  }
57}
58
59// A/B testing różnych strategii bundle optimization
60class BundleOptimizationABTest {
61  constructor() {
62    this.variants = {
63      control: {
64        strategy: 'current',
65        splitChunks: 'default',
66        compression: 'gzip'
67      },
68      optimized: {
69        strategy: 'aggressive_splitting',
70        splitChunks: 'custom',
71        compression: 'brotli'
72      },
73      experimental: {
74        strategy: 'module_federation',
75        splitChunks: 'micro_chunks',
76        compression: 'zstd'
77      }
78    };
79  }
80
81  async runABTest(duration = 7 * 24 * 60 * 60 * 1000) { // 7 days
82    const testId = this.generateTestId();
83
84    // Rozpocznij test
85    await this.startTest(testId);
86
87    // Monitoruj metryki
88    const results = await this.monitorTest(testId, duration);
89
90    // Analizuj wyniki
91    const analysis = await this.analyzeResults(results);
92
93    // Wybierz zwycięzcę
94    const winner = this.selectWinner(analysis);
95
96    return {
97      testId,
98      duration,
99      results,
100      analysis,
101      winner,
102      recommendation: this.generateRecommendation(analysis, winner)
103    };
104  }
105
106  async monitorTest(testId, duration) {
107    const results = {
108      control: { metrics: [], samples: 0 },
109      optimized: { metrics: [], samples: 0 },
110      experimental: { metrics: [], samples: 0 }
111    };
112
113    const startTime = Date.now();
114
115    while (Date.now() - startTime < duration) {
116      for (const variant of Object.keys(this.variants)) {
117        const metrics = await this.collectVariantMetrics(variant);
118        results[variant].metrics.push(metrics);
119        results[variant].samples++;
120      }
121
122      await new Promise(resolve => setTimeout(resolve, 3600000)); // 1 hour
123    }
124
125    return results;
126  }
127
128  selectWinner(analysis) {
129    const scores = {};
130
131    for (const variant of Object.keys(analysis)) {
132      const data = analysis[variant];
133
134      scores[variant] = (
135        (1 / data.avgLoadTime) * 0.4 +     // 40% weight
136        (1 / data.avgBundleSize) * 0.3 +   // 30% weight
137        data.conversionRate * 0.2 +        // 20% weight
138        (1 - data.errorRate) * 0.1         // 10% weight
139      );
140    }
141
142    return Object.keys(scores).reduce((a, b) =>
143      scores[a] > scores[b] ? a : b
144    );
145  }
146}

Etap blue-green to uproszczenie: klasyczne blue-green przełącza cały ruch naraz, a stopniowe zwiększanie ruchu to canary release.

Podsumowanie i lista kontrolna

Na koniec lista kontrolna i hipotetyczny rachunek zwrotu z inwestycji:

1const bundleOptimizationChecklist = {
2  analysis: [
3    'Webpack Bundle Analyzer setup',
4    'Source map analysis',
5    'Performance budget configured',
6    'CI/CD bundle size monitoring',
7    'Duplicate detection'
8  ],
9
10  codeOptimization: [
11    'Tree shaking enabled',
12    'Dead code elimination',
13    'Unused dependencies removed',
14    'Code splitting implemented',
15    'Lazy loading configured'
16  ],
17
18  bundleStrategy: [
19    'Chunk splitting optimized',
20    'Vendor chunks separated',
21    'Runtime chunk extracted',
22    'Common chunks identified',
23    'Dynamic imports utilized'
24  ],
25
26  compression: [
27    'Gzip/Brotli enabled',
28    'Minification configured',
29    'CSS optimization',
30    'Image optimization',
31    'Font optimization'
32  ],
33
34  caching: [
35    'Long-term caching setup',
36    'Content-based hashing',
37    'Cache invalidation strategy',
38    'Service worker implemented',
39    'CDN optimization'
40  ],
41
42  monitoring: [
43    'Real user monitoring',
44    'Performance metrics tracking',
45    'Regression detection',
46    'A/B testing capability',
47    'Alert system configured'
48  ]
49};
50
51// Hipotetyczne ROI (wartości ilustracyjne, nie dane z badań)
52const optimizationROI = {
53  investment: {
54    developmentTime: '2-3 weeks',
55    toolingSetup: '$5,000',
56    ongoingMaintenance: '$2,000/month'
57  },
58
59  returns: {
60    conversionRateIncrease: '+25-40%',
61    bounceRateDecrease: '-30-50%',
62    serverCostReduction: '-20-30%',
63    mobileUserRetention: '+35-60%',
64    seoRankingImprovement: '+15-25%'
65  },
66
67  businessImpact: {
68    revenueIncrease: '$50,000-150,000/year',
69    costSavings: '$20,000-40,000/year',
70    paybackPeriod: '2-4 months',
71    totalROI: '400-800%'
72  }
73};

Optymalizacja zwykle się opłaca, ale zwrot zależy od projektu, więc opieraj się na własnych pomiarach. Zacznij od Bundle Analyzera bo jedna mapa modułów zwykle wskazuje największego winowajcę. W edytorze poniżej zbudujesz dashboard parku, a w kolejnej lekcji poznasz architekturę opartą na zdarzeniach.

Pamiętaj: każdy kilobajt w bundlu to ładunek, który zwiedzający muszą przewieźć przez bramę, więc pakuj tylko to, co niezbędne.

Kod do tej lekcji: index.js
1// Dashboard i Interfejs Parku
2// Ćwiczenie: Zaimplementuj system renderowania dashboardu
3
4class ParkDashboard {
5  constructor() {
6    this.widgets = [];
7    this.data = {};
8  }
9
10  // Zaimplementuj metody:
11  // addWidget(name, renderFn) - dodaj widget
12  // updateData(key, value) - zaktualizuj dane
13  // render() - wyrenderuj wszystkie widgety
14
15  addWidget(name, renderFn) {
16    // Twój kod tutaj
17  }
18
19  updateData(key, value) {
20    // Twój kod tutaj
21  }
22
23  render() {
24    // Twój kod tutaj
25  }
26}
27
28const dashboard = new ParkDashboard();
29console.log("Dashboard gotowy do konfiguracji");

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. Co to jest tree shaking w kontekście bundlowania JavaScript?

  2. 2. Który format modułów umożliwia efektywne tree shaking?

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

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj etapy refaktoryzacji kodu do wzorca projektowego:

  • Edytor kodu

    W pliku index.js są trzy testy jednostkowe: funkcje, które dostają klasę do sprawdzenia i mają zwrócić true dla poprawnej klasy, a false dla klasy z błędem. Uzupełnij luki: ___BLANK1___ to operator porównania, który sprawdza, że getModule zwraca ten sam obiekt, który zarejestrowano, ___BLANK2___ to operator porównania, który sprawdza, że dwa id są różne, a ___BLANK3___ to liczba alertów, których oczekujemy, gdy tylko jeden z dwóch sensorów przekracza próg. Sprawdzarka uruchomi Twoje testy na poprawnych klasach ParkCore, DinosaurRegistry i SecuritySystem oraz na wersjach z błędami (kopia modułu zamiast oryginału, jedno id dla wszystkich, alarm przy odczycie równym progowi).

  • Układanie w pionie

    Ułóż elementy interfejsu w kolejności, w jakiej pojawiają się w kodzie HTML strony (od zewnętrznego kontenera):

  • Edytor kodu

    W pliku index.js jest klasa PerformanceMonitor. Uzupełnij luki: ___BLANK1___ to metoda obiektu performance, która zwraca bieżący czas w milisekundach o wysokiej precyzji (tą samą metodą measure liczy czas końca), ___BLANK2___ to metoda tablicy, która sumuje wszystkie czasy (startuje od 0), a ___BLANK3___ to metoda Math, która zwraca największy z czasów. measure(name, fn) ma zwrócić wynik fn i zapisać pomiar { name, duration }, a getReport(name) zwrócić { count, min, max, avg } dla pomiarów o tej nazwie albo null, gdy ich nie ma.

  • Klikanie w kolejności

    Ułóż kroki implementacji lazy loading komponentu w JavaScript:

  • Układanie w poziomie

    Ułóż elementy eksportu modułu w odpowiedniej kolejności:

  • Klikanie w kolejności

    Ułóż elementy wzorca Observer:

  • Układanie w pionie

    Ułóż kroki implementacji wzorca Factory w odpowiedniej kolejności:

  • Układanie w pionie

    Ułóż przepływ danych w systemie zarządzania dinozaurami:

  • Układanie w pionie

    Ułóż elementy wstrzykiwania zależności w odpowiedniej kolejności:

  • Układanie w pionie

    Ułóż etapy wdrażania code splitting w aplikacji:

  • Układanie w pionie

    Ułóż etapy implementacji wzorca Observer w aplikacji:

  • Układanie w poziomie

    Ułóż elementy zamrażania obiektu:

Przydatne artykuły