How to create and minify Vuetify 3 Nuxt 3 project bundle
Tags: nuxt3, vuetify3, purgecss, bundle
Published: 2022-12-30 02:04:11

Sektions: [intro](#intro) | [what is vuetify](#what-is-vuetify) | [get started](#get-started) | [about optimization](#about-optimization-and-customization-of-vuetify-3) | [minimize bundle](#minimize-vuetify-3-bundle) | [conclusion](#conclusion)

_[@repo](https://github.com/CMQNordic/vuetify-3-nuxt-3-minimal-optimized-starter) | [Netlify live demo)](https://magical-peony-07c1dc.netlify.app/)._

### [**Intro**](#intro)

In this article I share my knowledge and thoughts about minifing production bundle in a **Nuxt 3 Vuetify 3** projects by purging unused code.  First part is about what Vuetify 3 is and how to add Vuetify to an existing Nuxt 3 project. Then I'll go into some optimization and minification of js, css and html with practical examples. 

_Feel free to check out the final [conslusion](#conclusion)._

### [**What is Vuetify**](#what-is-vuetify)

Vuetify is Vue component library providing pre-made components and **directives** to use in front-end projects. A directive is a reusable chunk of code that can be used within a html element. An example is `v-ripple` directive that animates background. For example `<button v-ripple>click me</button>` adds a background animation when it's pressed. Vuetify comes with many smaller **components** to be used as building blocks of layouts. I.e `<v-app>` (wrapping whole application and applying themes), or `<v-navigation-drawer>` that is a frame sliding in from the side often used as navigation menu on small devices. Some components i.e.`v-date-picker` or `v-data-table` are more complicated.  Designing and coding those components by ourselfs in Vue (or even worse vanilla javascript) would be a difficult and a time consuming task. Additionally Vutify 3 delivers css features similar to Tailwind. You can use utility classes to control the padding (_pa-5_), margin (_mb-10_), text color (_text-red-lighten-2_), background (_bg-orange_), font (_text-body-1_), elevation (_elevation-3_) and more. Many of them with mobile first variations for displays: `xs` (_< 600px_), `sm` (_600px > < 960px_), `md` (_960px > < 1264px_) and `lg` (_1264px > < 1904px_). For example class `text-md-body-1` applies `body-1` font for medium size displays and up.

Vuetify tegether with Nuxt gained a lot of popularity as they significantly speed up building Vue based applications and prototyping. You can build your whole UI with those predefined components and directives, or just add cherry-picked ones to your project as widgets.

**The main concerns** when adding a library like Vuetify to your project is how it affects the size of code delivered from server to then client (a.k.a the **final bundle**).

### [**Get started**](#get-started)

_[@repo](https://github.com/CMQNordic/vuetify-3-nuxt-3-minimal-optimized-starter) | [Netlify live demo (generate)](https://magical-peony-07c1dc.netlify.app/)._

► Use following simple step to create **new Vuetify 3** project on top of **Vue 3**. 
_Official instructions [here](https://next.vuetifyjs.com/en/getting-started/installation/#installation)._
```bash
npm create vuetify 
```

► Use following 4 steps to **add Vuetify 3** to an **existing Nuxt 3** project: 

**1.** First (if needed) create a new Nuxt 3 project. _Official instructions [here](https://nuxt.com/docs/getting-started/installation#new-project)._
```bash
npx nuxi init  nuxt-3-vuetify-3-minimal-project-starter
```

**2.** Add Vuetify 3 to existing project. _Official instructions [here](https://next.vuetifyjs.com/en/getting-started/installation/#manual-steps)._
```javascript
npm add vuetify@next

// additionally scraffold new /plugins/vuetify.ts file 
npx nuxi add plugin vuetify 
```

**3** Utilize Vuetify by adding following to `plugins/vuetify.ts`
```javascript
import { createVuetify } from 'vuetify';
import 'vuetify/styles'; // pre-build css styles

/* Add all components and directives, for dev & prototyping only. */
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';

/* Add build-in icon used internally in various components */
/* Described in https://next.vuetifyjs.com/en/features/icon-fonts/ */
import { mdi, aliases as allAliases } from 'vuetify/iconsets/mdi-svg';
const aliases = allAliases;

export default defineNuxtPlugin((nuxtApp) => {
  
  const vuetify = createVuetify({
    components,
    directives,
    icons: {
      defaultSet: 'mdi',
      aliases,
      sets: { mdi }
    }
  });

  nuxtApp.vueApp.use(vuetify);

  if (!process.server) console.log('❤️ Initialized Vuetify 3', vuetify);
});
```

**4** Instruct Nuxt 3 to transpile Vuetify when building by modifying file `nuxt.config.ts`
```javascript
export default defineNuxtConfig({  
    build: {
        transpile: ['vuetify'],
    }
});
```

**Finally** as an additional step in order to have something visual to test with please add following code (basic vuetify components and styling) to `app.vue` file.
```html
<template>
<div>
  <v-app>
    <v-navigation-drawer temporary v-model="showDrawer" location="right">
      <div class="w-100 text-right">
        <v-btn flat icon="$close" size="x-large" @click="toggle" />
      </div>
      <p class="text-center"><b>I'm the drawer</b></p>
    </v-navigation-drawer>

    <v-app-bar>
      <v-app-bar-nav-icon @click="toggle" />
      <v-toolbar-title>I'm the header</v-toolbar-title>
    </v-app-bar>

    <v-main class="d-flex align-center">
      <v-container>
        <v-sheet elevation="5" class="py-4 text-center">
          <h1 class="text-h5">Built and styled with Vuetify 3</h1>
          <p>Minimized production bundle (70kB in tot)</p>
          <v-btn class="mt-4">press me to ripple</v-btn>
        </v-sheet>
      </v-container>
    </v-main>

    <v-footer app elevation="5">
      <v-row no-gutters justify="center" 
             class="text-overline font-weight-black">
        <p class="my-auto">I'm the footer</p>
        <v-spacer />
<p class="my-auto">See how I was built</p>
        <v-btn icon variant="plain">
          <v-icon icon="$info" color="green-accent-4" />
        </v-btn>
      </v-row>
    </v-footer>
  </v-app>
</div>
</template>

<script setup>
  const showDrawer = ref(false);
  
  const toggle= () => {
		console.log('usedToggle()');
		showDrawer.value = !showDrawer.value;
	};

  /* Redundant unused code to test treeshaking */
  const unusedFunction = () => console.log('unusedFunction');
</script>

<style>
  /* Redundant unused code to test treeshaking */
  .unused-selector-app {
    color: orange;
  }
</style>
```
Now run `npm run dev` and verify the page looks like in the live demo [here](https://magical-peony-07c1dc.netlify.app/).

Now run `npm run generate` followed by `npm run preview`. _Because we have SSR set to true by default, Nuxt 3 will generate a static project in `.output/public/`._
 
🩻**Analazing generated bundle:**
`.output/public/index.html`  14kB (3kB gzipped)
`.output/public/_nuxt/enter.xxx.js`  408kB  (125kB gzipped)
`.output/public/_nuxt/enter.xxx.css` 371kB (51kB gzipped)

The production bundle is **"huge"** for the simplicity of this project 👎

Read further why and what we can do about it.

### [**About optimization and customization of Vuetify 3**](#about-optimization-and-customization-of-vuetify-3)

In initial setup, in file `vuetify.ts`, we add ALL components and directives, import ALL pre-build css style selectors and ALL default icon aliases. It is ok for prototyping but it's not very optimal for production as generated bundle contains too much unused code.

Becouse we here for test purposes impoement whole site in one single file `app.vue` (without any routes) then our statically generated bundle consists of 3 main, easy to analyze parts:

- `.output/public/index.html` (refered to as **html bundle**)

- `.output/public/_nuxt/enter.xxx.js` (refered to as **js bundle**)

- `.output/public/_nuxt/enter.xxx.css` (refered to as **css bundle**)

**What happens to unused js code?**
Search js bundle for phrase: _'unusedFunction'_  -> not found 👍
Search js bundle for phrase: _'toggle'_ -> found 👍
Both of those are defined in the bottom of `app.vue`, one is used the other not. We verify here that Vite by default removes all unused imported or defined javascript when bundling. That's good. 👍  

**What happens to unused icons aliases?**
Search js bundle for phrase: _'mdi-info'_  -> found 👍
Search js bundle for phrase: _'mdi-minus'_ -> found 👎
We find out that all icons are found despite the fact we use only icon aliases: menu ("hamburger" used internally by `<v-app-bar-nav-icon />`), close_ and info icons. That's less good. 👎

**What happens to unused css selectors?**
Search css bundle for phrase: _'unused-selector-app'_  -> found 👎
Search css bundle for phrase: _'bg-red-lighten-4'_  -> found 👎
Search css bundle for phrase: _'my-sm-9'_ -> found 👎
None of the selecors above (first one defined by us in `app.vue` and two other ones added by vuetify) is used nowhere but in the bundle we find ALL selectors from imported `vuetify/styles` and everything from `<styles>` part of `app.vue` file. This despite the fact that our simple project only use some few of those. 👎

> As on dec 2022 Nuxt staticaly generates bundle (npm run generate) and injects `<styles>` part from `app.vue` (scoped or not) into **html head** for **each** generated page. Keep this in mind when you introduce lots of global styles in `app.vue` file, as all those styles will be part of html code for each requested page.

It must be mentioned that removal of unused css is a major issue in many projects and a topic of many discussions. There does not exist any perfect tool for it at the writing moment. In order not to remove too much we must have knowledge of how removal works and we must have good knowledge about the code whe want to purge. 

**Most important to remember** here is that there  might me a lot of unused css code both in html head and subsequently loaded css files. 

So.. how do we securly remove all this unused code? Keep on reading.

Vuetify 3 provides functionality called **treeshaking** that removes unused components from final bundle, or more correctly adds only used ones. You can read official documentation [here](https://next.vuetifyjs.com/en/features/treeshaking/#treeshaking). Unfortunately this treeshaking  optimization aim for javascript bundle. It **does not remove** all unused **css selectors**!

At writing moment I found a plugin named **nuxt-purgecss** to be the best option (still not optimal) to remove unused CSS code from final build bundle.

_I recommend to read this good article on the topic - [How Do You Remove Unused CSS From a Site?](https://css-tricks.com/how-do-you-remove-unused-css-from-a-site/)._

**Finally shortly about customization of Vuetify 3 project**
Vuetify 3 uses SASS to craft all styles for the framework. By default it delivers pre-compiled CSS for all styles, including themes `light` and `dark`. Sometimes it's desired to re-build to manipulate the sass variables to get a feel-and-look for our needs. Some variables in vuetify are used for general styles and themes but unfortunately others are nested and used inside the components (aka. component specific variables).

>  We can customize many SASS variables in Vuetify 3 and rebuild framework styles at build time. Read more about how to do it [here](https://next.vuetifyjs.com/en/features/sass-variables/). By my opinion if possible this **deep variable customisation** should be **avoided**. I do not recommend it as it works right now. There are other more simple and faster ways to overwrite themes, colors, backgrounds, font styles for body and heading etc.

> Note! Enabling **deep variable customisation** will have major impact on dev and build strt-up duratin and might slow down the development significantly if your project uses more than some few components.

_Customization of icons, fonts, styles and themes is a big topic on its own. You can give it a read in my other article just about this topic - [How to customize icons, fonts, styles and themes in Vuetify 3 project](https://github.com/CMQNordic/vuetify-3-nuxt-3-minimal-optimized-starter)._
 
### [**Minimize Vuetify 3 bundle**](#minimize-vuetify-3-bundle)

Let's get our hands dirty.

#### 1. [Vuetify treeshaking](#vuetify-treeshaking)
To auto inculde only used vuetify components and directives (a.k.a [automatic treeshaking](https://next.vuetifyjs.com/en/features/treeshaking/#treeshaking)) we must install required dependency.  
```
npm i -D vite-plugin-vuetify
```

Then we need to modify following in our project:

Remove imports of ALL components and directives from `plugins/vuetify.ts`
```javascript
 ...

/* Add all components and directives, test & prototyping only. */
// REMOVE! import * as components from 'vuetify/components';
// REMOVE! import * as directives from 'vuetify/directives';

...

export default defineNuxtPlugin((nuxtApp) => {
  
  const vuetify = createVuetify({
      // REMOVE! components,  
      // REMOVE! directives,   
      icons: {
          defaultSet: 'mdi',
          aliases,
          sets: { mdi }
      }
  });
  
  ...
});
```   

Add treeshaking istructions for Vite in `nuxt.config.ts`   
```javascript

/* ADD! */
import vuetify from 'vite-plugin-vuetify';

export default defineNuxtConfig({
  ...

  /* ADD! */
  modules: [
    /* Treeshaking: https://next.vuetifyjs.com/en/features/treeshaking/ */
    async (options, nuxt) => {
        nuxt.hooks.hook('vite:extendConfig', (config) => {
          config?.plugins?.push(vuetify());
       });
    }
  ],

  ...
});
```

Thats all. When running the project with `npm run generate` followed by `npm run preview` you will generate static files in `.output/public/` folder. 


🩻**Analazing generated bundle after treeshaking:**
`.output/public/index.html`  14kB (3kB gzipped)
`.output/public/_nuxt/enter.xxx.js` 184kB (67kB gzipped) 
`.output/public/_nuxt/enter.xxx.css` 277kB  (34kB gzipped)

Our bundle is now **smaller** as js bundle was before 408kB (125kB gzipped). Treeshaking did a good job. 👍

#### 2. [Theme and icons Optimization](#theme-and-icons-optimization)
Ee only use 3 icons aliases: _menu_, _close_ and _info_. Loading only the used ones will minimize internal vuetify object in browser (on heap). Have this in mind when you later add more external icons to your project.

**Add only used icons** by changing in `plugins/vuetify.ts`
```javascript
...
import { mdi, aliases as allAliases } from 'vuetify/iconsets/mdi-svg';
/* REMOVE */ // const aliases = allAliases; 

/* ADD */ 
const aliases = {
	/* Only used icon aliases here */
	menu: allAliases.menu,
	close: allAliases.close,
	info: allAliases.info,
};

...
const vuetify = createVuetify({
  icons: {
      defaultSet: 'mdi',
      aliases,
      sets: { mdi },
    },
  }); 
```
Generally we shall always add only used icons to the project and now whole libraries..

What about themes? 
When we analyze the created html bundle notice a large section `<style id="vuetify-theme-stylesheet">`  in the html head. Read more about it in official documentation [here](https://next.vuetifyjs.com/en/features/theme/#implementation). It contains whole configuration for _v-theme--light_, _v-theme--dark_ and lots of generated selectors for each color configuration.

We did not specify any theme to use but by default theme _light_ was applied to everything within the `v-app` block. Therfore in generated `index.html` we can see that many elements use selectors `v-theme--light`. But what if we disable themes in Vuetify totally? Will this section with unnecessary unused theme selectors and variables disapear? Lets try..

We can try to **disable themes** as described [here](https://next.vuetifyjs.com/en/features/theme/#disable-theme) in `plugins/vuetify.ts`: 

```javascript
export default createVuetify({
  /* ADD */ theme: false, 
  ...
})
```

After re-generating no theme selectors are applied anymore on any html elements, good, disabling of themes worked as expected. But... the `<style id="vuetify-theme-stylesheet">` section with unused selectors still remains as before in the head of generated index.html file. 👎

Consider this a bug? Why does Vuetify add thise "disabled" code? Is there any way to remove all this redundand code, or only enable `light` theme and not `dark` to reduce amout of added code in html head? _To be updated when I know, but feel free to comment this in comments!_

##### 3. [Clean out unused CSS](#clean-out-unused-css)


Purge-css is a "dummy" tool. It scans files (html, vue, js ) on locations that we provide to it, and creates a list with selectors it marks as used. Then it analyzes the final generated css code created by our bundler and removes all selectors from it that are not part of its "found-used-selectors" list. For example: In file x.html with `<p class="bg-red">` it will find `bg-red`. In file y.vue with `<v-btn class="elevation-6" elevation='1'>` it find `elevation-6` but NOT `elevation-1` becouse the latter is created with help of a prop. Subsequently `bg-red` and `elevation-6` will not be removed, while `elevation-1` will not exist in the final bundle!

It is essential to instruct purgecss to look in all locations of project where source code used by bundler is located. For Nuxt framework it is predefined by the nuxt-purgecss plugin (default nuxt-purgecss settings [here](https://github.com/Developmint/nuxt-purgecss/blob/main/src/config.ts)) but it becomes tricky with third party librarys like Vuetify where the components and selectors are created by render functions defined deep down in `node_modules/vuetify` folder and some classes might even sometimes be rendered at run time in client. Additionally Vuetify 3 generates some of its its style selectors based on props whic does not help us here.

We need to configure **purgecss** to be sure it **only remove** unused code. So how can we handle it?

I like using Vuetify with Nuxt and it is often for the possibilty of **following little trick**.

First **build a static site** as nuxt provides this in an easy way (css: true + npm run generate). Then ALL html shall be generated with all classes and selectors in `dist` folder.

After first build **provide the content** of the `dist` folder (**copy of it**) to purgecss as a input option "content: <copy-of-dist-path>" and do your final production build. Then purge will have all class names and selector it needs to keep.

First install the nuxt-purgecss plugin 
```bash
npm i -D nuxt-purgecss 
```

Add purgecss module and with settings in `nuxt.config.ts`:
```javascript
...
export default defineNuxtConfig({
  modules: [
    ...

    /* ADD */ 
    /* Remove unused CSS */
    [
      'nuxt-purgecss',
      {
        content: [
          /* Copy of 'dist' from first npm run generate */
          'modules/purgecss/static-generated-html/**/*.html',
        ],
        greedy: [
          /* Generated as runtime, keep all related selectors */
          /v-ripple/,
        ],
      },
    ]
  ]
  
  ...
});
```

After all those modifications run now 'npm run generate', copy content of 'dist' to new folder 'modules/purgecss/static-generated-html', run 'npm run generate' again followed by 'npm run preview'. 
This generate static files in '.output/public/' folder. 

🩻**Analazing generated file after treeshaking and csspurge:**
`.output/public/index.html`  14kB (3kB gzipped)
`.output/public/_nuxt/enter.xxx.js`  184kB  (67kB gzipped)
`.output/public/_nuxt/enter.xxx.css` 14kB  (3.4kB gzipped)

The css is much much smaller than from beggining.👍 
We started with css bundle of **371kB** and ended up with **14kB** (unzipped). Thats a pretty awsome reduction. 😀

### [**CONCLUSION**](#conclusion)
We created an educational project starter using Vuetify 3 on top of Nuxt 3. We did some optimization to remove unused code from js,css and html in the production bundle. Them we deployed the project.

_[@repo](https://github.com/CMQNordic/vuetify-3-nuxt-3-minimal-optimized-starter) | [Netlify live demo (generate)](https://magical-peony-07c1dc.netlify.app/)._

BEFORE OPTIMIZATION
`.output/public/index.html`  14kB (3kB gzipped)
`.output/public/_nuxt/enter.xxx.js`  408kB  (125kB gzipped)
`.output/public/_nuxt/enter.xxx.css` 371kB (51kB gzipped)

AFTER OPTIMIZATION
`.output/public/index.html`  14kB (3kB gzipped)
`.output/public/_nuxt/enter.xxx.js`  184kB  (67kB gzipped)
`.output/public/_nuxt/enter.xxx.css` 14kB  (3.4kB gzipped)

IWe reduced the totla bundle size approx **795kB** to **212kB** (zipped from **179kB** to **73kB**). Around 50kb of the zipped size is pure default and always-included Vue+Nuxt base functionality!
Total reduction of almost 80%.

> I know that index.html contains some more unused theme code. At writing moment I have no solution for that but will update here asap I know.

Additionaly we learned that when building site statically with nuxt and adding styles in `app.vue` file, then those styles are always added to head section of all generted html pages. This is something to have in mind when adding styles there and build static site.

Do you have any comment what more can be done to minimize bundle for this starter even more?

I really hope you did learn something usefull in this article.

✌️