skip to content

In Vue 3, what is the difference between registering a component with app.component() and importing it locally in `<script setup>`, and which should you prefer?

level: juniorimportance: must knowfreq 55%

answer

  1. who can see the component
  2. app-wide registry vs one file
  3. what the bundler can drop
  4. explicit dependency graph

basics

~20 s

Vue's app.component() registers a component app-wide, so any template can use it without importing, but it always ships in the bundle. A local import in script setup is visible only in that file and tree-shakes cleanly, so it is the default.

solid answer

~50 s

`app.component('BaseButton', BaseButton)` puts the component in the application's registry; every template in that app, including the registered components themselves, can then use `<BaseButton>` without an import. In `<script setup>`, `import BaseButton from './BaseButton.vue'` is enough to use it in that file's template: no registration step, and it is visible only there. The Vue docs list two drawbacks of global registration: the build cannot **tree-shake** a globally registered component, so it ships even if no page uses it, and dependencies become implicit, so you cannot see from a parent where a child comes from. Local imports keep the dependency graph explicit, work with editor navigation and type checking out of the box, and let the bundler put a component only in the chunks that use it. Reserve global registration for a handful of truly ubiquitous primitives.

go deeper

for a junior

Know both forms, that script setup needs only an import, and that a global registration makes a tag usable in every template of the app.

for a middle

Explain why a registered component cannot be tree-shaken and why local registration does not reach descendant components.

for a senior

Audit an app's global registrations, move rarely used ones to local imports and measure what leaves the entry chunk.

for a principal

Define the policy for which base components, if any, a design system registers globally, trading call-site convenience against bundle cost and discoverability.

## Two ways Vue finds a component When a Vue template contains a tag such as `<BaseButton>`, Vue must know which component implementation that tag means. Vue 3 offers two ways to make that link, called **registration**: - **Global registration** with `app.component(name, component)` adds the component to the registry of one application instance created by `createApp`. - **Local registration** makes a component available to a single component. In `<script setup>` it is just an `import`; in a non-`<script setup>` component it is the `components` option. ```ts import { createApp } from 'vue' import App from './App.vue' import BaseButton from './components/BaseButton.vue' createApp(App).component('BaseButton', BaseButton).mount('#app') ``` `app.component()` returns the app, so calls can be chained, and calling it with only a name returns the component already registered under that name. ## How they compare | Aspect | `app.component()` | Local import in `<script setup>` | |---|---|---| | Visible in | Every template of that app | Only the importing file | | Needs an import at the use site | No | Yes | | Tree-shaking | Not possible; it always ships | Unused components are dropped | | Chunk placement | Wherever the registering module loads, usually the entry | Only the chunks that import it | | Finding the implementation | Search the registry setup | Follow the import | | Template type checking | Needs a `GlobalComponents` type declaration | Works from the import | ## Why global registration defeats tree-shaking A bundler removes a module only when nothing references it. `app.component('BaseButton', BaseButton)` is itself a reference, made in the app's startup code, and the bundler cannot know which string tags templates will ask the registry for at runtime. So every globally registered component: 1. is included in the build even if no template uses it; 2. is typically pulled into the **entry chunk**, because the registration runs at startup, even when only one lazily loaded page renders it; 3. hides the real dependency from tools that follow imports. A local import has none of these costs: if a page that imports `DataGrid` is split into its own chunk, `DataGrid` travels with that chunk. ## The explicitness argument The docs compare heavy global registration to **too many global variables**. In a large codebase, a template that says `<UserBadge>` with no import gives the reader no path to the file, and renaming or deleting the component cannot be checked by following imports. A local import makes the dependency graph visible to people, editors and build tools alike. One scoping rule surprises people: **a locally registered component is not available in its descendants**. If a parent imports `UserBadge`, a child that renders `<UserBadge>` must import it too. Global registration is the only mechanism that makes a component visible everywhere. ## When global registration is still reasonable - A very small set of **base components** used on nearly every screen, where an import in every file adds noise. - Components contributed by a library whose documented setup registers them app-wide. - Prototypes and in-DOM templates without a build step, where there is no import to write. For everything else, prefer local imports: the default Vue project setup and `<script setup>` make them the path of least resistance.

  • A parent in Vue 3 imports `UserBadge` in `<script setup>`; a child component renders `<UserBadge>` without importing it. What happens?
    The child's template cannot resolve the tag: local registration is scoped to the component that did it and is not inherited by descendants. Vue logs a `Failed to resolve component` warning in development and renders an unknown element. The child must import `UserBadge` itself, or the component must be registered globally.
  • If an app has two `createApp` instances, is a component registered with `app.component()` on one available in the other?
    No. Each application instance has its own registry, created by its own `createApp` call, so registration on one app does not leak into another. That per-app scoping is a deliberate change from Vue 2's single global constructor.

saying these in an interview costs you the question

  • Globally registered components that no page uses are removed by tree-shaking.
  • A component imported by a parent is automatically available in all of its children.
  • In script setup you must also list imported components in a components option.
  • app.component registers the component for every Vue app on the page.