<aside> πŸ§‘β€πŸŽ“ To remove unwanted modules, we must make sure module is indeed unwanted:

How to inspect bundle contents?

</aside>

To remove unwanted modules means to de-prioritize the module, by moving from critical chunk, to a noncritical one. To achieve it, the following actions are required:

1. Learn why is a module included in a bundle

To find why is a module included in a bundle, the Webpack compilation stats are required. To generate them, build the application with corresponding flag:

yarn build --stats # using Yarn
npm run build -- --stats # using NPM

<aside> 🧠 Alternatively, try using https://statoscope.tech/ to get a UI to play around with!

</aside>

The build/bundle-stats.json will be generated as a result. Next, add the reason-finding utility to the root of your project. Create a reason.js file and paste the following content:

const fs = require('fs');
const path = require('path');

const moduleNameToLookup = process.argv[2] || 'CategoryPage.container.js';
console.log(`lookup of "${moduleNameToLookup}"...`)
console.log()

const statsPath = path.resolve(__dirname, './build/bundle-stats.json');
const stats = JSON.parse(fs.readFileSync(statsPath));

console.log('NOTE: preview is generated from compiled source, original module contents may differ.')

stats.modules.forEach((module) => {
    const { name, reasons, chunks } = module;

    if (!name.includes(moduleNameToLookup)) {
        return;
    }

    const pChunks = chunks.map((chunk) => {
        const { names, files } = stats.chunks[chunk];
        const fileNames = files.map(file => file.split('/').slice(-1)[0]).join(', ');

        if (!names) {
            return `${fileNames}`;
        }

        return `${fileNames} (part of ${names.map(name => `${name}`)} chunk group)`;
    });

    console.log('\\n' + Array.from({ length: 30 }, () => '=/').join('') + '\\n');
    console.log('File:');
    console.log(`\\t${name}`)
    console.log();
    console.log(`Chunks:`);
    pChunks.forEach((pChunk, i) => {
        console.log(`\\t ${i}. ${pChunk}`);
    });

    const validReasons = reasons.reduce((acc, reason) => {
        const { loc, moduleName } = reason;
        const reasonModule = stats.modules.find((module) => module.name === moduleName);

        if (!reasonModule || !loc) {
            // skipping if no module or no location
            return acc;
        }

        const [lineFrom, rest] = loc.split(':');
        const [charFrom, charTo] = rest.split('-');
        const lines = reasonModule.source.split('\\n');
        const segment = lines.slice(lineFrom - 1).join('\\n');
        const preview = segment.slice(charFrom, charTo);

        if (!preview.includes('export') && !preview.includes('import')) {
            // skipping if preview does not contain any import / export
            return acc;
        }

        const isReasonAlreadyIncluded = acc.some((otherReason) => (
            otherReason.preview === preview
            && otherReason.moduleName === moduleName
        ));

        if (isReasonAlreadyIncluded) {
            return acc;
        }

        return [
            ...acc,
            {
                preview,
                moduleName,
            }
        ]
    }, []);

    validReasons.forEach((reason, i) => {
        const { type, moduleName, preview } = reason;
        console.log('\\n' + Array.from({ length: 60 }, () => '-').join('') + '\\n');
        console.log(`${i + 1}. reason`);
        console.log();
        console.log(`File: ${moduleName}`);
        console.log(`Preview: ${preview}`);
    });
})

Next, run it, passing the module reason of which inclusion into the bundle we are interested to find:

node reason.js CategoryPage.container.js

It will output the similar result:

<aside> 🧠 If the imports of a module do not lead you anywhere directly, try searching for one of the reasons (reason file name) of a module of interest.

</aside>

Untitled

2. Make all module imports dynamic

<aside> 🧠 Static import is an import made using import A from B syntax. For example:

import { CATEGORY } from 'Component/Header/Header.config';

Dynamic import is an import made using import(A) syntax. For example:

const CartDispatcher = import(
  /* webpackMode: "lazy", webpackChunkName: "cart" */
  'Store/Cart/Cart.dispatcher'
);

The magic comments (i.e. webpackChunkName) are commonly used to hint Webpack a name of a chunk group to place a module into.

</aside>

If the module is included in a bundle, likely, it is statically imported in it. The goal is to transform these static imports into dynamic ones.

<aside> 🧠 All imports of a module must be dynamic for it to be moved into another chunk, if one of many will be dynamic, the module might become duplicated across chunks!

</aside>

How to make any import dynamic?

To make any import dynamic, replace its import dynamic one and rewrite logic using it to await its import (treat result of an import as promise). For example, consider the following static import and its usage later:

import { updateMeta } from 'Store/Meta/Meta.action';

updateMeta({});

Transforming this to dynamic import would look as follows:

const MetaActionImport = import('Store/Meta/Meta.action');

MetaActionImport.then(({ updateMeta }) => {
	updateMeta({});
});

To further improve this logic, we can append the magic comment, hinting where (in which chunk group) to put the import:

const MetaActionImport = import(
	/* webpackMode: "lazy", webpackChunkName: "meta" */
	'Store/Meta/Meta.action'
);

MetaActionImport.then(({ updateMeta }) => {
	updateMeta({});
});

How to make a component import dynamic?

If the import of React component needs to become dynamic, it’s newly created dynamic import should be wrapped with lazy React utility, and rendering with Suspense React utility. For example, consider the following React component:

import Breadcrumbs from 'Component/Breadcrumbs';
import Loader from 'Component/Loader';

function Component(props) {
	return (
		<div>
			<Loader />
			<Breadcrumbs />
		</div>
	);
}

Let’s make Breadcrumbs import dynamic and add lazy and Suspense utilities:

import { lazy, Suspense } from 'react';
import Loader from 'Component/Loader';

const Breadcrumbs = lazy(() => import('Component/Breadcrumbs'));

function Component(props) {
	return (
		<div>
			<Loader />
			<Suspense fallback={ <div /> }>
				<Breadcrumbs />
			</Suspense>
		</div>
	);
}

To further improve this logic, we can append the magic comment, hinting where (in which chunk group) to put the import:

import { lazy, Suspense } from 'react';
import Loader from 'Component/Loader';

const Breadcrumbs = lazy(() => import(
	/* webpackMode: "lazy", webpackChunkName: "meta" */
	'Component/Breadcrumbs'
));

function Component(props) {
	return (
		<div>
			<Loader />
			<Suspense fallback={ <div /> }>
				<Breadcrumbs />
			</Suspense>
		</div>
	);
}

3. De-prioritize a dynamic import

Once the modules are dynamically imported, it is important to make sure they do not affect the critical rendering path. Make sure to apply request de-prioritization techniques to dynamic imports of removed modules:

How to de-prioritize non-critical requests?

<aside> πŸŽ‰ This is how you remove unwanted modules!

</aside>