Code Splitting
The process of splitting an application bundle into multiple chunks is called bundle splitting. There are multiple reasons why the bundle could/should be split into chunks, but most of them are related to application performance. Splitting into code chunks can be accomplished in two different ways:
- Static split
- Dynamic split
In both of these scenarios, multiple chunks are produced, but the purpose of using them is different.
Static split #
During a static split, chunks are defined as separate webpack application entry points. This way, webpack grabs all the code reachable from a particular entry point and moves it into a separate chunk. With no additional configuration, all duplicated code across chunks will be copied to every chunk, resulting in an increased total application bundle size. Let's see how a static split is configured.
Utils #
webpack/parts.ts #
Change webpack/parts.ts to include an additional entry point:
entry: {
main: './index',
utils: './utils/index'
} as Entry,
In this split, we define an additional chunk called utils. The chunk will contain all parts of the application code reachable from the ./utils/index entry point. If you build the app, you should see an additional chunk in the output. As mentioned above, all the code that belongs to (is reachable from) both chunks gets bundled into every chunk. Let's fix this problem.
webpack/parts.ts #
Change webpack/parts.ts to include optimization section:
optimization: {
splitChunks: {
chunks: 'all'
}
}
This optimization configures SplitChunksPlugin to remove all duplicates (to keep shared code in one place). Strictly speaking, it is not needed for local dev workflows, but debugging the app with no duplication is a bit less confusing.
webpack/webpack.config.dev.ts #
Change webpack/webpack.config.dev.ts to include the optimization part:
optimization: parts.optimization,
webpack/webpack.config.ts #
Change webpack/webpack.config.ts to include the optimization part:
optimization: {
...parts.optimization,
minimize: true,
removeAvailableModules: true
}
Vendors #
So why would you split the app into multiple entry points? Well, statistically, some parts of the app change more frequently than others. We can take advantage of this by splitting less frequently changing parts into separate chunks. In the previous example, we assumed that the utils chunk changes less than the main app chunk. The browser can cache this chunk and keep it until a new version is released. A similar idea can be applied to third‑party application code (vendors). Again, statistically, third‑party dependencies are added more frequently at the beginning of the project. As the codebase stabilizes, more independent parts become stable. This makes them good candidates for browser caching, hence splitting into a vendors chunk.
webpack/parts.ts #
Change optimization section of webpack/parts.ts:
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/i,
name: 'vendors',
chunks: 'all'
}
}
},
runtimeChunk: {
name: 'vendors'
}
} as Options.Optimization
We are defining a chunk name vendors and assigning all the source files with the node_modules in the path to it.
splitChunks- finds modules which are shared between chunks and splits them into separate chunks to reduce duplicationcacheGroups- groups JavaScript modules into cache groupsvendors- user defined cache group (any string value that could represent a key entry within thecacheGroupsobject)test- regex for testing file paths. Controls which modules are selected by this cache groupname- chunk name,chunks- indicates which chunks will be selected for optimizationruntimeChunk- creates a separate chunk for the webpack runtime code and chunk hash maps
Dynamic split #
In contrast to static splitting, the purpose of dynamic splitting is to create chunks that are used less frequently by the user or to offload less important chunks to the background while the main application is loading. Chunks are created during build time from hints in the code, but it is up to the application logic to load them when needed.
Dependencies #
To instrument webpack for creating code chunks that can be loaded conditionally, we have to use the JavaScript import function. Webpack understands this syntax out of the box. Unfortunately, Babel does not. It is one of those cases where a feature works without Babel, but bringing Babel in breaks it and requires additional configuration.
@babel/plugin-syntax-dynamic-import- extends babel functionality to understand JavaScriptimportfunctionality.
npm -i @babel/plugin-syntax-dynamic-import -D --save-exact
Babel configuration #
babel.config.js #
Add the dynamic import plugin to the babel.config.js config:
plugins: [
// ...
"@babel/plugin-syntax-dynamic-import",
// ...
]
Webpack configuration #
webpack/parts.ts #
Change webpack/parts.ts contents:
output: {
path: path.resolve(distFolder(), 'js'),
filename: '[name].bundle.js',
publicPath: '/js/'
} as Output,
Sources #
src/app/index.ts #
Change src/app/index.ts to load feature code dynamically:
export type DynamicImport = Promise<{ default: () => string }>
function* func (): Generator<void | DynamicImport, void> {
const result = yield console.log('test')
console.log(result)
yield import(/* webpackChunkName: "feature" */ './feature/index')
}
export default func
We gave the feature chunk the name
featureby using the webpack‑related commentwebpackChunkName. By default, webpack assigns sequential numbers to anonymous chunks. This makes it a bit more difficult to understand the purpose of loaded chunks during debugging.
src/index.ts #
Change src/index.ts to execute the loaded chunk:
import 'core-js/stable'
import 'regenerator-runtime/runtime'
import func, { DynamicImport } from '@app'
import { toCapital } from '@utils'
const f = func()
f.next()
setTimeout((): void => {
const next = f.next(toCapital('hello world'));
(next.value as DynamicImport)
.then((data): void => {
console.log(data.default())
})
}, 3000)