Analyzing and Improving Bundle Size
Tags: vue, performance, frontend, webdev
Published: 2020-04-19 05:39:38

Setting up frontend project these days is as easy as running a single command. We will get all of the good stuff and best practices alongside with the template  (vue-cli or create-react-app) when we initialised the project. But what happened when we adding more and more components, pages, 3rd party libs, etc in our project? the bundle size will increase as the time goes on and gradually slowing down our apps. What shall we do? There are some improvements methods to do depending on our project conditions. First thing first before we do any action we need to analyse it first to know what we're up against.

1. Google Lighthouse
this is an interesting and very useful tools to give a high level information and suggestion about how our app perform in browser. It will provide score and suggestion on how we improve the apps. This tools can be a baseline guide on which methods we should choose to improve our site.

2. webpack-bundle-analyzer https://www.npmjs.com/package/webpack-bundle-analyzer
this tools help us to check each size of our application chunk. By looking of the report generated by this tool we can find and minimize unused chunk of code being bundled in our application

3. browser network inspection tool
this is basic tool that offered by most browser to help us spotted files and data being transferred to our site. By combining these 3 tools we will start our improvement

Let's get started to code i'm going to use vue in this example (will add a react version later). We will be starting with a project with bunch of libraries packed into it, then we will improve step by step

clone this project https://github.com/heruujoko/performante-vue-example

navigate to branch feature/without-optimization and try to run npm run build to see our condition of initial build
![before optimize](https://i.ibb.co/mqGFg5S/build-before-optimized.png)

Just looking at the result, somehow it still look fine and will do just ok in most browser. But we can improve more on that.

#CSS Part

Look for the biggest file in the build its our css. the project doesn't have much styling and the css is too big to be that size. We could be wrong, but lets not guessing and try with google lighthouse. serve the build file in your local machine then right-click on the browser and look for **audit** tab
![audit lighthouse](https://i.ibb.co/PN5NDFn/audit.png)
run the audit and we will find reports about unused css and our current performance. 
![audit report performance](https://i.ibb.co/1JNpQvd/initial-performance.png)
![audit report css](https://i.ibb.co/JtyQZ1t/unusedcss.png)
this is because most of the css framework provide bunch of classes for all purpose use. but not all css class we use in our site, so we will need to select only we need. But how? are we suppose to copy/cut paste the class? NO WAY!! too tedious. We will use css-purge plugin to to that. what it does is looking into our build files and delete any unused css in our build files that still present in our css files. install `@fullhuman/postcss-purgecss` package to our project and update our `postcss.config.js` to the following:

```javascript
const autoprefixer = require('autoprefixer');
const tailwindcss = require('tailwindcss');
const postcssPurgecss = require(`@fullhuman/postcss-purgecss`);

const purgecss = postcssPurgecss({
    // Specify the paths to all of the template files in your project.
    content: [
        './public/**/*.html',
        './src/**/*.vue',
    ],
    // Include any special characters you're using in this regular expression.
    // See: https://tailwindcss.com/docs/controlling-file-size/#understanding-the-regex
    defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
    // Whitelist auto generated classes for transitions and router links.
    // From: https://github.com/ky-is/vue-cli-plugin-tailwind
    whitelistPatterns: [/-(leave|enter|appear)(|-(to|from|active))$/, /^(?!(|.*?:)cursor-move).+-move$/, /^router-link(|-exact)-active$/],
});

module.exports = {
    plugins: [
        tailwindcss,
        autoprefixer,
        ...process.env.NODE_ENV === 'production'
            ? [purgecss]
            : [],
    ],
};
```
basically the additions config is just to show purge-css where to look for css classes to keep (our index html and all *.vue files) and only enable this on production build enviroment. Ok lets try `npm run build` again to see the result
![after optimize css](https://i.ibb.co/ZSvXknp/css-optimized.png)
surprisingly, we only need the tip of the iceberg now down to 3.47 KiB !!

#JS Part
CSS part was easy just add few lines of config with plugin and we have our code optimized. But JS? we need to be more careful, taking up wrong piece of code could fail our apps. To do that, we need webpack-bundle-analyzer. install it by
```shell
npm i webpack-bundle-analyzer -D
```
then create vue.config.js with the following code:
```javascript
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

let plugins = [];
let optimization = {};

// comment line 6 to disable analyzer
plugins.push(new BundleAnalyzerPlugin());

module.exports = {
    lintOnSave: false,
    configureWebpack: {
        plugins,
        optimization,
    },
};
```
and run:
 
```shell
npm run build
```
http://locahost:8000 will pop up to our browser and show us overview of our bundle
![bundle analyzer](https://i.ibb.co/xmSMST3/analyzer.png)

actually analyzing this part is not a straight to method. Most likely based on experience and requirements whether we need to include some part of the code. In this case we can see moment and lodash took a big part in our bundle and looks like we don't need them that much. They are utilities that bundles all possible usecase they cover and bundle all the functions and module. Since we just only need findIndex for lodash we can change our code into

```typescript
import { Vue, Component } from "vue-property-decorator";
import UserResponse from "@/interfaces/UserResponse";
import User from "@/interfaces/User";
import axios from "axios";
import findIndex from "lodash/findIndex";

@Component
export default class Networkable extends Vue {
  users: User[];

  constructor() {
    super();
    this.users = [];
  }

  async getData() {
    const resp = await axios.get("https://reqres.in/api/users");
    const parsed: UserResponse = resp.data;
    this.users = parsed.data;
  }

  findEmma() {
    const index = findIndex(this.users, (u: User) => {
      return u.first_name == "Emma";
    });
    alert(`emma is at index ${index}`);
  }

  mounted() {
    this.getData();
  }
}
```
we import only function that we need.

secondly, take a look on moment package they took up a lot of space from their locale module. In this case we only need english locale, we can strip-down all those locale by updating our `vue.config.js`

```javascript
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const webpack = require('webpack');
let plugins = [];
let optimization = {};

// comment line 6 to disable analyzer
plugins.push(new BundleAnalyzerPlugin());

// ignore moment locale
plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/));

module.exports = {
    lintOnSave: false,
    configureWebpack: {
        plugins,
        optimization,
    },
};
```
moment is a good library but it don't have a good size and it's mutable (a lot of article explain this) for frontend when size matter i will suggest to take a look date-fns (https://date-fns.org/) over moment. Finally we can enhance a bit by removing axios and use fetch API provided by most browser. This step is really depending on your target browser when you have to support very very legacy browser (IE) then you should not do this. By the time this article written the fetch API has already supported in major browser you can see the details here https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

update our network request to be like this

```typescript
async getData() {
    const resp = await fetch("https://reqres.in/api/users").then(response => response.json());
    const parsed: UserResponse = resp;
    this.users = parsed.data;
  }
```
now lets see how our bundle looks
![final bundle](https://i.ibb.co/wQr9mrb/final-bundle.png)
![final build](https://i.ibb.co/5x2Rkmk/final-build.png)

and our final performance result
![final performance](https://i.ibb.co/8mBsmXX/final-performance.png)

we have reduced so much of the bundle size without breaking/changing any features. 

#Infra level optimization
All the steps we do above is on the codebase level, infrastructure level can give boost to performance too with compression when delivering our bundle usually with gzip or brotli compression. You can find more info here https://computingforgeeks.com/how-to-enable-gzip-brotli-compression-for-nginx-on-linux/

That's all from me on what I've learned along the way being frontend engineers. Let me know in the comments if you have more tips on how we improve the performance on our site

reference: 
- https://markus.oberlehner.net/blog/setting-up-tailwind-css-with-vue/
- https://computingforgeeks.com/how-to-enable-gzip-brotli-compression-for-nginx-on-linux/