<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:
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>
<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>
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({});
});
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>
);
}
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>