Everything before this page was about an app. This page is about the thing you are actually being hired to build: a shared package whose cost is paid by every app that installs it, including the apps that use almost none of it. That asymmetry is the whole argument for budgets.
$foo from a plugin only when the app genuinely needs app-wide setup..client.ts plugins, import.meta.client guards, addComponent({ mode: 'client' }), addServerImports for server-only utilities. Each one keeps code out of a bundle where it cannot be used.sideEffects: false only when it is true, so bundlers can drop unused exports. Avoid CommonJS dependencies; when unavoidable, add them to vite.optimizeDeps.include so consumers do not hit the reload loop.runtimeConfig.public tiny. It is serialised into every SSR response of every consuming app. Large or derived data belongs in a build-time template that is tree-shaken.global: true components and no unnecessary global CSS. Both land in the entry chunk. A design-system layer registers components normally, with a prefix, and lets per-page splitting do its job.buildDir, and does once-only work in hooks.debug option that logs your setup timings through useLogger, so a consumer investigating a slow build can see your contribution rather than guess.size-limit or a nuxt analyze JSON diff) and the build-time delta (hyperfine). Fail the build when either regresses beyond a threshold you chose deliberately.Two fixtures and one script give you both numbers:
test/fixtures/
├─ empty/ # a bare Nuxt app
└─ basic/ # the same app plus `extends: ['../../..']` (or the module installed)
{
"size-limit": [
{
"name": "client bundle (fixture with the layer)",
"path": "test/fixtures/basic/.output/public/_nuxt/*.js",
"limit": "220 kB"
}
],
"scripts": {
"budget": "nuxt build test/fixtures/basic && size-limit"
}
}
name: budget
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install
- run: pnpm budget
- name: Build time with and without the layer
run: |
npx hyperfine --warmup 1 --runs 3 \
'pnpm nuxt build test/fixtures/empty' \
'pnpm nuxt build test/fixtures/basic'
A debug option that makes your cost visible instead of mysterious:
export default defineNuxtModule<ModuleOptions>({
meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },
defaults: { debug: false },
setup(options, nuxt) {
const logger = useLogger('toolkit')
if (!options.debug) return setupToolkit(options, nuxt)
const started = performance.now()
const result = setupToolkit(options, nuxt)
nuxt.hook('build:done', () => logger.info(`toolkit setup: ${(performance.now() - started).toFixed(0)}ms`))
return result
},
})
An opt-in feature that still imports its library at module scope, or registers its plugin unconditionally, costs every consumer. Register plugins, components and CSS inside the branch that the option enables, and verify with nuxt analyze on a fixture that leaves the option off.
The current name and default of the CSS inlining feature (features.inlineStyles) before quoting it as a lever; it did not appear in the config reference I checked.
empty and basic fixtures, build both, and record the entry-chunk delta and the build-time delta. Put both numbers in your layer's README.size-limit with a threshold five per cent above today's number and watch a deliberate regression fail CI.global: true, re-measure, and then remove it. Note the delta; it is a good interview anecdote."By treating its cost as a number rather than an intention. I measure what an empty fixture app costs with and without the package: client bundle delta and build-time delta. Then I keep both in CI as budgets so a regression fails a pull request. The design rules follow from that: tree-shakeable composables instead of plugins, client and server code split properly, no global components or global CSS unless a consumer asks for them, a tiny public runtime config, opt-in features that register nothing when they are off, and a debug option that prints my setup timings so a consumer can see my share of their build."
Client-side performance
Code splitting, lazy hydration, images and fonts, third-party scripts, and the Core Web Vitals each of them moves.
Senior craft
What changes when the thing you build is used by other teams rather than by end users - API design you cannot easily take back, release discipline, security, and review.