DEV Community

Martin Czerwinski
Martin Czerwinski

Posted on • Edited on

How to create and minify Vuetify 3 Nuxt 3 project bundle

Sektions: intro | what is vuetify | get started | about optimization | minimize bundle | conclusion

@repo | Netlify live demo).

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.

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

@repo | Netlify live demo (generate).

► Use following simple step to create new Vuetify 3 project on top of Vue 3.
Official instructions here.

npm create vuetify 
Enter fullscreen mode Exit fullscreen mode

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

npx nuxi init  nuxt-3-vuetify-3-minimal-project-starter
Enter fullscreen mode Exit fullscreen mode

2. Add Vuetify 3 to existing project. Official instructions here.

npm add vuetify@next

// additionally scraffold new /plugins/vuetify.ts file 
npx nuxi add plugin vuetify 
Enter fullscreen mode Exit fullscreen mode

3 Utilize Vuetify by adding following to plugins/vuetify.ts

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);
});
Enter fullscreen mode Exit fullscreen mode

4 Instruct Nuxt 3 to transpile Vuetify when building by modifying file nuxt.config.ts

export default defineNuxtConfig({  
    build: {
        transpile: ['vuetify'],
    }
});
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

Now run npm run dev and verify the page looks like in the live demo here.

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

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

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

Minimize Vuetify 3 bundle

Let's get our hands dirty.

1. Vuetify treeshaking

To auto inculde only used vuetify components and directives (a.k.a automatic treeshaking) we must install required dependency.

npm i -D vite-plugin-vuetify
Enter fullscreen mode Exit fullscreen mode

Then we need to modify following in our project:

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

 ...

/* 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 }
      }
  });

  ...
});
Enter fullscreen mode Exit fullscreen mode

Add treeshaking istructions for Vite in nuxt.config.ts


/* 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());
       });
    }
  ],

  ...
});
Enter fullscreen mode Exit fullscreen mode

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

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

...
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 },
    },
  }); 
Enter fullscreen mode Exit fullscreen mode

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. 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 in plugins/vuetify.ts:

export default createVuetify({
  /* ADD */ theme: false, 
  ...
})
Enter fullscreen mode Exit fullscreen mode

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

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) 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: " 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

npm i -D nuxt-purgecss 
Enter fullscreen mode Exit fullscreen mode

Add purgecss module and with settings in nuxt.config.ts:

...
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/,
        ],
      },
    ]
  ]

  ...
});
Enter fullscreen mode Exit fullscreen mode

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

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 | Netlify live demo (generate).

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.

✌️

Top comments (2)

Collapse
 
amalhao profile image
Alexandre Malhão • Edited

Good article! Please update if you find new enhancements.

Edit: One thing I noticed is that if I do the step 3 the buttons behave a bit strange, and lose the ripple effect. I assume we're deleting something necessary for it do function properly.

Collapse
 
frederikheld profile image
Frederik Held • Edited

Thanks for the great write up. But I'm a bit shocked that Nuxt doesn't take care of this. The more research I do, the less happy I am that I have chosen Nuxt for SSG :-P I should have stuck with Jekyll ...