Reduce Nuxt UI CSS by half with experimental component detection
Overview
Nuxt UI already tree shakes component code so that components you don't use never end up in the JavaScript bundle. However, this was not true for the CSS. Every component in the library ships a theme file full of Tailwind classes, and those classes all got compiled into the stylesheet whether you used the component or not.
Nuxt UI 4 added an experimental option that fixes this. It scans your source for U* components, works out their internal dependencies, and only feeds those theme files to Tailwind.
On this Bordermedia site, the main stylesheet went from 247 kB down to 118 kB. Gzipped, that is 33 kB down to 18 kB, and entry.css is render blocking, so greatly affects perceived load time and things like your Google Lighthouse scores.
Enabling it
Super simple, one option in nuxt.config.ts:
export default defineNuxtConfig({
modules: ["@nuxt/ui"],
ui: {
experimental: {
componentDetection: true,
},
},
});
Restart the dev server and you should see the detection result in the build log:
✔ Nuxt UI detected 9 components in use (including dependencies)
Nine in my case. This site only uses UButton, UFormField, UInput and UTextarea. The other five are internal dependencies that Nuxt UI pulled in on its own.
As of Nuxt UI 4.11.0, the same option is now available for the vite plugin. So if you are running Nuxt UI in a plain Vue and Vite app, it goes in the ui() plugin options in vite.config.ts instead.
Note: Pass an array of component names if there are Nuxt UI components that cannot be detected. For example:
componentDetection: ['Modal', 'DropdownMenu', 'Popover']. See below for further details.
Measuring the difference
The CSS size saving will depend entirely on how much of the library you use. A dashboard using half of Nuxt UI will save far less than a marketing site that is using only four components.
Build once with the option off:
npm run build
ls -l .output/public/_nuxt/entry.*.css
Then turn it on, build again, and compare.
Here is what changed on my Bordermedia site for entry.css.
| Build | Raw | Gzip |
|---|---|---|
| Detection off | 252,963 B | 33,190 B |
| Detection on | 120,818 B | 17,978 B |
| Saved | 132,145 B | 15,212 B |
Gotchas
Detection runs when the dev server starts
New components are picked up on the next dev server start. So if you add a new component to your code mid session then its styles may not be there. It does log Nuxt UI detected new components: when it catches one during a rebuild, but if something looks unstyled, trying a restart should fix the problem.
Only the app folder gets scanned, not Markdown
In a Nuxt project the scan covers the app/ directories and looks at .vue, .ts, .js and .tsx files. Markdown is not scanned. If you use Nuxt UI components inside content/*.md through MDC, they will not be detected.
Prose components are safe. Nuxt UI adds the whole prose theme automatically when it sees @nuxt/content or @nuxtjs/mdc in your project, so <ContentRenderer> output keeps its styling.
Dynamic components need to be listed
The scan is a regex looking for UButton and <u-button> style names in your source. It cannot see a component whose name is built at runtime, so <component :is="...">, anything opened through useOverlay(), and toasts from useToast() will not be found.
Pass an array of components to enforce that the CSS is included:
ui: {
experimental: {
componentDetection: ["Modal", "DropdownMenu", "Toast"],
},
},
The array adds to automatic detection rather than replacing it, and Nuxt UI warns you at build time if you list a name it does not recognise.
Documentation
The option is documented on both installation pages: