Vue.js

Vapor Mode is now the default renderer for new Vue components

VUE

Vue's Vapor Mode moved from experimental to production-proven on our render-heavy dashboards last year, the kind of internal tooling with enough live-updating widgets on screen at once that the old virtual DOM overhead was genuinely visible to users as jank rather than just a number in a benchmark. This year, with the remaining ecosystem gaps largely closed, it became the default renderer for new components rather than something we opted into deliberately for specific performance-sensitive cases.

The performance ceiling for Vue apps has moved up noticeably as a result, most visibly on exactly the kind of screen that motivated our first cautious adoption: dashboards with dozens of independently updating data points, where the old rendering model's overhead scaled poorly as the number of reactive bindings on screen grew. Vapor Mode's compile-time approach to reactivity sidesteps a meaningful chunk of that overhead, and on our busiest internal dashboard the difference between the old and new renderer was large enough that engineers noticed it without being told to look for it.

The internal benchmark we ran comparing the two renderers on our busiest dashboard made the difference concrete rather than a vague sense of smoothness:

metric                    virtual DOM    vapor mode
--------------------------------------------------------
initial render (120 widgets)   340ms         210ms
update 1 widget                 8.2ms          0.9ms
update all 120 widgets         410ms          38ms
memory (idle, after 10m)        86MB          52MB

The update-one-widget number is the one that actually explains the jank users had been reporting: under the old renderer, updating a single reactive value still triggered enough surrounding diff work to be perceptible, where Vapor Mode's compiled update touches only the DOM node that actually changed.

The component itself doesn't look any different -- it's a plain SFC that now just compiles through the Vapor pipeline by default:

<script setup lang="ts">
import { ref } from 'vue';

const count = ref(0);
</script>

<template>
  <button @click="count++">Clicked {{ count }} times</button>
</template>

The mental model shift the migration required a couple of years ago now barely registers for developers joining the team fresh, since they're simply writing components against the current default rather than consciously choosing a newer rendering mode over an established one. That's a nice inversion from how the early adoption period felt, when using Vapor Mode meant explaining to a new team member why this particular component looked slightly different from the rest of the codebase and what tradeoff justified the difference.

The ecosystem gaps that held up broader adoption for a while were mostly in third-party component libraries that assumed the older rendering internals, and closing them took patience rather than any clever workaround, mostly waiting for library maintainers to catch up and occasionally contributing a fix ourselves when a library we depended on heavily hadn't gotten there yet. Now that the gaps have closed, there isn't really a remaining case where we'd deliberately choose the old renderer for a new component, which is exactly the point at which a migration stops being a project and starts being just how things work.

Migrating existing components rather than just defaulting new ones to Vapor Mode has been a slower, ongoing process, and we've been deliberate about not forcing it. A component that works fine under the older renderer and isn't on a performance-sensitive screen gets left alone until someone touches it for an unrelated reason, at which point converting it becomes a small addition to that other work rather than a dedicated migration task competing for its own sprint slot. That approach means our codebase currently has a genuine mix of both rendering approaches coexisting, which Vue supports cleanly, and we've felt no urgency to force a full conversion just for the sake of consistency.

The exception to that patience has been anything on our busiest internal dashboards, where we did prioritize deliberate conversion of existing components specifically because the performance benefit was large enough to justify pulling engineering time away from other work. Those targeted conversions were where we first proved out Vapor Mode's benefit concretely enough to justify making it the default everywhere else, and we'd recommend that sequencing to any team considering a similar migration: prove the benefit on your most demanding real screen before flipping the default for everything else.

Flipping the actual default was a one-line change to our shared Vite config, once the ecosystem gaps were closed enough to trust it everywhere:

// vite.config.ts
import vue from '@vitejs/plugin-vue';

export default {
  plugins: [vue({ vapor: true })],
};

New components compile through the Vapor pipeline automatically from that point on, with no per-component opt-in required and no visible difference in how a developer writes the component itself.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients