Kurs JavaScript i TypeScript · Moduł 9: Wzorce projektowe i architektura
Optymalizacja bundla - analiza i optymalizacja pakietów
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.jsonnpx 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.htmlPoró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-lodashDruga 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. Co to jest tree shaking w kontekście bundlowania JavaScript?
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: