Pack your Web Project Efficiently with Webpack

For every web developer, improving web performance is our eternal goal. What does it mean? In the simplest term, it’s to load our web faster. And the primary reasons for loading the web slowing are:

  • Amount of JavaScript for initial download
  • Amount of CSS for initial download
  • Amount of network requests on initial download
example of code coverage

For now, we are focusing on the first point. One way that Webpack can help is through code splitting.

But what is code splitting? Process of splitting pieces of your code into async chunks at build time.

Essentially, we will only load the necessary file the first time, and async load the module that we won’t use initially later.

how code splitting works in webpack

In webpack, we do this through dynamic import explicitly:

Now image we have a Modal module that we know won’t be needed initially (maybe not until a button is clicked). In index.js we normally import it directly :

import { Modal } from “./modal”;

Instead, we can import it but return a promise instead:

const getModal = () => import(“./modal”);

And when we know it will be needed:

button.addEventListener(“click”, e => {
getModal().then(modalModule => {

Now when we test it with development server, and click the button, we can see a chuck file appears indicating the Modal is only load when we click:

The lazy “chunk” is 0.js.

The same applies for big 3rd party libraries. Image we have a gsap module:

const getGSAP = () => import(“gsap”);button.addEventListener("click", e => {
getGSAP().then(gsap => {

We can also do dynamic import routes:

This way we can pass variables to the import function and request different chunks depends.

const getTheme = themeName => import{`/src/themes/${themeName}`}

So at build time, Webpack will go into the /src/themes folder and create a chunk for each theme .

If you don’t like the standard chunk file name, you can also change it using the magic comments feature:

They are inline comments to make features work. By adding comments to the import, we can do things such as name our chunk or select different modes.

// Single target
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
/* webpackExports: ["default", "named"] */
// Multiple possible targets
/* webpackInclude: /\.json$/ */
/* webpackExclude: /\.noimport\.json$/ */
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
/* webpackPrefetch: true */
/* webpackPreload: true */
import(/* webpackIgnore: true */ 'ignored-module.js');

So we can set chunk name like:

if (process.env.NODE_ENV === “development”) {   const setButtonStyle = (color) => import(
/* webpackMode: “lazy-once” */`./button-styles/${color}.js`);
} else {
const setButtonStyle = (color) => import(

Based on Webpack documentation, there aredifferent modes for resolving dynamic imports can be specified. The following options are supported:

  • 'lazy' (default): Generates a lazy-loadable chunk for each import()ed module.
  • 'lazy-once': Generates a single lazy-loadable chunk that can satisfy all calls to import(). The chunk will be fetched on the first call to import(), and subsequent calls to import() will use the same network response. Note that this only makes sense in the case of a partially dynamic statement, e.g. import(`./locales/${language}.json`), where multiple module paths that can potentially be requested.
  • 'eager': Generates no extra chunk. All modules are included in the current chunk and no additional network requests are made. A Promise is still returned but is already resolved. In contrast to a static import, the module isn't executed until the call to import() is made.
  • 'weak': Tries to load the module if the module function has already been loaded in some other way (e.g. another chunk imported it or a script containing the module was loaded). A Promise is still returned, but only successfully resolves if the chunks are already on the client. If the module is not available, the Promise is rejected. A network request will never be performed. This is useful for universal rendering when required chunks are always manually served in initial requests (embedded within the page), but not in cases where app navigation will trigger an import not initially served.

Apart from code splitting, we can go one step further with preload and prefetch:

  • prefetch: resource is probably needed for some navigation in the future
  • preload: resource might be needed during the current navigation.

Let’s say we have a lodash module that we sure it might be needed the user open the webpage:

const getLodash = () => import(/* webpackPreload: true */”lodash-es”);

This will result in <link rel="prefetch" href="lodash-es-chunk.js"> being appended in the head of the page, which will instruct the browser to prefetch in idle time the lodash-es-chunk.js file.

Preload directive has a bunch of differences compared to prefetch:

  • A preloaded chunk starts loading in parallel to the parent chunk. A prefetched chunk starts after the parent chunk finishes loading.
  • A preloaded chunk has medium priority and is instantly downloaded. A prefetched chunk is downloaded while the browser is idle.
  • A preloaded chunk should be instantly requested by the parent chunk. A prefetched chunk can be used anytime in the future.

Finally don’t forget to use tools like webpack-bundle-analyzer to analyse your bundles and then decide how to optimise.

That’s so much of it. Happy Reading!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store