Vue 3 shipped this month, and the headline addition for us is the Composition API, an alternative to the Options API we have used since Vue 2 for organizing a component's data, methods, and lifecycle hooks. We ported a small internal reporting tool to it this week to get a real feel for the change rather than just reading the RFC.
What is different
- Logic is organized by feature/concern rather than by option type — a `setup()` function can group all the state and behavior for one concern together, instead of it being split across `data`, `computed`, and `methods`
- Reusable logic extraction ("composables") replaces a lot of what we used mixins for in Vue 2, without mixins' well-known naming collision and "where did this property come from" problems
- The Options API is still fully supported, so this is additive rather than a breaking change to how existing Vue 2 knowledge applies
Our early take
For small, simple components, the Options API still feels more approachable, and we are not going to force the Composition API on every component going forward. But for the reporting tool's more complex components, which had several unrelated concerns interleaved across `data` and `methods` in the old version, splitting that logic into separate composables made the resulting code noticeably easier to follow.
We are not moving any client projects onto Vue 3 yet — the ecosystem, including several plugins we rely on, needs time to catch up first — but we expect the Composition API to be how we write new Vue components once that migration happens.
A concrete composable we extracted
The reporting tool had three separate components each independently implementing roughly the same "fetch this resource, track loading and error state, refetch on a dependency change" logic, previously copy-pasted with small variations between each one. We pulled this into a single `useAsyncResource` composable that any component can call with a fetch function and a reactive dependency, and all three components now share one implementation rather than three slightly diverging copies. This is exactly the kind of cross-cutting logic that Vue 2's mixins could technically achieve but made genuinely awkward to trace, since a mixin's properties get merged into a component with no visible indication in the component's own code of where a given property actually came from.
What surprised us about the reactivity system underneath it
Vue 3's reactivity system, built on JavaScript Proxies rather than Vue 2's `Object.defineProperty`-based approach, fixes a few long-standing Vue 2 limitations we had simply learned to work around — most notably, adding a new property to a reactive object after it is created now triggers reactivity correctly, where Vue 2 required a special `Vue.set` call to make that new property actually reactive. None of our ported component's code depended on that Vue 2 workaround, so the change was invisible in practice, but knowing the underlying mechanism improved means we can drop a habit that was really a workaround for a limitation that no longer exists.
Migration path concerns for our existing Vue 2 client projects
Porting one small internal tool is a very different exercise from migrating an established client codebase with dozens of components and several third-party plugin dependencies, and we are deliberately not treating this experiment as evidence that a real migration would be equally smooth. Several plugins our client projects depend on have not yet published Vue 3 compatible versions, and until they do, recommending a migration to an actual client project would mean either dropping functionality or maintaining a fork of a third-party plugin ourselves, neither of which we are willing to commit a client to yet.
Where the Options API still genuinely wins
For a simple form component or a presentational component with little internal logic, writing a `setup()` function that returns everything needed by the template is arguably more ceremony than the equivalent Options API component, which reads top-to-bottom in a more immediately obvious way to a developer scanning it for the first time. We are not adopting a blanket "always use the Composition API" rule internally, and expect our own component library to have a genuine, deliberate mix of both styles depending on a given component's actual complexity, at least for the foreseeable future.
What we are watching before recommending a migration to clients
Beyond plugin compatibility, we want to see real production experience — our own or the broader community's — with a couple of specific concerns: how well the Composition API's more implicit style holds up in code review for developers newer to a given codebase, and whether the extra indirection of composables makes debugging a live production issue meaningfully harder or easier than the Options API's more explicit structure. We expect answers to both questions to become clearer over the next several months as more teams put Vue 3 into real production use.
Comparing bundle size against the Vue 2 version of the same tool
Curious whether Vue 3's rewritten internals would show up as a measurable difference for a real, if small, application, we compared the production bundle size of the ported reporting tool against its original Vue 2 version. The Vue 3 build came out smaller, mostly attributable to Vue 3's better tree-shaking support, since the framework's APIs are now exposed as named exports the bundler can eliminate when unused rather than Vue 2's single large runtime object that a bundler has a harder time meaningfully slimming down. It is a modest difference for a tool this small, but it is a real, measurable one, and we expect it to matter more on a larger application with more unused framework surface area to shake out.
The new global API and what it means for our shared plugins
Vue 3 also changes how an application instance is created and how plugins, directives, and global components get registered — a `createApp()` call now returns an application instance that plugins attach to explicitly, rather than mutating a single shared global `Vue` object the way Vue 2 plugins do. This is a welcome change for us specifically because we run a handful of internal tools on the same page in a couple of admin-dashboard contexts, where Vue 2's single mutable global object meant two Vue instances on the same page could not always cleanly avoid stepping on each other's global registrations. Vue 3's per-application instance model removes that entire class of problem, though it does mean every one of our shared internal plugins needs a small update to its registration code before it works under Vue 3, which is part of the ecosystem catch-up we are currently waiting on before recommending a client migration.
Results
We are not moving any client projects onto Vue 3 yet — the ecosystem, including several plugins we rely on, needs time to catch up first — but we expect the Composition API to be how we write new Vue components once that migration happens, and this week's small experiment has given the team enough hands-on familiarity to make that eventual transition considerably less of a cold start than it would otherwise have been.
Two smaller Vue 3 features that mattered more than we expected
Beyond the Composition API, two smaller additions proved immediately useful on the ported tool: Teleport, which lets a component render part of its template into a different part of the DOM entirely — useful for a modal that needs to escape an ancestor's `overflow: hidden` without breaking the component tree's logical structure — and native support for multiple root nodes in a single component template, removing Vue 2's long-standing requirement that every component wrap its template in one single root element. Neither is as headline-grabbing as the Composition API, but both removed small, specific workarounds our component library had accumulated over time purely to route around Vue 2's stricter template rules.
Performance differences we noticed anecdotally
We did not run a rigorous benchmark, but the ported tool's initial render felt subjectively snappier under Vue 3, consistent with the framework's own claims about a rewritten virtual DOM diffing algorithm being faster than Vue 2's. We are treating this as an anecdotal impression rather than a verified result until we have time to run an actual controlled benchmark, but it is a promising early signal alongside the other improvements described above. This matches independent early benchmarks the broader Vue community has already started publishing, though we would still want to reproduce a result like that on our own representative application before treating it as a settled fact rather than an early, promising signal.