Webpack
Docs
https://webpack.js.org/guides/
Pending
File always read/executed from bottom to top and right to left?
Setup Webpack
npm i --save-dev webpack
Note it's only installed for dev environment.
Inside
package.json
, add:
"scripts": { "build": "webpack --config webpack.config.js" }
Note you might have to only add the "build" line.
Create
webpack.config.js
and add:
const path = require('path'); module.exports = { mode: 'production', entry: './app.js', output: { path: path.join(__dirname, 'dist'), publicPath: '/', filename: 'final.js', }, target: 'node', };
Note you might want to change ./app.js
to your correct path.
https://webpack.js.org/guides/installation/
Webpack Basic Setup Explained
const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { // 1 main: path.join(__dirname, "src/index.js"), }, output: { // 2 path: path.join(__dirname, '..', 'dist') }, plugins: [ new HtmlWebpackPlugin({ // 3 title: 'Webpack Configuration Split', }), ], };
Entry Point - tells webpack where to start building the dependency graph, this is used to create the bundle
Output - tells webpack where to create the outcome of the process - but not the naming of chunks since it depends on the environment
HtmlWebpackPlugin - tells webpack to generate the HTML index file
Further Webpack Setup Explanations
Add Cache Busting - a unique hash to created bundle - any change in files means new hash value
Generate the HTML index file
Add Sentry to the project
output: { path: path.resolve(__dirname, "build"), filename: "[contenthash].bundle.js", //[1] publicPath: "/", }, plugins: [ new HtmlWebpackPlugin({ //[2] template: path.join(__dirname, "public", "index.html"), }), new Dotenv(), sentryWebpackPlugin({ //[3] org: "supa-org", // change this to match yours project: "awesome-react", // change this to match yours authToken: "aiemn47==+iHDXvjMb8", // Get auth tokens here https://sentry.io/orgredirect/organizations/:orgslug/settings/auth-tokens/ }), ],
https://webpack.js.org/configuration/output/
https://webpack.js.org/plugins/html-webpack-plugin/
https://webpack.js.org/configuration/plugins/
Essential plugins: file-loader
plugin
file-loader
pluginfile-loader
allows to import file-based assets into our webpack managed JS and CSS files.
Some examples of files we may want to import include images (.jpg and .png) and font files (.woff2 and .eot).
We just need to make sure we tell webpack to run file-loader whenever it comes across one of these files.
Install with npm install file-loader --save-dev
Add loader to webpack config:
module: { entry: './src/app.js', output: { path: __dirname + '/dist', filename: 'build.js' }, loaders: [ ... { test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, use: [ { loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'fonts/' } } ] } ... ] },
This will tell webpack to search for any fonts being pulled in through one of our webpack entry points, in this case, our entry point is a file in a src
directory called app.js
.
If webpack finds any font files being referenced inside of any CSS files being pulled into app.js
, it'll duplicate the font files, and place them into whatever directory we specify using outputPath
.
This is required for our fonts to be referenced correctly within our compiled down version of our CSS located in /dist
.
In this instance, webpack
and file-loader
will duplicate all fonts to a folder called fonts/
within our dist/
directory since the output path of the fileLoader is relative to the path specified in the output.path
property of the overall webpack config.
https://www.chriscourses.com/blog/loading-fonts-with-webpack
Build
npm run build
node ./dist/final.js
This is to locally preview your buid.
Note you may want to change ./final.js
to your correct path.
Environment Variables Setup On Webpack
Webpack
npm i dotenv-webpack --save-dev
Note it's only installed for dev environment.
Update Your
webpack.config.js
const Dotenv = require('dotenv-webpack'); module.exports = { plugins: [ new Dotenv({ path: './.env', // default safe: false, // true = use .env.example to validate systemvars: false, // true = also load system environment variables allowEmptyValues: false // true = allow empty env vars }) ] };
Create Your
.env
FileAccess Vars In Your Code
console.log(process.env.PUBLIC_API_URL);
Development vs Production
https://webpack.js.org/guides/development/
https://webpack.js.org/guides/production/
Cache Busting
This includes a hash in the filename which changes every build.
Prevents browser from caching outdated files.
Cache Busting HowTo
Add to
webpack.config.js
:
const HtmlWebpackPlugin = require('html-webpack-plugin');
And then
plugins: [new HtmlWebpackPlugin({ template: "./src/template.html", hash: true })],
modify in
webpack.config.js
:
filename: "[contenthash].bundle.js",
https://webpack.js.org/plugins/html-webpack-plugin/
Setup Webpack With React
https://webkul.com/blog/webpack-with-react/
How to setup TypeScript + Babel + Webpack?
module
is where we pass the rules for bundling files
test
- file extension that needs to be targeted by the specific loaderexclude
- files to be ignored by the bundler.
Deets:
babel-loader
is added and setup fir reactts-loader
is addedcss-loader
andstyle-loader
are added - must be written in that exact orderfile-loader
is added
module: { rules: [ { test: /\.m?js|jsx$/, exclude: /(node_modules|bower_components)/, use: { loader: "babel-loader", //[1] options: { presets: [ "@babel/preset-react", ["@babel/preset-env", { targets: { ie: "11" } }], ], }, }, }, { test: /\.ts|tsx?$/, use: "ts-loader", //[2] exclude: /node_modules/, }, { test: /\.(sa|sc|c)ss$/, use: ["style-loader", "css-loader"], //[3] }, { test: /\.(png|jpe?g|gif)$/i, use: [ { loader: "file-loader", //[4] }, ], }, ], },
https://stackoverflow.com/questions/38320220/how-to-setup-typescript-babel-webpack
How to setup Webpack + ESLint?
A. Initialize ESLint configuration using
npx eslint --init
Answer all the questions prompted according to your project setup.
In the final prompt, it will ask you whether you want to install the required packages.
This will also create ESLint the config file.
B. Install packages Manually
That's if you're confident and know which packages to install. For instance:
npm install --save-dev eslint @eslint/js typescript typescript-eslint eslint-webpack-plugin
Then create ESLint Config file in root directory: eslint.config.js
(note the change from .eslintrc
since ESLint v9.0.0).
Note the recent change to "ESLint flat config".
Add to webpack.config.js file:
const ESLintPlugin = require('eslint-webpack-plugin'); module.exports = { ... plugins: [new ESLintPlugin()], ... };
In the ESLint Config File, add a test rule:
export default [ { "rules": { "max-len": [1, 70, 2, {"ignoreComments": true}] } }, ];
Tips
See fully resolved config ESLint uses:
npx eslint --print-config path/to/index.js
You might need to output it to a file:
npx eslint --print-config path/to/index.ts > full-eslint-config.json
Docs
ESLint in VSCode, HowTo https://www.robinwieruch.de/vscode-eslint/
ESLint in Webpack, HowTo https://www.robinwieruch.de/webpack-eslint/
https://webpack.js.org/plugins/eslint-webpack-plugin/
https://eslint.org/docs/
https://eslint.org/docs/latest/use/configure/migration-guide
https://eslint.org/docs/latest/use/configure/configuration-files
https://typescript-eslint.io/getting-started/
How to setup Prettier + ESLint?
Setup Prettier
1.a. install: npm install --save-dev --save-exact prettier
1.b. create an empty config file: node --eval "fs.writeFileSync('.prettierrc','{}\n')"
1.c. create a .prettierignore file to let the Prettier CLI and editors know which files to not format: node --eval "fs.writeFileSync('.prettierignore','# Ignore artifacts:\nbuild\ncoverage\n')"
Config ESLint Accordingly
2.a. install: npm i -D eslint-config-prettier
2.b. install: npm i -D eslint-plugin-prettier
Docs
https://prettier.io/docs/install
https://github.com/prettier/eslint-config-prettier
https://github.com/prettier/eslint-config-prettier
Setup Git Hooks to run the linter on push or commit?
install
husky
and create a sample pre-commit hook:npm install husky --save-dev && npm exec husky init
Edit the
husky
pre-commit hook in.husky/pre-commit
by adding:npm run lint && npm test
Setup
lint-staged
so we only lint and format the code that has changed in the commit
https://github.com/typicode/husky
https://github.com/lint-staged/lint-staged
Setup A11 JSX Linter Plugin
install:
config ESLint:
import jsxA11y from "eslint-plugin-jsx-a11y";
... jsxA11y.flatConfigs.recommended, ...
add image element with missing alt:
<img src=""></img>
run linter:
npm run lint
It should show an error
Split Webpack configuration to be environment-specific
Specify environment variables in
package.json
"scripts": { "build": "webpack --config webpack.prod.js" "dev": "webpack serve --open --config webpack.dev.js" "start": "webpack-dev-server --config webpack.dev.js", },
Create 3 config files
webpack.common.js
- common chunk, instructions necessary both in development and production
webpack.dev.js
- development chunk
webpack.prod.js
- production chunk
Copy/Paste everything from
webpack.config.js
intowebpack.common.js
Install
webpack-merge
package:npm i -D webpack-merge
Setup
webpack.prod.js
Add this:
const { merge } = require('webpack-merge'); const common = require("./webpack.common.js");
module.exports = merge(common, { ... });
Cut/paste anything in webpack.common.js
that is production only into webpack.prod.js
Typically you will have:
mode: "production", output: { filename: "[contenthash].bundle.js", },
Note that production
mode in our webpack configuration enables Tree Shaking
for our bundle.
This greatly decreases the bundle size, i.e. by 80%, by removing all the code that is not used from our codebase and the dependencies (node_modules).
Setup
webpack.dev.js
Add this:
const { merge } = require('webpack-merge'); const common = require("./webpack.common.js");
module.exports = merge(common, { ... });
Cut/paste anything in webpack.common.js
that is development only into webpack.dev.js
mode: "development", devtool: "inline-source-map", output: { filename: "bundle.js", },
Fix ESLint
.eslintignore
Add: webpack.*.js
https://dev.to/didof/how-to-split-dev-prod-webpack-configuration-n53
Source maps in webpack
Files that enable debugging of your code even after it's been bundled, minified, and optimized for production.
They provide a mapping between the transformed (e.g., minified) code and the original source code, allowing you to step through your original code in a browser's developer tools during debugging, despite the code being highly optimized for performance.
devtool: 'inline-source-map'
or
devtool: 'source-map'
Image optimization using webpack
Various plugins available such as:
image-webpack-loader
- most popularcompression-webpack-plugin
image-minimizer-webpack-plugin
- plugin version ofimagemin
responsive-loader
- generates multiple responsive images and outputs them assrcset
webpack-image-srcset-loader
- similar toresponsive-loader
svg-sprite-loader
- optimizes and bundles SVG files assprites
https://cloudinary.com/blog/guest_post/optimize-images-using-webpack-in-react
https://www.npmjs.com/package/compression-webpack-plugin
https://webpack.js.org/plugins/image-minimizer-webpack-plugin/
Font optimization using webpack
Loading fonts in Webpack involves setting up rules that handle different font formats, such as WOFF and TTF.
In Webpack 5, the asset/resource
module streamlines this process, automatically placing fonts in the appropriate output directory.
Improving fonts includes:
Subsetting - an important technique where only the necessary characters from a font are included, reducing file size.
Preloading fonts - helps them load faster, improving performance.
Typical checklist:
fonts should be hosted on the server - not loaded from 3rd party servers
fonts styling loaded via CSS
webpack config has a
file-loader
setup
module.exports = { entry: path.join(__dirname, "src/index.tsx"), ... module: { rules: [ ... { test: /.(woff2)$/i, use: { loader: "file-loader", options: { outputPath: "fonts/", name: "[contenthash]_[name].[ext]", }, }, }, ... ], }, };
file-loader
allows our webpack managed JS and CSS files to import file-based assets i.e. images and fonts
This will tell webpack:
in this case, our entry point is
index.tsx
file insrc
directory.search for any
.woff2
file being pulled in through one of our webpack entry points, even if indirectly i.e. referenced inside of a CSS file referenced inside our webpack entry pointsif found, duplicate the font files, and place them into the directory specified using
outputPath
.
This is required for our fonts to be referenced correctly within our compiled down version of our CSS located in /dist
.
In this instance, webpack
and file-loader
will duplicate all fonts to a folder called fonts/
within our dist/
directory since the output path of the fileLoader is relative to the path specified in the output.path
property of the overall webpack config.
CSS optimization using webpack
Best Practice:
Extract our CSS as a separate file that can be cached differently
Minify our CSS so the file we ship to the browser is smaller
Extract the Critical CSS and priotise it
Most popular plugins:
css-minimizer-webpack-plugin
- optimize and minify your CSSmini-css-extract-plugin
- extracts CSS into separate files, 1 CSS file per JS filehtml-critical-webpack-plugin
- extracts critical CSS
Implementation:
Install plugins
npm i --save-dev mini-css-extract-plugin
npm i --save-dev css-minimizer-webpack-plugin
npm i --save-dev html-critical-webpack-plugin
Extract and Minify CSS
Add mini-css-extract-plugin
and css-minimizer-webpack-plugin
to webpack config:
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = { ... module: { rules: [ { test: /.s?css$/, use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], }, ], }, optimization: { minimizer: [ // For webpack v5, you can use the
...
syntax to extend existing minimizers (i.e.terser-webpack-plugin
), uncomment the next line //...
, new CssMinimizerPlugin(), ], }, plugins: [new MiniCssExtractPlugin({filename: "[contenthash].css"})], };
Extract Critical CSS
We automatically extract the above-the-fold CSS and ship it in a different file that will be priotised.
This will reduce the CLS while also improving the FCP, especially in big applications.
Add html-critical-webpack-plugin
to webpack config:
const HtmlCriticalWebpackPlugin = require("html-critical-webpack-plugin"); const path = require("path");
module.exports = merge(common, { ... plugins: [ ... new HtmlCriticalWebpackPlugin({ base: path.resolve(__dirname, 'build'), src: 'index.html', dest: 'index.html', inline: true, minify: true, extract: true, width: 375, height: 565, penthouse: { blockJSRequests: false, } }) ], ... });
This is what html-critical-webpack-plugin
does:
Reads the file from disk as defined by the
src
optionExtracts the CSS from that file that is deemed as "critical" - it uses
Pupetter
to render the app in the backgroundWrites the new file back to disk with that critical CSS inlined, at the location of the
dest
option
Docs:
https://webpack.js.org/loaders/style-loader/#examples
https://webpack.js.org/plugins/css-minimizer-webpack-plugin/
https://webpack.js.org/plugins/mini-css-extract-plugin/
https://www.npmjs.com/package/html-critical-webpack-plugin
https://github.com/anthonygore/html-critical-webpack-plugin
JS Optimization: JavaScript Bundle Analysis with Webpack
Install plugin
npm install --save-dev webpack-bundle-analyzer
JS Optimization: Remove Unused Dependencies From Production
You can either:
go thru each package under
"dependencies": { ... }
and check if it's useduse
depcheck
package to do that for you - tho depcheck says "no longer actively maintained"
Note: tree-shaking
already removes most unused dependencies. Tho it's good practice to keep things as clean as possible.
https://www.npmjs.com/package/depcheck
JS Optimization: Replace Heavy Dependencies From Prod By Custom Implementation
Submit your package.json to https://bundlephobia.com/
It will go through your production dependencies, packages under "dependencies": { ... }
) and allow you to order by size.
Go thru the largest packages and check if their usage.
If one is barely used, i.e. for a single method, then replace it by your own implementation.
Create the relevant file under utils
, add the implementation (AI is your friend).
Then replace the imports and remove the dependency from package.json
Check your changes by re-running the build and analysis.
npm run build
npm run analyze
JS Optimization: Code Splitting By Route With Webpack
Code Splitting By Route = Route-based code splitting = Bundle Splitting
TLDR: only load the code needed for the current view.
Code splitting allows to produce numerous bundles, each bundle can be dynamically loaded during runtime - you lazy-load code on-demand.
Benefits:
Shorter initial load time
Easier scalability for large apps
Gotchas:
Must wrap lazy-loaded components in Suspense with a fallback UI.
If a chunk fails to load, e.g. due to a network issue, you may need to handle that gracefully.
Some libraries, e.g. analytics, error boundaries, need careful placement to cover async-loaded routes.
Implementation:
Install Package
npm install @loadable/component
npm i --save-dev @types/loadable__component
Add Lazy Loading To Route
... import loadable from "@loadable/component";
const Loading = () => (
Loading);
const LoadableMoviePage = loadable(() => import("./views/MoviePage"), { fallback: }); ... <Route path="/movie/:id" element={}> ...
Bonus:
Code Splitting by Route
"entry point" tells Webpack where to start building the dependency graph of your application.
By specifying multiple entry points, you can create multiple bundles, each of which can be loaded independently.
Docs:
https://www.dhiwise.com/post/optimizing-your-react-applications-with-webpack-code-splitting
https://rahuljain-dev.medium.com/unveiling-the-power-of-code-splitting-with-webpack-for-react-and-redux-applications-cc5a7cadf7d0
JS Optimization: Lazy Loading & Dynamic Import
Warning: not Webpack specific
Lazy Loading = Module Lazy Loading = Just In Time Loading
Lazy loading modules that are not needed for the initial load.
Module will be loaded asynchronously.
Implementation:
-import _ from "lodash"
export default function MovieCard({ movie }: MovieCardProps) { ... const theme = useSelector((state: RootState) => state.themeReducer.theme);
const [shortPlot, setShortPlot] = useState(movie.overview);
function plotShorten(text: string, length = 250) { // const shortText = _.take(text.split(""), length).join(""); // return shortText + "...";
return import("lodash").then((_) => { // dynamic import
const shortText = _.default.take(text.split(""), length).join("");
setShortPlot(shortText + "...");
}); }
useEffect(() => {
plotShorten(movie.overview);
}, [movie]);
return ( ...
Plot: {shortPlot} </MoviePlot>
)
Because the module will be loaded asynchronously we can only use it in places that accept a Promise.
To use it in JSX we need to add a new state that will get updated when the module is.
https://webpack.js.org/guides/code-splitting/#dynamic-imports
JS Optimization: Compress and Minify JS with Webpack
Various plugins:
compression-webpack-plugin
- Compressterser-webpack-plugin
- Minify and Uglify - shipped with Webpack 5UglifyJS
- Minify and Uglify - the former standard
Implementation:
Compress static assets using
compression-webpack-plugin
Install plugin
npm i --save-dev compression-webpack-plugin
Add Plugin to Webpack Config file
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = { plugins: [ new CompressionPlugin({ filename: "[path][base].gz", algorithm: "gzip", test: /.js$|.css$|.html$/, threshold: 10240, minRatio: 0.8, }), ... ] };
Set up
Brotli
as a better compression algorithm
Add one more compression plugin to Webpack Config file
module.exports = { plugins: [ ... new CompressionPlugin({ filename: "[path][base].br", algorithm: "brotliCompress", test: /.(js|css|html|svg)$/, compressionOptions: { level: 11 }, threshold: 10240, minRatio: 0.8, }) ... ] };
When running the build, you will get 2 compressed versions of the bundles: one in ".gzip" and the other in ".br".
Depending on the browser requests, the server will send the smallest one in a process called Content Negotiation
.
Explicitly Minify and Uglify with
terser-webpack-plugin
Webpack 5 ships with the latest terser-webpack-plugin
.
You must install terser-webpack-plugin
only if you wish to customize the options.
npm i --save-dev terser-webpack-plugin
Docs:
https://webpack.js.org/plugins/compression-webpack-plugin/
https://www.npmjs.com/package/terser-webpack-plugin
Webpack Module Federation (MF)
A feature of webpack that allows for the dynamic loading of multiple versions of a module from multiple independent build systems.
Allows for the creation of microfrontend-style applications, where multiple systems can share code and be dynamically updated without having to rebuild the entire application.
https://webpack.js.org/concepts/module-federation/
https://github.com/module-federation
https://module-federation.io/
Alternatives Module Federation to building Microfrontends:
SingleSpa https://single-spa.js.org/
Bit https://bit.dev/
Last updated