Code splitting is a technique that allows us to split our JavaScript code into smaller chunks, which can be loaded on demand when needed. This helps optimize the initial load time of our application by reducing the amount of code that needs to be downloaded and parsed upfront.
Consider a web application with various pages, such as a dashboard, user profile, settings page etc. Each of these pages requires a significant amount of JavaScript code to function properly. Without code splitting, the entire application code would need to be loaded upfront, resulting in a slow initial loading time and increased resource consumption.
With code splitting, we can dynamically load only the code that is needed for each specific page. For example, when a user navigates to the dashboard page, only the JavaScript code necessary for that page is fetched and executed. Similarly, when the user moves to the user profile or settings page, the corresponding code chunks are loaded on-demand.
Code Splitting explained via cartoon
Webpack provides built-in support for code splitting through its dynamic import syntax and a feature called splitChunks. Here's an overview of how we can use code splitting in webpack:
To split our code, we can use the dynamic import syntax, which returns a Promise that resolves to the module we want to load. Here's an example:
import './index.scss'
const form = document.getElementById('add-form') as HTMLFormElement
const output = document.getElementById('result') as HTMLOutputElement
form.addEventListener('submit', async (e) => {
e.preventDefault()
const first = +(e.target as HTMLFormElement)['first'].value
const second = +(e.target as HTMLFormElement)['second'].value
const { sum } = await import('./utils')
const result = sum([first, second])
output.innerText = `Total = ${result}`
})
export function multiply(nums: number[]) {
const result = nums.reduce((acc, num) => acc * num, 1)
return result
}
export function sum(nums: number[]) {
const result = nums.reduce((acc, num) => acc + num, 0)
return result
}
In our codebase, we've organized our utility functions into a separate utils.ts
file. One of these utility functions, let's say the sum
function, is dynamically loaded only when the form is submitted.
Next let’s update our module property in tsconfig.json
from es6
to ESNext
. ES6 modules, as they stand, have no way to support import()
.
{
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"skipLibCheck": true,
"strict": true,
"target": "es6"
}
}
Webpack's default behavior does not include searching for .ts
files during module resolution. To enable resolution of TypeScript files, we need to modify the resolve.extensions
configuration. It is crucial to include the default values, such as .js
, alongside .ts
to maintain compatibility with modules that expect automatic resolution of .js
files.
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const path = require('path')
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
{
test: /\\.ts$/,
use: 'ts-loader',
},
],
},
optimization: {
minimizer: [new CssMinimizerPlugin(), new TerserPlugin()],
},
output: {
filename: 'main.js',
path: path.resolve(__dirname, '../dist'),
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
}),
new MiniCssExtractPlugin({
filename: 'index.css',
}),
new PurgeCSSPlugin({
paths: ['./src/index.html'],
}),
],
resolve: {
extensions: ['.ts', '.js'],
},
}
After running the build process with the updated Webpack configuration, inspect the dist
directory and observe that Webpack has generated two JavaScript files: the main bundle and a dynamically imported bundle.
main.js
or bundle.js
, contains the code from your entry point file(s) (e.g., index.js
or main.ts
) and any statically imported modules. It represents the initial bundle that is loaded when the application starts.1a2b3c.js
.By splitting the code into separate bundles, Webpack enables more efficient loading of your application. The main bundle is loaded initially, and the dynamically imported bundle is fetched and loaded only when the corresponding dynamic import is triggered during runtime.
When using dynamic imports in Webpack, tree shaking may not work as expected by default. Inspect the utils bundle and observe that webpack has bundled both the sum
and the multiply
function in the final output. When a module is dynamically imported, it automatically makes that module ineligible for tree shaking. The result of the dynamic import is an object with all the exports of the module. Due to the dynamic nature of JavaScript, webpack can't easily determine which exports will be used, so webpack can't do any tree shaking.
However, there is a workaround available. The webpack team suggests utilizing the webpackExports
magic comment to explicitly inform webpack about the specific imports that should be included in the bundle. By using this comment, only the specified imports will be included, resulting in a more optimized bundle size.
https://github.com/webpack/webpack.js.org/issues/2684
<aside> 📦 Webpack magic comments, also known as webpack chunk or module comments, are special annotations that you can include in our code to provide hints or instructions to the webpack bundler during the build process. These comments allow us to control the behavior of webpack in relation to code splitting, chunk naming, and other bundling optimizations.
</aside>
import './index.scss'
const form = document.getElementById('add-form') as HTMLFormElement
const output = document.getElementById('result') as HTMLOutputElement
form.addEventListener('submit', async (e) => {
e.preventDefault()
const first = +(e.target as HTMLFormElement)['first'].value
const second = +(e.target as HTMLFormElement)['second'].value
const { sum } = await import(/* webpackExports: ["sum"] */ './utils')
const result = sum([first, second])
output.innerText = `Total = ${result}`
})
After re-running the build process and examining the generated bundle for the utils
module, you will notice that only the sum
utility function has been bundled in the output.