In this blog, we will see how we could improve our front end developer experience as well as performance by introducing webpack (plugins and loaders in the eco system) as an extra tool in our front-end development toolbox 🧰 🧰 .
Some of the problems webpack tries to solve include;
-
We can arrange our front-end project into different files and folders for easier development and maintenance and bundle/pack that maybe into one or several files as our app requires.
-
Ensure our code is compatible across browsers by introducing loaders like post-css and babel to add vendor prefixes and transpile newer ECMAScript syntax into older javascript and adding pollyfils for newer web apis/ES implements respectively.
-
Minify/compress our files (js/css/images) accordingly for performance reasons.
-
Include/import only the required portions of our dependencies (library/package) into our final bundle (Tree Shaking) instead of the whole package, to reduce the size our end-users have to download.
Because of the nature of our app ( Drupal/WordPress maybe), we will independently manage our templates (html) but webpack could also be configured to do that.
Our blog is about improvement, so it's better if we start with something to improve upon.I went ahead and made an app and hosted it on heroku,
also pushed its code to github (branch name: unoptimized).
The final code is also available on github (branch name: optimized).
Since we're going to be adding files or changing/updating their contents throughout the blog, I will be linking that to each file in the optimized branch for the ease of reference.
Below is the folder structure of our app so far.
Its a two page app (index.html and charts.html), the libs folder contains our .js and .css files that serve the two templates, and these are the files we will be focusing on.
About the templates:
For the templates, I'm importing both bootstrap css and bootstrap js (with its dependency ie poppers.js) via cdn, also libs/main.css (custom css) is included in all template files.
In the index.html; libs/main.js is included, while in charts.html; libs/charts.js and its dependency chart.js via cdn.
The Plan:
-
Install and configure post-css loader to ensure vendor prefixes are added according to our project specifications.
-
Install and configure babel so our javascript runs on all browsers (again according to our project specifications).
-
Introduce css preprocessors so we can modularize our css files and take advantage of the magic around such setups.
-
Download and bundle our app dependencies instead of loading them via cdn, including only the parts of the dependencies our app needs (like from bootstrap index.html only needs
'bootstrap/js/dist/dropdown'
for the navbar and charts.html only needs'bootstrap/js/dist/modal'
for modal. -
Minify images for faster downloads.
We will now discover the reason our app works on some browsers and not in others and is probably not as performant (downloadable files) as it should be, as we go through our webpack configurations.
Below is the code that powers our templates (without it's dependencies).
(all dependencies are loaded via cdn at this point)
main.css contents:
// start of main.css * { margin: 0; padding: 0; } /** https://developer.mozilla.org/en-US/docs/Web/CSS/place-items */ header { min-height: 20vh; display: flex; place-items: center; background: dimgrey; } .content { min-height: 60vh; /** background: #dcdcdc;*/ background-image: url("../assets/bikers_trees.jpg"), url("../assets/nature_trees.jpg"); background-position: center; background-size: cover; } #todo-list { display: flex; justify-content: space-between; align-items: center; flex-direction: column; flex: 1; margin: 0rem 2rem; } footer { min-height: 20vh; display: grid; place-items: center; background: dimgrey; } // end of main.css // main.js contents console.log("++++ latest Web APIS ++++++ ") const content = document.querySelector(".content") const original = { name: "Web technology for developers Web APIs; Testing: structuredClone()" }; original.itself = original; const clone = structuredClone(original); content.innerText = clone.name content.style.color = "white" console.log(clone) // end of main.js // charts.js const chart01 = document.getElementById('chart01').getContext('2d'); const chart02 = document.getElementById('chart02').getContext('2d'); const todoListDiv = document.querySelector('#todo-list') const todoList = { value: ["Walk Dog", "Publish Blog", "Renew expired subscriptions"] } todoList.value = [...todoList.value, "Buy groceries"] todoList.value = [...todoList.value, "Fix reactivity Bug"] for (const todo of todoList.value) { // Creates a "todo" document const todoDiv = document.createElement("div") todoDiv.classList.add("todo") const newItem = document.createElement("li") newItem.innerText = todo newItem.classList.add("item") todoDiv.appendChild(newItem) // Add item to list todoListDiv.appendChild(todoDiv) } // Just the same examples on chart.js documentation // @ https://www.chartjs.org/ const myChart = new Chart(chart01, { type: 'bar', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [{ label: '# of Votes', data: [12, 19, 3, 5, 2, 3], backgroundColor: [ 'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)' ], borderColor: [ 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)' ], borderWidth: 1 }] }, options: { scales: { y: { beginAtZero: true } } } }); const myChart02 = new Chart(chart02, { type: 'bar', data: { labels: ['Houses', 'Hotels', 'Lofts', 'Parks', 'Cottage', 'Cruise-Ships'], datasets: [{ label: '# of Votes', data: [12, 19, 3, 5, 2, 3], backgroundColor: [ 'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)' ], borderColor: [ 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)' ], borderWidth: 1 }] }, options: { scales: { y: { beginAtZero: true } } } }); // end of charts.js
For instance;
- the structuredClone(original) implement in main.js on line 6, does not work on my version of chrome because of the support across all browsers.
- place-items center, will not work on all browsers because, of the support across all browsers at caniuse,
flex: 1;
on line 29 of libs/main.css, will not work on all browsers at the time of this writing, the same for the array spreads on lines 9 and 10 of libs/charts.js .
Implementing the solution:
Prerequisites;
- Nodejs and npm or yarn installed on your machine
CSS and Browser support with postcss#
Lets initialize a new npm project by running yarn init -y
in the root of the a_web folder, and install the necessary dev dependencies to get our css working as intended across all browsers, by runing 'yarn add -D webpack webpack-cli cross-env css-loader mini-css-extract-plugin postcss postcss-loader postcss-preset-env'
Lets add three more scripts to our package.json as;
- "build-dev": "webpack",
- "watch": "webpack --watch",
- "build": "cross-env NODE_ENV=production webpack" build-dev; builds our dev bundle with source maps and not minified as we will see in another file we will add next (webpack.config.js)
watch; runs in the background, and watches for file changes(saves) and recompiles the files (css/js) during development; to be loaded by your template files.
build; sets and enviroment valiable of NODE_ENV=production using cross-env lib and builds the production bundle, minified and without source-maps as set in the webpack.config.js file.
Upto this point, we will be required to add several files to get our css fully working as intended: namely;
- webpack.config.js,
- postcss.config.js, and
- .browserslistrc
Purpose of each file(s):
webpack.config.js:
It's in this file that we will define;
- application specific settings according to mode; development or production.
- Entry files and output folder (file names).
- Control the output of our files in regard to mode (development/production)
- Define how to process/transpile file types and plugins to apply to each as defined in the plugins property, eg scss/css or javascript/ts
postcss.config.js:
postcss-loader we installed and defined in module.rules0 reads from this file, and uses "postcss-preset-env" plugin to determine which browsers to add support for, but (you can run npx browserslist in the command line before and after adding the below file (.browserslistrc)).
.browserslistrc:
Compliments postcss.config.js and .babelrc(to add later) and adds extra rules for more granular browser support.
Our .browserslistrc contains two lines of text/code ie;
last 2 versions > 0.5%
Last 2 versions tells webpack to support the "last two versions " of any browser(s), and "> 0.5%" tells webpack to add support to any browser version that has a traffic/usage of more than 0.5%.
Below is the code contained in each file (some details in comments).
postcss.config.js file contents
// start of postcss.config.js module.exports = { plugins: [require("postcss-preset-env")] } // the end of postcss.config.js // start of webpack.config.js const MiniCSSExtractPlugin = require("mini-css-extract-plugin") const path = require("path") let mode = "development", source_map = "source-map" // if NODE_ENV is set to prod, we disable source-maps, // and set webpack mode is production for it to use // its built in optimizations accordingly eg minified/optimized // files. if (process.env.NODE_ENV === "production") { mode = "production" source_map = "eval" } module.exports = { mode: mode, /** * entries for raw js files (source) */ entry: { main: path.resolve(__dirname, 'src/main.js'), charts: path.resolve(__dirname, 'src/charts.js'), }, /** * output folder, * where [name] === entry[name]/entry[i] from above */ output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), // clean: true; cleans the output folder from previous builds. clean: true, }, /** * devtools controls if and how source maps are generated. */ devtool: source_map, /** * https://webpack.js.org/configuration/plugins/ */ plugins: [ new MiniCSSExtractPlugin() ], /** * https://webpack.js.org/configuration/module/#rule */ module: { rules: [ { test: /\.(s[ac]|c)ss$/i, /** * postcss-loader, * css-loader and * finally we extract css to * a separate file with MiniCSSExtractPlugin.loader plugin. * Another option, is to use style-loader to inject inline css into * our template files but we don't need that approach. */ use:[ MiniCSSExtractPlugin.loader, "css-loader", "postcss-loader" ] } ] } } // end of webpack.config.js @todo add babel
#Running the builds so far:
- Copy all the files in the libs folder (main.js, charts.js and main.css) into the src folder to match the property
entry
of module.exports in wepack.config.js, and includeimport "./main.css"
at the top of src/main.js file. - Run
yarn build-dev
Browser vendor prefixes added.
Taking a look inside the dist folder: (defined by the property output
of module.exports in wepack.config.js,)
Recap so far: From the top,
- we have the images files (not minimized, but we will come back and fix this, for smaller files === faster downloads)
- In the charts.bundle.js the syntax
todoList.value = [...todoList.value, "Buy groceries"] todoList.value = [...todoList.value, "Fix reactivity Bug"]
, and in the main.bundle.js our structuredClone() method still exists, we dont want this for the browser support. - In the main.css, we have achieved the webpack magic: (vendor prefixes) added by the post-css configurations that makes sure our css rules are supported across the browser versions/types defined in the .browserslistrc file.
Let's add babel for es6 support:
- Run 'yarn add babel-loader @babel/core @babel/preset-env -D'
- Lets add another file named
babel.config.js
and update the modules.rules[] property of webpack.config.js with one more rules object like below.
Contents of babel.config.js
// start of babel.config.js // @babel/preset-env is complimented by .browserslistrc module.exports = { presets: ["@babel/preset-env"] } // end of babel.config.js // Start of module.rules[1] @ { test: /\.js$/, exclude: /node_modules/, /** * babel-loader (babel.config.js) */ use: [ "babel-loader" ] }, // End of module.rules[1]
Recap so far:
- Run
yarn build-dev
again and lets search through the dist/charts.bundle.js and dist/main.bundle.js for "[...todoList.value, "Buy groceries"]" and "structuredClone(original)" respectively.
From this we can tell only structuredClone() was not converted to an older version of it's implementation to ensure older browser support like the array spreads for the todoList variable for instance (also remember structuredClone() was not recognized in my version of chrome, and one way to solve this is to update my browser version, but our end users might be "lazy" to do this). A better way to achieve this would be to introduce polyfills so we don't force our users to always update their browsers (remember customer is king) : enter core-js.
- Run
yarn add core-js
to add core-js as a project dependency and change the contents of the babel.config.js file to
module.exports = { presets: [ [ "@babel/preset-env", {debug: true, useBuiltIns: "usage", corejs: 3}, ] ] }
this tells webpack to only auto include the polyfills we need in our final output i.e only the necessary support specific to our application.
- Let's run
yarn build-dev
one more time before moving on to the next step. - Next, let's tell our templates files to now load these files in the dist folder instead.The first step is easy, just tell index.html to load dist/main.css, and dist/main.bundle.js; and charts.html to load dist/main.css and dist/charts.bundle.js, we may also delete the libs folder, because we don't need it at this stage of our app, and maybe test our app to make sure nothing was broken before moving on to manage and bundle all the other dependencies we're requiring via cdn.
#Application dependencies :
Let's install our app dependencies and include them in our entry files and update webpack.config to process sass files by running;
'yarn add sass sass-loader -D && yarn add bootstrap chart.js @popperjs/core'
Then update our webpack rules as required, to process scss files as well and update the loaders to use sass-loader: too, because we'll also optimize our css by including only the parts of bootstrap our app requires using sass imports.
Steps:
-
Change
test: /\.css$/i
, totest: /\.(sc|c)ss$/i
, in webpack.config.js and include "sass-loader" as the last item in the array value of key "use" in the same module.rules0. -
Include only the bootstrap modules we require ie add
import 'bootstrap/js/dist/modal'
and import Chart from 'chart.js/auto' in our charts.js and import 'bootstrap/js/dist/dropdown' in main.js because we need that in nav-item dropdown in index.html: and delete all the other libraries our templates were loading via cdn. -
Lastly we will need to rename our main.css file to main.scss and update the import of the same @ main.js file, then we will need to import only the scss component our app uses/needs from bootstrap so it looks like below;
// Configuration @import "bootstrap/scss/functions"; @import "bootstrap/scss/variables"; @import "bootstrap/scss/mixins"; @import "bootstrap/scss/utilities"; // Layout & components @import "bootstrap/scss/root"; @import "bootstrap/scss/reboot"; @import "bootstrap/scss/type"; @import "bootstrap/scss/images"; @import "bootstrap/scss/containers"; @import "bootstrap/scss/buttons"; @import "bootstrap/scss/transitions"; @import "bootstrap/scss/dropdown"; @import "bootstrap/scss/button-group"; @import "bootstrap/scss/nav"; @import "bootstrap/scss/navbar"; @import "bootstrap/scss/modal"; @import "bootstrap/scss/close"; * { margin: 0; padding: 0; } /** https://developer.mozilla.org/en-US/docs/Web/CSS/place-items */ header { min-height: 20vh; display: flex; place-items: center; background: dimgrey; } // more code from older file...
Run yarn build-dev
again and test if the two pages are broken, i.e index.html and charts.html, we have included only the parts we need from the libraries our app depends on instead of all, we have also splited our javascript code into separate files so we can only include chart.js on the page/template it's required.
Minify images:
- Run
yarn add image-minimizer-webpack-plugin @squoosh/lib -D
to install the image minifiers we will be using and and update the contents of the webpack.config.js as follows: At the top addconst ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
and below the module property of module.exports = {}, add another property namedoptimization
with a value like so:
optimization: { minimizer: [ "...", new ImageMinimizerPlugin({ minimizer: { implementation: ImageMinimizerPlugin.squooshMinify, options: { // Your options for `squoosh` }, }, }), ],
And run yarn build
to build a production optimized bundle with everything optimized and minified before uploading this version of the app on heroku and testing the improvements.
Conclusion / Benefits:
- We have improved developer experience and can now use the latest es6 implement and latest css rules without worrying of our app being inconsistent across all browsers.
- We have also made sure users download only what a webpage requires to function by removing additional code that was included in our dependencies, but our app didn't require them.
- You can throw in any new css rule or es implementation without the worry of inconsistency across browsers; post-css, babel and core-js 🎉🥳 🥂 🍻!
- Better SEO ranking (bigger reach), due to best practices (consistency across browsers) resulting in more sales or/and engagements.
- There are several other tools/options one could use to optimize and improve their front-end developer experience as well as performance; grunt, gulp, parcel or vite, but you might find some, limiting according to your project needs or setup sometimes.
Resources:
- Webpack js documentation
- Github repo for this blog.
- Babel documentation
- PostCSS
- Polyfill (MDN)
- Vendor Prefix (MDN)
- https://cssdb.org/
- Bootstrap Optimize (but apply the same to any library you're using)
- Core-js