Improving developer experience as well as front-end performance with webpack.

Submitted by Nicholas on Tue, 12/21/2021 - 11:14

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 maintainenace 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.

a web unoptimized project folder structure
a_web unoptimized project folder structure

Ā 

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)

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

Uncaught ReferenceError structuredClone
Error on my chrome version (structuredClone)

Ā 

Implementing the solution:

Pre-requsites;

  • 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.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 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).

// start of .browserslistrc
last 2 versions
> 0.5%
// the end of .browserslistrc
 
// 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: /\.css$/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:

Browser vendor prefixes added.

Taking a look inside the dist folder: (defined by the property `output` of module.exports in wepack.config.js,)

dist dev builds


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.

Ā 

// 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.
    Ā 
    todoList.value_results
    ...todoList.value_results

    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

    babel.config.js update
    babel.config.js update

    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`, 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;
    main.scss updates
    main.scss updates

    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 of ` { 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 improvments.

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: