Optimizing Frontend Builds with Vite

Discussing optimization of frontend code using vite for bundling

Vite: The Next Generation Frontend Build Tool

Vite in french means fast which fits this tool just right. As Vite is 10 to 100 times faster than traditional build tools written in javascript itself like webpack, parcel, rollup. Vite under the hood uses "esbuild" which is a bundler which os written in go, hence the performance.

Scope

In this blog post, we will be optimizing the frontend code by doing following.

  1. Minifying all files (HTML, CSS/SCSS, JS).
  2. Adding relevant syntax transforms and polyfills to support old browsers. If your HTML has SVG embedded minimize SVG separately using tools like SVGO, which is not covered in this post.

Getting Started

First of all, Make sure you have nodejs (version: 14.18+ or 16+) installed. After that you can create a frontend project using using vite by following -

  1. Navigate to directory in which you want to create project folder and run the following command.
npm create vite@latest
  1. You can select the type of frontend technology you want to use in your app from many options when prompted. But for this blog only select "vanilla-js" project template only. As optimizations are only covered for vanilla js, HTML and CSS/SCSS.
  2. Enter the project folder's name when prompted and project will be created.
  3. Open folder in vs code.

Minifying HTML

We reduce the size of the HTML file by removing whitespaces and line breaks. It will minify the final HTML but render it almost unreadable by lack of indentation, which is fine for production.

npm install -D vite-plugin-minify

Vite will only minify the HTML file that is being bundled (as it should). So, if you have multiple HTML files in the project, involve them as entry points in vite config as below.

// vite.config.js
import { resolve } from 'path'
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        file1: resolve(__dirname, 'index.html'),
        file2: resolve(__dirname, 'nested/index.html'),
        file3: resolve(__dirname, '3rdfile.html')
      }
    }
  }
})

You can find out more about multi-page apps and vite config here. We also need to add this plugin, as below, in vite.config.js to be called during the build.

// vite.config.js
import { resolve } from 'path'
import { defineConfig } from 'vite'
import { ViteMinifyPlugin } from 'vite-plugin-minify' 

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        file1: resolve(__dirname, 'index.html'),
        file2: resolve(__dirname, 'nested/index.html'),
        file3: resolve(__dirname, '3rdfile.html')
      }
    }
  },
  plugins: [
    ViteMinifyPlugin({}),
  ]
})

Minifying SCSS

If you are using CSS it will be minified by vite automatically just make sure it is included in HTML using the tag as below.

<link rel="stylesheet" href="link/to/file.css" />

If you are using some CSS preprocessor, in our case, SCSS. Need to add it as a dev dependency.

npm install -D sass

But it is the same for other preprocessors too as you can see here. The final CSS produced will also be minified automatically during the build, just need to include it via the link tag in the HTML file as below.

<link rel="stylesheet" href="link/to/file.scss" />

Minifying Javascript

Javascript minification and mangling will be done by vite by using esbuild. You can check build.minfy for more info.

Vite for Browsers not supporting native ES Modules

But vite won't transform your javascript code if you are targeting ES6 incompatible/old browsers. Also, More importantly, vite uses dynamic import of ES modules to load javascript code, which is only supported by modern browsers. So, to resolve two of these issues as mentioned above, vite team has provided us with a plugin named vite-plugin-legacy. You can read more about it on GitHub. Make sure to add it as a dependency (as it needs to be included with the final code) -

npm install vite-plugin-legacy

We also need to edit vite.config.js to use vite-plugin-legacy during the build.

import legacy from '@vitejs/plugin-legacy'
import { resolve } from 'path'
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        file1: resolve(__dirname, 'index.html'),
        file2: resolve(__dirname, 'nested/index.html'),
        file3: resolve(__dirname, '3rdfile.html')
      }
    }
  },
  plugins: [
    ViteMinifyPlugin({}),
    legacy({
      targets: ['defaults', 'not IE 11']
    })
  ]
})

Here target property takes the browser list compatible query, it is optional, and if not set takes the value as 'defaults' which is recommended by browserslist.

Polyfilling and Syntax Transforms

Vite allows developers to use latest JS or ECMAScript features. But these features may not be supported by all browsers. So, syntax transforms and polyfilling of JS features itself is important. Vite uses @babel/preset-env to perform syntax transforms during build stage based on the target option specified in vite config.

For polyfilling core Ecmascript features such as Promises, symbols etc this plugin uses @babel/preset-env's useBuiltIns: 'usage' option which uses core-js. Both syntax transforms and polyfilling (of ECMA script features only) happens by default (you can read more about this here). But certain DOM and Browser APIs such as fetch API, are not polyfilled because they're not supported by core-js. So, support for these APIs needs to be added manually by including polyfill code right in the source code or by using a service like polyfill.io.

For example, if we use IntersectionObserver API in my project's code, this API is supported by all modern browsers but not by legacy browsers. So, polyfill code is required when we are targeting those legacy browsers too. Polyfill code for older browsers, that don't support this API, can be added in two ways. First method is to use polyfill.io which is cdn that sends back polyfill code to browser by sensing the needs of browser accordingly. Second method is to include polyfill code directly in the source code. Polyfill code for almost every HTML5 API can be found here.

Also, make sure of 2 things -

  1. The polyfill code is executed, conditionally, only when that API is undefined in the browser, else we end up compromising the performance that the browser's native API brings. So, continuing the example of IntersectionObserver API conditionally rendered polyfill code will look like below.

     if ("IntersectionObserver" in window) {    
     // polyfill code for IntersectionObserver 
     }
    
  2. To avoid unnecessary errors make sure all polyfill code is loaded first and then the remaining source code starts executing. So, using defer attribute accordingly on the script tag can be useful.

End Remarks

While vite is genuinely blazingly fast in its bundling process. I ran into many optimization and compatibility problems while making projects with vite and hoped that somebody had a blog somewhere about it, discussing solutions. Hope You find these tips helpful. I would love to know your thoughts and feel free to share and tag me if you find better solutions.