JavaScript and TypeScript course Β· Module 9: Design Patterns and Architecture
Bundle optimization - analysis and package optimization
In this lesson9
Imagine a truck that delivers one bag of feed to the park while dragging a whole warehouse behind it, because nobody checked the cargo. Bundle optimization is a comprehensive process of analyzing and optimizing JavaScript packages: a smaller size, faster loading and efficient use of browser resources. It is the last step after code splitting, tree shaking and lazy loading.
Why does it matter?
The numbers below are a hypothetical example, not research results, but they show what we measure:
1// Hypothetical example: illustrative numbers, not measurements
2
3// Performance metrics before optimization
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// Performance metrics after optimization
13const afterOptimization = {
14 bundleSize: '420KB',
15 firstContentfulPaint: '0.9s',
16 largestContentfulPaint: '1.6s',
17 timeToInteractive: '2.1s',
18 cumulativeLayoutShift: 0.05
19};
20
21// Performance improvement
22const improvement = {
23 bundleSizeReduction: '85%',
24 fcpImprovement: '73%',
25 lcpImprovement: '69%',
26 ttiImprovement: '66%',
27 clsImprovement: '80%'
28};
29
30// Business impact cost analysis
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%', // Due to slow loading
44 after: '12%',
45 improvement: '-66%'
46 },
47 revenueImpact: {
48 monthlyLoss: '$24,000', // Due to slow bundles
49 monthlyGain: '$45,000' // After optimization
50 }
51};Of these metrics, LCP and CLS belong to the Core Web Vitals (the third one is INP), FCP is a supporting metric, and since Lighthouse 10 TTI no longer counts toward the score. Always measure your own numbers.
Bundle analysis tools
You introduce code splitting in four steps: analyzing the bundle size, choosing split points, configuring dynamic imports and verifying that chunks load as planned.
Webpack Bundle Analyzer
Webpack Bundle Analyzer draws a map of modules and shows which ones take up the most space:
1# Installation and usage
2npm install --save-dev webpack-bundle-analyzer
3
4# Generate report (the analyzer reads the webpack stats file)
5npx webpack --profile --json=stats.json
6npx webpack-bundle-analyzer stats.json
7
8# Integration with 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};The analyzer reads the webpack stats file (stats.json), not the finished .js files.
Analysis in CI/CD
A GitHub Actions workflow checks the size on every push and pull request:
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 (with the size-limit and @size-limit/file packages in devDependencies) fails the build when a file exceeds its limit from package.json.
Source Map Explorer
Source Map Explorer shows which source files make up the finished bundle:
1# Source map analysis
2npm install --global source-map-explorer
3
4# Analyze specific bundle
5source-map-explorer bundle.js bundle.js.map
6
7# HTML report (keep it to compare with the next version)
8source-map-explorer bundle.js --html report.htmlThere is no built-in version comparison, so keep the HTML reports between releases.
Webpack Dashboard
Webpack Dashboard turns the raw webpack log into a readable panel in the terminal:
1// Interactive dashboard during development
2const DashboardPlugin = require('webpack-dashboard/plugin');
3
4module.exports = {
5 plugins: [
6 new DashboardPlugin()
7 ]
8};The panel appears when you run webpack through the package's CLI, e.g. webpack-dashboard -- webpack --watch. Then you see the module sizes with every change.
Advanced optimization techniques
Dynamic chunking strategies
splitChunks divides node_modules into groups (cache groups) that the browser caches independently of the application code:
1// webpack.config.js - Advanced chunk configuration
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 in separate chunks
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};A higher priority wins when groups conflict, and enforce: true ignores the size thresholds. After a change in your own code, the user does not download React again.
Module Federation for micro-frontends
Module Federation, built into webpack 5, lets independent applications share code at runtime:
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 guarantees a single copy of React, and the two module.exports are two separate configuration files.
Incremental bundling
A disk cache speeds up later builds, because webpack rebuilds only what has changed:
1// Webpack cache configuration for faster rebuilds
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 for 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 lists the files whose change invalidates the cache.
Budgets and monitoring
Budget configuration
A performance budget is a size limit that the size-limit tool reads from 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}The limits apply to the gzipped size. Remove the first line, because JSON has no comments.
Automated performance monitoring
The monitor collects loading times from the Performance API for real users and compares them with thresholds:
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 // Check bundle sizes (transferSize from 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 // Analyze duplicates in bundles
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 // Analyze unused dependencies
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// Automatic monitoring in production
100if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
101 const monitor = new BundlePerformanceMonitor();
102
103 // Measure after page load
104 window.addEventListener('load', () => {
105 setTimeout(() => {
106 monitor.measureBundlePerformance();
107 }, 2000);
108 });
109}performance.memory exists only in Chromium, hence the check before using it. You write the helper methods (sendToMonitoring, findDuplicatedModules, findUnusedDependencies and the savings calculations) yourself.
Real User Monitoring (RUM)
PerformanceObserver reports every downloaded file and every long task:
1// RUM for 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 for 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 // Send to analytics system
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}A long task blocks the main thread for at least 50 ms, and sendBeacon works even while the tab is closing.
Library-specific optimization
Lodash optimization
Here tree shaking comes back: the automatic removal of unused code (dead code) from the final bundle. It works on ES modules, because import and export are static, unlike require in CommonJS. The "sideEffects": false field in package.json tells the bundler that the package modules have no side effects, so it can skip the unused ones. Classic Lodash is CommonJS:
1# Build a modular ES-module version of Lodash (this is how lodash-es is generated)
2npx lodash-cli modularize exports=es
3
4# Babel plugin for automatic optimization
5npm install --save-dev babel-plugin-lodashThe second part is a Babel plugin that turns an import of the whole library into imports of single functions:
1// babel.config.js
2module.exports = {
3 plugins: [
4 'lodash' // Automatically converts imports to cherry-picked
5 ]
6};
7
8// Before optimization (70KB)
9import _ from 'lodash';
10
11// After optimization (2KB)
12import debounce from 'lodash/debounce';
13import throttle from 'lodash/throttle';Today it is simpler to reach for lodash-es. For the same reason, the Module pattern with an IIFE, although it hides private variables, cannot be trimmed, because it is a single object.
Date library optimization
Moment.js does not support tree shaking, and its authors themselves recommend newer alternatives:
1// Date library size comparison (approximate and version-dependent; check current numbers e.g. on 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 from Moment.js to 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};The sizes are approximate. Watch the tokens: date-fns uses dd and yyyy, while Moment uses DD and YYYY.
CSS framework optimization
PurgeCSS scans the source files and removes the CSS selectors it did not find in them:
1// PurgeCSS for removing unused styles
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// Example size reductions (illustrative values, they depend on the project)
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 protects classes that JavaScript adds dynamically.
Advanced webpack optimization
Custom webpack plugins
A custom plugin is a class with an apply method that hooks into the compiler:
1// Plugin for bundle analysis and optimization
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 // Save report
19 compilation.assets[this.options.reportPath] = {
20 source: () => JSON.stringify(report, null, 2),
21 size: () => JSON.stringify(report, null, 2).length
22 };
23
24 // Log warnings
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 // Find large modules
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 // Find duplicates
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// Plugin usage
88module.exports = {
89 plugins: [
90 new BundleOptimizationPlugin({
91 thresholds: {
92 maxChunkSize: 200000,
93 maxModuleSize: 30000
94 }
95 })
96 ]
97};This is a sketch in the webpack 4 API, and you write the helper methods (calculateTotalSize, findModuleDuplicates, generateRecommendations, logWarnings) yourself. In webpack 5 you add assets in the processAssets hook with compilation.emitAsset, get a chunk's size from compilation.chunkGraph.getChunkSize(chunk), and use compilation.moduleGraph.getIncomingConnections(module) instead of the removed module.reasons.
Runtime chunk optimization
The runtime is the webpack code that loads chunks, and once extracted it does not invalidate the application cache. Next to it, JS and CSS minification:
1// Runtime chunk optimization
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 works only in production, and pure_funcs lists functions whose calls can be removed.
Bundle tests
Automated bundle testing
It is easy to break an optimization with a single import, so tests for size, duplicates and tree shaking guard it:
1// Test suite for bundle optimization
2const bundleTests = {
3 // Bundle size test
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 // Duplicate test
14 async testNoDuplicateModules() {
15 const bundleAnalysis = await analyzeBundleContent();
16 const duplicates = bundleAnalysis.duplicatedModules;
17
18 expect(duplicates).toHaveLength(0);
19 },
20
21 // Tree shaking effectiveness test
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 // Cache effectiveness test
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// CI/CD integration
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});You write the helper functions, such as getBundleStats, yourself, based on stats.json.
Performance regression detection
The regression detector compares the current metrics with a saved baseline:
1// System for detecting performance regressions
2class PerformanceRegressionDetector {
3 constructor() {
4 this.baselineMetrics = null;
5 this.threshold = 0.1; // 10% increase = regression
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 // Check bundle sizes
22 const sizeRegression = this.detectSizeRegression(
23 this.baselineMetrics.bundleSize,
24 currentMetrics.bundleSize
25 );
26
27 if (sizeRegression) {
28 regressions.push(sizeRegression);
29 }
30
31 // Check load times
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}An increase above 10% counts as a regression, and above 25% as a high-priority regression.
Production deployment strategies
You can release a new bundle gradually and compare variants on real traffic:
1// Progressive deployment strategy for bundles
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 different bundle optimization strategies
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 // Start test
85 await this.startTest(testId);
86
87 // Monitor metrics
88 const results = await this.monitorTest(testId, duration);
89
90 // Analyze results
91 const analysis = await this.analyzeResults(results);
92
93 // Select winner
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}The blue-green stage is a simplification: classic blue-green switches all traffic at once, while gradually increasing the traffic share is a canary release.
Summary and checklist
Finally, a checklist and a hypothetical return on investment calculation:
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// Hypothetical ROI (illustrative values, not research data)
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};Optimization usually pays off, but the return depends on the project, so base your decisions on your own measurements. Start with the Bundle Analyzer because a single module map usually points to the biggest culprit. In the editor below you will build the park dashboard, and in the next lesson you will learn about event-driven architecture.
Remember: every kilobyte in the bundle is cargo that visitors have to carry through the gate, so pack only what is essential.
Code for this lesson: index.js
1// Park Dashboard and Interface
2// Exercise: Implement a dashboard rendering system
3
4class ParkDashboard {
5 constructor() {
6 this.widgets = [];
7 this.data = {};
8 }
9
10 // Implement the methods:
11 // addWidget(name, renderFn) - add a widget
12 // updateData(key, value) - update data
13 // render() - render all widgets
14
15 addWidget(name, renderFn) {
16 // Your code here
17 }
18
19 updateData(key, value) {
20 // Your code here
21 }
22
23 render() {
24 // Your code here
25 }
26}
27
28const dashboard = new ParkDashboard();
29console.log("Dashboard ready for configuration");Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is tree shaking in the context of JavaScript bundling?
2. Which module format enables effective tree shaking?
These are 2 of 6 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Vertical ordering
Arrange the stages of refactoring code to a design pattern:
- Code editor
The index.js file has three unit tests: functions that receive the class to check and should return true for a correct class and false for a class with a bug. Fill in the blanks: ___BLANK1___ is the comparison operator that checks that getModule returns the same object that was registered, ___BLANK2___ is the comparison operator that checks that two ids are different, and ___BLANK3___ is the number of alerts we expect when only one of two sensors exceeds its threshold. The checker runs your tests on correct ParkCore, DinosaurRegistry and SecuritySystem classes and on versions with bugs (a copy of the module instead of the original, one id for everybody, an alarm at a reading equal to the threshold).
- Vertical ordering
Arrange the user interface elements in the order in which they appear in the HTML code of the page (starting with the outer container):
- Code editor
The index.js file has a PerformanceMonitor class. Fill in the blanks: ___BLANK1___ is the method of the performance object that returns the current time in milliseconds with high precision (measure uses the same method for the end time), ___BLANK2___ is the array method that adds up all durations (it starts from 0), and ___BLANK3___ is the Math method that returns the largest duration. measure(name, fn) should return the result of fn and store the measurement { name, duration }, and getReport(name) should return { count, min, max, avg } for the measurements with this name, or null when there are none.
- Click in order
Arrange the steps for implementing lazy loading of a component in JavaScript:
- Horizontal ordering
Arrange the elements of a module export in the correct order:
- Click in order
Arrange the elements of the Observer pattern:
- Vertical ordering
Arrange the steps of Factory pattern implementation in the correct order:
- Vertical ordering
Arrange the data flow in the dinosaur management system:
- Vertical ordering
Arrange the elements of dependency injection in the correct order:
- Vertical ordering
Arrange the stages of implementing code splitting in an application:
- Vertical ordering
Arrange the stages of Observer pattern implementation in an application:
- Horizontal ordering
Arrange the elements of freezing an object: