Improving developer experience as well as front-end performance with webpack.
Tags: webpack, javascript, postcss, babel
Published: 2022-06-03 10:26:41

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](https://caniuse.com/) 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](https://webpack.js.org/plugins/html-webpack-plugin/).

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](https://pure-headland-11901.herokuapp.com/),
[also pushed its code to github (branch name: unoptimized)](https://github.com/flaircore/a_web/tree/unoptimized).
The final code is also available on [github (branch name: optimized)](https://github.com/flaircore/a_web/tree/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](https://github.com/flaircore/a_web/tree/optimized) for the ease of reference.

**Below is the folder structure of our app so far.**

![a_web unoptimized project folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jsmkgfkkkrccf8wq5y2p.PNG)

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](https://www.npmjs.com/package/chart.js?activeTab=readme) 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](https://getbootstrap.com/docs/5.0/customize/optimize/#lean-javascript) 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:**
```css


// 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:**
```javascript
// 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

```
 **chart.js contents:**

```javascript
// 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](https://github.com/flaircore/a_web/blob/unoptimized/libs/main.js), does not work on my version of chrome because of [the support across all browsers](https://caniuse.com/?search=structuredClone).
- place-items center, will not work on all browsers because, of [the support across all browsers at caniuse](https://caniuse.com/?search=place-items),

![Uncaught ReferenceError structuredClone Error on my chrome version (structuredClone)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xeghu67684d5xe0ca26c.PNG)

- [`flex: 1;`](https://developer.mozilla.org/en-US/docs/Web/CSS/flex) on line 29 of [libs/main.css](https://github.com/flaircore/a_web/blob/unoptimized/libs/main.css), will not [work on all browsers](https://caniuse.com/?search=flex) at the time of this writing, the same for [the array spreads on lines 9 and 10](https://caniuse.com/?search=spread) of [libs/charts.js](https://github.com/flaircore/a_web/blob/unoptimized/libs/charts.js) .


**Implementing the solution:**
**Prerequisites;**

- [Nodejs](https://nodejs.org/en/) and npm or yarn installed on your machine

**CSS and Browser support with [postcss](https://github.com/postcss/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](https://webpack.js.org/guides/getting-started/) cross-env [css-loader](https://webpack.js.org/loaders/css-loader/) mini-css-extract-plugin [postcss postcss-loader](https://webpack.js.org/loaders/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](https://www.npmjs.com/package/cross-env) and builds the production bundle, minified and without source-maps  as set in the [webpack.config.js](https://webpack.js.org/configuration/devtool/#root) 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.rules[0] (in webpack.config.js) reads from this file, and uses "postcss-preset-env" plugin to determine which browsers to add support for, but (you can run [npx browserslist](https://github.com/browserslist/browserslist#debug) 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](https://github.com/browserslist/browserslist#debug).
Our .browserslistrc contains two lines of text/code ie;

```txt

    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**

```javascript
// start of postcss.config.js
module.exports = {
    plugins: [require("postcss-preset-env")]
}
// the end of postcss.config.js
```
**
webpack.config.js file contents**

```javascript
// 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,](https://github.com/flaircore/a_web/blob/optimized/webpack.config.js)
    and include `import "./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,)

![dist build folder and files](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p9p0h8p9aajxmxtcqimx.PNG)

**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](https://developer.mozilla.org/en-US/docs/Glossary/Vendor_Prefix): (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](https://webpack.js.org/loaders/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

```javascript
// start of babel.config.js
// @babel/preset-env is complimented by .browserslistrc
module.exports = {
    presets: ["@babel/preset-env"]
}
// end of babel.config.js
```

**module.rules[1], item**

```javascript
// 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.


![...todoList.value_results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oi3om68dw7h1tb4ecwi9.PNG)

From this we can tell only [structuredClone()](https://caniuse.com/?search=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()](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#browser_compatibility) 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](https://github.com/zloirock/core-js#readme).

- Run `yarn add core-js` to add core-js as a project dependency and change the contents of the babel.config.js file to

```javascript
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](https://webpack.js.org/loaders/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](https://getbootstrap.com/docs/5.0/customize/optimize/) by including only the parts of bootstrap our app requires using sass imports.

**Steps:**

- Change `test: /\.css$/i`, to `test: /\.(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.rules[0] (webpack.config.js).

- 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;

```scss
 // 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 add `const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");` and below the module property of module.exports = {}, add another property named `optimization` with a value like so:

```javascript
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](https://webpack-optimized.herokuapp.com/).

**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](https://webpack.js.org/guides/getting-started/)
- [Babel documentation](https://babeljs.io/docs/en/)
- [PostCSS](https://github.com/postcss/postcss)
- [Polyfill](https://developer.mozilla.org/en-US/docs/Glossary/Polyfill) (MDN)
- [Vendor Prefix](https://developer.mozilla.org/en-US/docs/Glossary/Vendor_Prefix) (MDN)
- [https://cssdb.org/](https://cssdb.org/)
- [Bootstrap Optimize](https://getbootstrap.com/docs/5.0/customize/optimize/#lean-javascript) (but apply the same to any library you're using)
- [Core-js](https://github.com/zloirock/core-js#readme)