skip to content

Why doesn't a Vue 3 production bundle include Transition or KeepAlive code when the app never uses them, unlike Vue 2's global API?

level: middleimportance: should knowfreq 40%

answer

  1. named exports, not one object
  2. compiler imports what templates use
  3. bundler drops unreferenced exports
  4. global registration and CDN builds defeat it

basics

~20 s

Vue 3 exposes its APIs and built-ins as named ES module exports, and the template compiler imports only the helpers a template uses. A bundler then drops unreferenced exports such as Transition, whereas Vue 2 hung everything on one global Vue object.

solid answer

~40 s

In Vue 2, APIs such as `Vue.nextTick` lived on a single global `Vue` object, so a bundler could not prove any of them unused and shipped them all. Vue 3 exports each API and internal helper as a **named ES module export** — `nextTick`, `Transition`, `KeepAlive`, `vShow`, `withDirectives`, the `v-model` helpers. The template compiler generates render code that **imports only what the template uses**: a `<Transition>` in a template becomes an import of `Transition`, and a template without one never references it. A bundler with tree shaking then removes every export nothing imports. It depends on using the ES module bundler build of Vue with a build step; the browser global build carries the whole runtime, and registering components globally keeps them in the bundle even if unused.

go deeper

for a junior

Recall that Vue 3 only ships the built-ins you use when you bundle it, because its APIs are named imports.

for a middle

Explain how the template compiler emits imports only for used helpers, and why Vue 2's global object prevented tree shaking.

for a senior

Find what keeps unused code reachable in a real bundle — global plugin registration, the full build, wholesale imports — and fix it at the source.

for a principal

Set library-adoption rules around tree-shakable imports and local registration, and keep bundle checks in the release process.

## The Vue 2 problem In Vue 2, the public API was attached to one object. You wrote `Vue.nextTick(...)`, `Vue.set(...)`, `Vue.observable(...)`. A bundler performing **tree shaking** — removing exports nothing imports — can only remove what it can prove unused, and a property on an object that the app imports as a whole is never provably unused. So every Vue 2 app shipped the whole global API, whether it called it or not. ## What Vue 3 changed Vue 3 restructured the package around **named exports**: - Public APIs: `import { nextTick, reactive, computed } from 'vue'`. - Built-in components: `Transition`, `TransitionGroup`, `KeepAlive`, `Teleport`, `Suspense`. - Directive runtimes: `vShow`, `vModelText`, `vModelCheckbox` and the other `v-model` helpers. - Render helpers: `withDirectives`, `createElementBlock` and the rest of the compiler's output vocabulary. The **template compiler** is the other half. A template like ```html <Transition> <div v-show="ok">hello</div> </Transition> ``` compiles to render code that imports `Transition`, `withDirectives` and `vShow` from `vue`. A template without a `<Transition>` compiles to code that never mentions it. After compilation, the only references to Vue's internals are the ones your templates and scripts actually need, and the bundler can delete the rest. ## What you get, and what you do not | Situation | Is unused code removed? | |---|---| | SFCs compiled at build time, ES module bundler build of Vue | Yes — unused built-ins and APIs are dropped | | Templates compiled in the browser (full build) | The compiler itself ships; the Vue docs put it at about 14 kB min+gzip | | Vue loaded as the browser global build from a script tag | No — the global build is one prebuilt file with everything | | Components registered globally with `app.component` | No — the registration references them, so they stay even if unused | | Options API support in a Composition-only app | Only if the `__VUE_OPTIONS_API__` flag is set to `false` | ## Levers that follow from this 1. **Use a build step** with the ES module bundler build so templates are precompiled and exports can be dropped. 2. **Register components locally** — import them where they are used — instead of registering a whole UI library globally. The Vue docs note that global registration prevents unused components from being removed. 3. **Import only what you use** from component libraries that offer per-component imports. 4. **Configure the compile-time flags** so features you do not use, such as Options API support, can be removed. 5. **Measure the output**: check the production build for which chunks contain `Transition`, `KeepAlive` or a library's full component set. ## Why it matters in practice - Built-ins cost only when used, so adding features to Vue's core does not tax apps that ignore them. - The biggest avoidable weight in real apps is rarely Vue itself; it is globally registered libraries, full-build template compilation, and dependencies imported wholesale. - Tree shaking is a property of the **bundler and module format**; Vue 3 is designed to cooperate with it, but it cannot fix a bundle whose imports keep everything reachable. ## Checking it in your own build 1. Build for production, not development; tree shaking and minification are production steps. 2. Open the build's chunk report or a bundle visualisation and search for `Transition`, `KeepAlive` or `Suspense` code. 3. If a built-in you never use is present, find who imports it: a plugin, a component library entry point, or a shared layout that renders it conditionally. 4. Check whether the app loads Vue's full build to compile templates in the browser; if so, precompile instead. ## Where the compiler's imports come from The SFC compiler turns each template into a render function at build time. Every tag, directive and binding it meets maps to a runtime helper, and the compiler adds a named import for each helper it emitted. A template that uses `v-model` on a text input imports the text-input model directive; a template without `v-model` imports none of them. That per-template precision is what makes Vue 3's size scale with the features an app uses rather than with the features Vue offers. ## A note on plugins Plugins that call `app.component` for dozens of components are the most common way a Vue 3 app gives back the benefit. When a library offers both a global-install plugin and individual component imports, the individual imports are the tree-shakable option.

  • A Vue 3 app installs a UI library with `app.use(Library)`, which registers 80 components globally, but uses six. What happens to the other 74?
    They stay in the bundle. Global registration references every component from the plugin's install code, so the bundler cannot prove them unused. Importing the six components locally where they are used, or using the library's per-component imports, lets the rest be dropped.
  • Why does compiling templates in the browser cost more than the compiler's size?
    Shipping the full build adds the template compiler to the bundle, and every template must also be compiled at run time before it can render, which costs CPU during startup. Precompiling at build time removes both, and lets the compiler emit imports of only the helpers each template needs.

saying these in an interview costs you the question

  • Believes Vue 3 always ships every built-in component, used or not.
  • Thinks tree shaking works the same with the browser global build from a CDN.
  • Assumes globally registered components are removed when unused.
  • Says Vue 2's global API was tree-shakable like Vue 3's named exports.
  • Credits Vue itself with removing unused code, rather than the bundler.