javascript
exercises
exercises.jsā”javascript
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// ā BUNDLERS & BUILD TOOLS - EXERCISES ā
// ā Practice Configuration & Build Concepts ā
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā EXERCISE OVERVIEW ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā Exercise 1: Webpack Configuration - Create config from scratch ā
ā Exercise 2: Vite Configuration - Setup a Vite project ā
ā Exercise 3: Rollup Configuration - Configure for library build ā
ā Exercise 4: Code Splitting - Implement lazy loading ā
ā Exercise 5: Build Optimization - Analyze and optimize ā
ā ā
ā Note: These exercises create configuration objects and simulate build ā
ā concepts. In real projects, you'd write actual config files. ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
console.log('Bundlers & Build Tools - Exercises');
console.log('====================================\n');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 1: Create a Webpack Configuration
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create a complete webpack.config.js for a React application ā
ā ā
ā Requirements: ā
ā 1. Entry point: src/index.js ā
ā 2. Output: dist/bundle.[contenthash].js ā
ā 3. Handle: .js, .jsx, .css, .scss, images, fonts ā
ā 4. Setup development server on port 3000 ā
ā 5. Enable source maps ā
ā 6. Configure path aliases (@components, @utils, @styles) ā
ā 7. Split vendor chunks ā
ā 8. Use production mode optimizations ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
function createWebpackConfig(options = {}) {
const {
mode = 'production',
port = 3000,
srcDir = 'src',
outputDir = 'dist',
} = options;
// TODO: Create the webpack configuration
// Solution:
const config = {
// Mode
mode: mode,
// Entry
entry: `./${srcDir}/index.js`,
// Output
output: {
path: `/${outputDir}`, // Would use path.resolve(__dirname, outputDir)
filename:
mode === 'production' ? '[name].[contenthash:8].js' : '[name].js',
chunkFilename:
mode === 'production'
? '[name].[contenthash:8].chunk.js'
: '[name].chunk.js',
assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
clean: true,
publicPath: '/',
},
// Module rules
module: {
rules: [
// JavaScript/JSX
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
['@babel/preset-react', { runtime: 'automatic' }],
],
plugins: ['@babel/plugin-transform-runtime'],
},
},
},
// CSS
{
test: /\.css$/,
use: [
mode === 'production'
? 'MiniCssExtractPlugin.loader'
: 'style-loader',
'css-loader',
'postcss-loader',
],
},
// SCSS
{
test: /\.scss$/,
use: [
mode === 'production'
? 'MiniCssExtractPlugin.loader'
: 'style-loader',
{
loader: 'css-loader',
options: { modules: true },
},
'postcss-loader',
'sass-loader',
],
},
// Images
{
test: /\.(png|jpg|jpeg|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 8kb - inline if smaller
},
},
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[contenthash:8][ext]',
},
},
],
},
// Resolve
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': `/${srcDir}`,
'@components': `/${srcDir}/components`,
'@utils': `/${srcDir}/utils`,
'@styles': `/${srcDir}/styles`,
'@hooks': `/${srcDir}/hooks`,
'@assets': `/${srcDir}/assets`,
},
},
// Plugins
plugins: [
// 'HtmlWebpackPlugin',
// 'MiniCssExtractPlugin',
// 'DefinePlugin',
// 'CopyPlugin'
],
// Dev Server
devServer: {
static: `./${outputDir}`,
port: port,
hot: true,
open: true,
historyApiFallback: true, // For SPA routing
compress: true,
client: {
overlay: true,
progress: true,
},
},
// Optimization
optimization: {
minimize: mode === 'production',
minimizer: [
'...', // Use defaults
'TerserPlugin',
'CssMinimizerPlugin',
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 20,
},
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 30,
},
common: {
minChunks: 2,
priority: 10,
reuseExistingChunk: true,
},
},
},
runtimeChunk: 'single',
},
// Source maps
devtool: mode === 'production' ? 'source-map' : 'eval-source-map',
// Performance hints
performance: {
hints: mode === 'production' ? 'warning' : false,
maxEntrypointSize: 250000,
maxAssetSize: 250000,
},
};
return config;
}
// Test
console.log('Exercise 1 - Webpack Configuration:');
const webpackConfig = createWebpackConfig({ mode: 'production', port: 3000 });
console.log(' Mode:', webpackConfig.mode);
console.log(' Entry:', webpackConfig.entry);
console.log(' Output:', webpackConfig.output.filename);
console.log(' Aliases:', Object.keys(webpackConfig.resolve.alias).join(', '));
console.log(' Dev server port:', webpackConfig.devServer.port);
console.log(
' Chunk splitting:',
webpackConfig.optimization.splitChunks ? 'enabled' : 'disabled'
);
console.log('');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 2: Create a Vite Configuration
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create a vite.config.js for a Vue or React application ā
ā ā
ā Requirements: ā
ā 1. Setup for React (or Vue) ā
ā 2. Configure development server with proxy ā
ā 3. Setup path aliases ā
ā 4. Configure build output ā
ā 5. Add SCSS support ā
ā 6. Configure environment variables ā
ā 7. Enable gzip compression for production ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
function createViteConfig(options = {}) {
const {
framework = 'react',
port = 5173,
apiProxy = 'http://localhost:8080',
outDir = 'dist',
} = options;
// TODO: Create the Vite configuration
// Solution:
const config = {
// Plugins based on framework
plugins: [
framework === 'react' ? '@vitejs/plugin-react' : null,
framework === 'vue' ? '@vitejs/plugin-vue' : null,
// 'vite-plugin-compression' for gzip
].filter(Boolean),
// Base path
base: '/',
// Server configuration
server: {
port: port,
open: true,
cors: true,
// Proxy API requests
proxy: {
'/api': {
target: apiProxy,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
secure: false,
},
'/ws': {
target: apiProxy.replace('http', 'ws'),
ws: true,
},
},
// HMR configuration
hmr: {
overlay: true,
},
},
// Build configuration
build: {
outDir: outDir,
sourcemap: true,
minify: 'esbuild',
target: 'es2020',
// Chunk size warning limit
chunkSizeWarningLimit: 500,
// Rollup options
rollupOptions: {
output: {
// Manual chunk splitting
manualChunks: {
vendor: ['react', 'react-dom'],
// or for Vue: ['vue', 'vue-router']
},
// Asset file names
assetFileNames: 'assets/[name]-[hash][extname]',
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
},
},
// CSS code splitting
cssCodeSplit: true,
},
// CSS configuration
css: {
// CSS modules
modules: {
localsConvention: 'camelCase',
},
// Preprocessor options
preprocessorOptions: {
scss: {
// Global imports
additionalData: `
@import "@/styles/variables.scss";
@import "@/styles/mixins.scss";
`,
},
},
// PostCSS config
postcss: {
plugins: ['autoprefixer'],
},
},
// Resolve configuration
resolve: {
alias: {
'@': '/src',
'@components': '/src/components',
'@utils': '/src/utils',
'@styles': '/src/styles',
'@hooks': '/src/hooks',
'@store': '/src/store',
},
},
// Environment configuration
envPrefix: 'VITE_',
envDir: './',
// Dependency optimization
optimizeDeps: {
include: ['lodash-es', 'axios'],
exclude: [],
},
// Preview server (for production preview)
preview: {
port: 4173,
open: true,
},
// Define global constants
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
},
};
return config;
}
// Test
console.log('Exercise 2 - Vite Configuration:');
const viteConfig = createViteConfig({ framework: 'react', port: 5173 });
console.log(' Framework:', viteConfig.plugins[0] ? 'React' : 'Unknown');
console.log(' Server port:', viteConfig.server.port);
console.log(' API proxy:', viteConfig.server.proxy['/api'].target);
console.log(' Output dir:', viteConfig.build.outDir);
console.log(' Minifier:', viteConfig.build.minify);
console.log(' Env prefix:', viteConfig.envPrefix);
console.log('');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 3: Create a Rollup Configuration for Library
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create a rollup.config.js for building a JavaScript library ā
ā ā
ā Requirements: ā
ā 1. Multiple output formats: ESM, CJS, UMD ā
ā 2. TypeScript support ā
ā 3. External dependencies (peer dependencies) ā
ā 4. Minified production build ā
ā 5. Source maps ā
ā 6. Banner with license info ā
ā 7. Preserve modules option for ESM ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
function createRollupConfig(options = {}) {
const {
name = 'MyLibrary',
input = 'src/index.ts',
external = [],
globals = {},
} = options;
// TODO: Create the Rollup configuration
// Solution:
const banner = `/*!
* ${name} v1.0.0
* (c) ${new Date().getFullYear()}
* Released under the MIT License
*/`;
const config = {
// Input
input: input,
// External dependencies (don't bundle these)
external: [
...external,
'react',
'react-dom',
/^lodash/, // Regex to match lodash/*
],
// Multiple outputs
output: [
// ESM format (modern bundlers, tree-shaking)
{
file: `dist/${name.toLowerCase()}.esm.js`,
format: 'esm',
sourcemap: true,
banner: banner,
exports: 'named',
},
// CommonJS format (Node.js)
{
file: `dist/${name.toLowerCase()}.cjs.js`,
format: 'cjs',
sourcemap: true,
banner: banner,
exports: 'named',
},
// UMD format (browsers + AMD + CommonJS)
{
file: `dist/${name.toLowerCase()}.umd.js`,
format: 'umd',
name: name,
sourcemap: true,
banner: banner,
globals: {
react: 'React',
'react-dom': 'ReactDOM',
...globals,
},
},
// Minified UMD for production
{
file: `dist/${name.toLowerCase()}.umd.min.js`,
format: 'umd',
name: name,
sourcemap: true,
banner: banner,
globals: {
react: 'React',
'react-dom': 'ReactDOM',
...globals,
},
plugins: ['@rollup/plugin-terser'],
},
],
// Plugins
plugins: [
// Resolve node_modules
'@rollup/plugin-node-resolve',
// Convert CommonJS modules to ES6
'@rollup/plugin-commonjs',
// TypeScript support
'@rollup/plugin-typescript',
// Babel for additional transforms
{
name: '@rollup/plugin-babel',
options: {
babelHelpers: 'bundled',
exclude: 'node_modules/**',
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
// JSON support
'@rollup/plugin-json',
// Replace environment variables
{
name: '@rollup/plugin-replace',
options: {
'process.env.NODE_ENV': JSON.stringify('production'),
preventAssignment: true,
},
},
],
// Watch options for development
watch: {
include: 'src/**',
exclude: 'node_modules/**',
},
// Preserve modules for ESM (better tree-shaking in consumers)
// For separate module files instead of single bundle:
preserveModulesRoot: 'src',
};
// Alternative: Preserve modules configuration
const preserveModulesConfig = {
...config,
output: {
dir: 'dist/esm',
format: 'esm',
sourcemap: true,
preserveModules: true,
preserveModulesRoot: 'src',
entryFileNames: '[name].js',
},
};
return { main: config, preserveModules: preserveModulesConfig };
}
// Test
console.log('Exercise 3 - Rollup Library Configuration:');
const rollupConfig = createRollupConfig({ name: 'AwesomeLib' });
console.log(' Input:', rollupConfig.main.input);
console.log(
' Output formats:',
rollupConfig.main.output.map((o) => o.format).join(', ')
);
console.log(
' External deps:',
rollupConfig.main.external.filter((e) => typeof e === 'string').join(', ')
);
console.log(' Plugins:', rollupConfig.main.plugins.length);
console.log('');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 4: Implement Code Splitting
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Implement code splitting strategies ā
ā ā
ā Requirements: ā
ā 1. Route-based code splitting ā
ā 2. Component-based lazy loading ā
ā 3. Library/vendor splitting ā
ā 4. Conditional feature loading ā
ā 5. Preloading critical chunks ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
// Simulate dynamic import behavior
function createLazyLoader(importFn, options = {}) {
const {
preload = false,
timeout = 10000,
fallback = null,
onError = null,
} = options;
let modulePromise = null;
let loadedModule = null;
return {
// Preload the module
preload() {
if (!modulePromise) {
modulePromise = importFn();
modulePromise
.then((mod) => {
loadedModule = mod;
})
.catch((err) => {
modulePromise = null;
if (onError) onError(err);
});
}
return modulePromise;
},
// Load and return the module
async load() {
if (loadedModule) return loadedModule;
if (!modulePromise) {
modulePromise = importFn();
}
// Add timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Module load timeout')), timeout);
});
try {
loadedModule = await Promise.race([modulePromise, timeoutPromise]);
return loadedModule;
} catch (error) {
modulePromise = null;
if (onError) onError(error);
throw error;
}
},
// Check if loaded
isLoaded() {
return loadedModule !== null;
},
};
}
// 1. Route-based code splitting
const routeModules = {
home: createLazyLoader(
() => Promise.resolve({ default: { name: 'HomePage' } }),
{ preload: true }
),
dashboard: createLazyLoader(() =>
Promise.resolve({ default: { name: 'DashboardPage' } })
),
settings: createLazyLoader(() =>
Promise.resolve({ default: { name: 'SettingsPage' } })
),
profile: createLazyLoader(() =>
Promise.resolve({ default: { name: 'ProfilePage' } })
),
};
async function loadRoute(routeName) {
const loader = routeModules[routeName];
if (!loader) {
throw new Error(`Unknown route: ${routeName}`);
}
const module = await loader.load();
return module.default;
}
// 2. Component-based lazy loading
function createLazyComponent(importFn) {
let Component = null;
let loading = false;
let error = null;
return {
// Render function (simplified)
render(props) {
if (Component) {
return { type: Component, props };
}
if (loading) {
return { type: 'Loading', props: {} };
}
if (error) {
return { type: 'Error', props: { error } };
}
loading = true;
importFn()
.then((mod) => {
Component = mod.default;
loading = false;
})
.catch((err) => {
error = err;
loading = false;
});
return { type: 'Loading', props: {} };
},
// Preload the component
preload() {
return importFn();
},
};
}
// 3. Feature-based conditional loading
async function loadFeatureIfEnabled(featureName, features = {}) {
if (!features[featureName]) {
console.log(` Feature "${featureName}" is disabled`);
return null;
}
const featureModules = {
analytics: () =>
Promise.resolve({
init: () => console.log(' Analytics initialized'),
track: (event) => console.log(` Tracked: ${event}`),
}),
chat: () =>
Promise.resolve({
init: () => console.log(' Chat widget initialized'),
open: () => console.log(' Chat opened'),
}),
notifications: () =>
Promise.resolve({
init: () => console.log(' Notifications initialized'),
show: (msg) => console.log(` Notification: ${msg}`),
}),
};
const loader = featureModules[featureName];
if (!loader) {
throw new Error(`Unknown feature: ${featureName}`);
}
console.log(` Loading feature: ${featureName}`);
return loader();
}
// 4. Preloading strategy
class PreloadManager {
constructor() {
this.preloaded = new Set();
this.queue = [];
}
// Add to preload queue
enqueue(name, importFn, priority = 0) {
this.queue.push({ name, importFn, priority });
this.queue.sort((a, b) => b.priority - a.priority);
}
// Preload on idle
async preloadOnIdle() {
// In browser: requestIdleCallback
// Simulating with setTimeout
while (this.queue.length > 0) {
const { name, importFn } = this.queue.shift();
if (this.preloaded.has(name)) continue;
try {
await importFn();
this.preloaded.add(name);
console.log(` Preloaded: ${name}`);
} catch (error) {
console.log(` Failed to preload: ${name}`);
}
// Yield to main thread
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
// Preload on link hover (for routes)
onLinkHover(routeName) {
const loader = routeModules[routeName];
if (loader && !loader.isLoaded()) {
loader.preload();
}
}
}
// Test
console.log('Exercise 4 - Code Splitting:');
(async () => {
// Route-based loading
console.log(' Route-based loading:');
const homePage = await loadRoute('home');
console.log(` Loaded: ${homePage.name}`);
// Feature loading
console.log(' Feature loading:');
const features = { analytics: true, chat: false };
await loadFeatureIfEnabled('analytics', features);
await loadFeatureIfEnabled('chat', features);
// Preload manager
console.log(' Preload manager:');
const preloader = new PreloadManager();
preloader.enqueue('dashboard', () => Promise.resolve(), 1);
preloader.enqueue('settings', () => Promise.resolve(), 0);
await preloader.preloadOnIdle();
console.log('');
})();
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 5: Build Optimization Analysis
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create a build analyzer and optimization checker ā
ā ā
ā Requirements: ā
ā 1. Analyze bundle composition ā
ā 2. Detect optimization opportunities ā
ā 3. Check for common issues (duplicate deps, large chunks) ā
ā 4. Suggest improvements ā
ā 5. Generate a report ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
class BuildAnalyzer {
constructor() {
this.chunks = [];
this.issues = [];
this.suggestions = [];
}
// Add a chunk for analysis
addChunk(chunk) {
this.chunks.push({
name: chunk.name,
size: chunk.size,
modules: chunk.modules || [],
type: chunk.type || 'chunk',
});
}
// Analyze chunks for issues
analyze() {
this.issues = [];
this.suggestions = [];
// Check total bundle size
const totalSize = this.chunks.reduce((sum, c) => sum + c.size, 0);
if (totalSize > 500 * 1024) {
// 500KB
this.issues.push({
type: 'size',
severity: 'warning',
message: `Total bundle size (${this.formatSize(
totalSize
)}) exceeds recommended limit (500KB)`,
});
this.suggestions.push('Consider code splitting or lazy loading');
}
// Check individual chunk sizes
for (const chunk of this.chunks) {
if (chunk.size > 244 * 1024) {
// 244KB
this.issues.push({
type: 'chunk-size',
severity: 'warning',
message: `Chunk "${chunk.name}" (${this.formatSize(
chunk.size
)}) is too large`,
});
this.suggestions.push(`Split "${chunk.name}" into smaller chunks`);
}
}
// Check for duplicate modules
const allModules = this.chunks.flatMap((c) => c.modules);
const moduleCounts = {};
for (const mod of allModules) {
moduleCounts[mod] = (moduleCounts[mod] || 0) + 1;
}
for (const [mod, count] of Object.entries(moduleCounts)) {
if (count > 1) {
this.issues.push({
type: 'duplicate',
severity: 'error',
message: `Module "${mod}" appears in ${count} chunks`,
});
this.suggestions.push(`Extract "${mod}" to a shared chunk`);
}
}
// Check for vendor splitting
const hasVendorChunk = this.chunks.some((c) => c.name.includes('vendor'));
if (!hasVendorChunk && this.chunks.length > 1) {
this.issues.push({
type: 'optimization',
severity: 'info',
message: 'No vendor chunk detected',
});
this.suggestions.push(
'Split node_modules into a separate vendor chunk for better caching'
);
}
return this.generateReport();
}
// Check for common problematic dependencies
checkDependencies(dependencies) {
const heavyDeps = {
moment: { size: 232, alternative: 'dayjs or date-fns' },
lodash: { size: 72, alternative: 'lodash-es with tree-shaking' },
axios: { size: 14, alternative: 'native fetch or ky' },
};
for (const dep of dependencies) {
if (heavyDeps[dep]) {
const info = heavyDeps[dep];
this.suggestions.push(
`Consider replacing "${dep}" (~${info.size}KB) with ${info.alternative}`
);
}
}
}
// Format file size
formatSize(bytes) {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
}
// Generate optimization report
generateReport() {
const totalSize = this.chunks.reduce((sum, c) => sum + c.size, 0);
return {
summary: {
totalChunks: this.chunks.length,
totalSize: this.formatSize(totalSize),
totalSizeBytes: totalSize,
issueCount: this.issues.length,
suggestionCount: this.suggestions.length,
},
chunks: this.chunks.map((c) => ({
name: c.name,
size: this.formatSize(c.size),
percentage: ((c.size / totalSize) * 100).toFixed(1) + '%',
})),
issues: this.issues,
suggestions: [...new Set(this.suggestions)], // Unique suggestions
score: this.calculateScore(),
};
}
// Calculate optimization score
calculateScore() {
let score = 100;
for (const issue of this.issues) {
if (issue.severity === 'error') score -= 15;
if (issue.severity === 'warning') score -= 10;
if (issue.severity === 'info') score -= 5;
}
return Math.max(0, score);
}
}
// Test
console.log('Exercise 5 - Build Analysis:');
setTimeout(() => {
const analyzer = new BuildAnalyzer();
// Simulate bundle chunks
analyzer.addChunk({
name: 'main',
size: 180 * 1024,
modules: ['app', 'utils'],
});
analyzer.addChunk({
name: 'vendor',
size: 320 * 1024,
modules: ['react', 'lodash'],
});
analyzer.addChunk({
name: 'dashboard',
size: 95 * 1024,
modules: ['chart', 'utils'],
}); // utils duplicate!
analyzer.addChunk({ name: 'settings', size: 45 * 1024, modules: ['forms'] });
// Check dependencies
analyzer.checkDependencies(['react', 'moment', 'lodash', 'axios']);
// Generate report
const report = analyzer.analyze();
console.log(' Bundle Analysis Report:');
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāā');
console.log(` Total Size: ${report.summary.totalSize}`);
console.log(` Chunks: ${report.summary.totalChunks}`);
console.log(` Score: ${report.score}/100`);
console.log('');
console.log(' Chunk Breakdown:');
report.chunks.forEach((c) => {
console.log(` ${c.name}: ${c.size} (${c.percentage})`);
});
console.log('');
console.log(' Issues:');
report.issues.forEach((i) => {
console.log(` [${i.severity.toUpperCase()}] ${i.message}`);
});
console.log('');
console.log(' Suggestions:');
report.suggestions.forEach((s, i) => {
console.log(` ${i + 1}. ${s}`);
});
console.log('');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// BONUS: Create package.json scripts
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
console.log('Bonus - Package.json Scripts Generator:');
function generateScripts(bundler, options = {}) {
const scripts = {};
switch (bundler) {
case 'webpack':
scripts.dev = 'webpack serve --mode development';
scripts.build = 'webpack --mode production';
scripts['build:dev'] = 'webpack --mode development';
scripts.analyze =
'webpack --profile --json > stats.json && webpack-bundle-analyzer stats.json';
break;
case 'vite':
scripts.dev = 'vite';
scripts.build = 'vite build';
scripts.preview = 'vite preview';
scripts['build:analyze'] = 'vite build --mode analyze';
break;
case 'rollup':
scripts.dev = 'rollup -c -w';
scripts.build = 'rollup -c';
scripts['build:prod'] = 'NODE_ENV=production rollup -c';
break;
case 'esbuild':
scripts.dev = 'esbuild src/index.js --bundle --watch --servedir=public';
scripts.build =
'esbuild src/index.js --bundle --minify --outfile=dist/bundle.js';
break;
}
// Common scripts
scripts.lint = 'eslint src/**/*.{js,jsx,ts,tsx}';
scripts['lint:fix'] = 'eslint src/**/*.{js,jsx,ts,tsx} --fix';
scripts.test = 'vitest';
scripts['test:coverage'] = 'vitest --coverage';
scripts.typecheck = 'tsc --noEmit';
scripts.format = 'prettier --write src/**/*.{js,jsx,ts,tsx,css,scss}';
return scripts;
}
const viteScripts = generateScripts('vite');
console.log(' Generated Vite scripts:');
Object.entries(viteScripts).forEach(([name, cmd]) => {
console.log(` "${name}": "${cmd}"`);
});
console.log(`
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā BUNDLERS & BUILD TOOLS - EXERCISES COMPLETE ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£
ā ā
ā Completed Exercises: ā
ā ā Exercise 1: Webpack Configuration ā
ā ā Exercise 2: Vite Configuration ā
ā ā Exercise 3: Rollup Library Build ā
ā ā Exercise 4: Code Splitting Strategies ā
ā ā Exercise 5: Build Optimization Analysis ā
ā ā Bonus: Package.json Scripts Generator ā
ā ā
ā Key Takeaways: ā
ā ⢠Choose bundler based on use case (app vs library) ā
ā ⢠Configure loaders/plugins for asset handling ā
ā ⢠Implement code splitting for better performance ā
ā ⢠Analyze and optimize bundle size regularly ā
ā ⢠Use content hashes for cache busting ā
ā ⢠Split vendor code for better caching ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
`);
}, 100); // Delay to let async code finish