Optimizing JavaScript Performance with Webpack and Code Splitting
This guide walks you through setting up a modern JavaScript project with Webpack and implementing code splitting. You'll learn how to break your application into smaller bundles that load on demand, reducing your initial page load time. Prerequisites include Node.js (v18 or later) installed and basic familiarity with JavaScript modules and the command line.
Project Setup and Installation
First, create a new project directory and initialize it with npm. This generates a package.json file to track your dependencies and scripts.
mkdir webpack-splitting-demo
cd webpack-splitting-demo
npm init -y
Install Webpack and the Webpack CLI as development dependencies. These are the core tools for bundling your code.
npm install --save-dev webpack webpack-cli
Create the basic folder structure for your source code and configuration files. Your application code will live in src.
mkdir src
touch webpack.config.js
Your project structure should now look like this:
webpack-splitting-demo/ ├── node_modules/ ├── src/ ├── package.json └── webpack.config.js
Configuring the Base Webpack Build
Open webpack.config.js and set up a basic configuration. This tells Webpack where your application starts and where to output the bundled file.
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
Create the main entry point file, src/index.js. For now, it will just log a simple message to prove the setup works.
console.log('Application entry point loaded.');
function initApp() {
document.body.innerHTML = 'Main App Loaded
';
console.log('App initialized.');
}
// Initialize when the DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApp);
} else {
initApp();
}
Create a simple HTML file in the src directory to serve as your application's page. This file will load the Webpack bundle.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack Splitting Demo</title>
</head>
<body>
<script src="../dist/main.js"></script>
</body>
</html>
Add a build script to your package.json to run Webpack easily from the command line.
{
"name": "webpack-splitting-demo",
"version": "1.0.0",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4"
}
}
Run your first build. Webpack will process index.js and create a dist folder containing main.js.
npm run build
⚠️ Note: If you open the HTML file directly in a browser, you may see a CORS error because the script is loaded from thefile://protocol. For proper testing, use a simple HTTP server likenpx serve distor the Webpack dev server (added later).
Creating Modules for Splitting
To demonstrate splitting, create a module that contains a large, expensive feature. This simulates a complex component or library you don't need immediately.
// src/feature.js
export function renderExpensiveFeature() {
const element = document.createElement('div');
element.innerHTML = `
<h2>Heavy Feature Loaded</h2>
<p>This content and its logic were split into a separate bundle.</p>
<p>Simulated large library size.</p>
`;
console.log('Expensive feature module executed.');
return element;
}
// Simulate a large library by exporting a big array
export const largeDataArray = new Array(10000).fill(null).map((_, i) => `Data Item ${i}`);
Now, modify your main index.js to dynamically import this feature only when a button is clicked. This uses the dynamic import() syntax, which Webpack recognizes as a split point.
console.log('Application entry point loaded.');
function initApp() {
document.body.innerHTML = '<h1>Main App Loaded</h1><button id="loadFeature">Load Expensive Feature</button>';
const button = document.getElementById('loadFeature');
button.addEventListener('click', () => {
console.log('Button clicked, loading feature...');
// Dynamic import returns a Promise
import('./feature.js')
.then(module => {
const featureElement = module.renderExpensiveFeature();
document.body.appendChild(featureElement);
console.log('Feature module loaded successfully.', module.largeDataArray.length);
})
.catch(err => {
console.error('Failed to load the feature module:', err);
});
});
console.log('App initialized.');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initApp);
} else {
initApp();
}
Rebuild your project. Webpack will now generate at least two files: main.js (your entry bundle) and a separate bundle for the dynamically imported feature.
npm run build
After running the build, check your dist directory. You should see output similar to this, where the numbers are chunk IDs:
dist/ ├── main.js └── src_feature_js.js
⚠️ Note: The exact name of the split bundle (chunk) may vary. Webpack defaults to using the module ID. We'll configure more predictable names in the next step.
Optimizing the Configuration
Update your Webpack configuration to improve the output. Setting a specific chunk filename pattern makes the bundles easier to identify and allows for better caching strategies.
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
Update your HTML file to load the new named entry bundle. The [name] placeholder is replaced with the entry point name, which defaults to main.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack Splitting Demo</title>
</head>
<body>
<script src="main.bundle.js"></script>
</body>
</html>
Install and configure the Webpack Dev Server. This provides live reloading and serves your files correctly during development, avoiding the CORS issue.
npm install --save-dev webpack-dev-server
Add a dev script to package.json and update the Webpack config to use the dev server.
{
"name": "webpack-splitting-demo",
"version": "1.0.0",
"scripts": {
"build": "webpack",
"dev": "webpack serve"
},
"devDependencies": {
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
compress: true,
port: 8080,
},
};
Testing and Verification
Start the development server. It will bundle your code and host it at http://localhost:8080.
npm run dev
Open your browser to http://localhost:8080. You should immediately see "Main App Loaded" and a button. Open the browser's Developer Tools (F12) and go to the Network tab.
Initially, only main.bundle.js loads. Click the "Load Expensive Feature" button. Observe the Network tab; you will see a new request for a file like feature.chunk.js. The page will then display the feature's content without a full page reload.
Check the Console tab in Dev Tools. You should see the sequence of logs confirming the dynamic import:
Application entry point loaded. App initialized. Button clicked, loading feature... Expensive feature module executed. Feature module loaded successfully. 10000
To verify the production build, stop the dev server (Ctrl+C) and run a production build. The mode change enables built-in optimizations like minification.
npx webpack --mode=production
Inspect the dist folder. The .js files will be minified. The split bundle will still be present, confirming that code splitting works in both development and production modes.
You have successfully set up a JavaScript project with Webpack and implemented dynamic import code splitting. This technique ensures users download only the code needed for the initial page view, improving load times. Next, you could explore splitting vendor libraries, using the SplitChunksPlugin, or integrating this setup with a framework like React or Vue.