@sedatdemirAra

Vue 3.6 Vapor Mode: Virtual DOM'suz Vue

Vue'nun reaktivite sistemi her zaman template seviyesinde hangi bağımlılığın değiştiğini biliyordu. Ama bu bilgiyi kullanma şekli dolaylıydı: önce Virtual DOM ağacını yeniden oluştur, sonra diff al, sonra gerçek DOM'u güncelle. Vapor Mode bu dolaylılığı ortadan kaldırıyor.

Neden Virtual DOM bir yük haline geldi

Virtual DOM, React'in 2013'te popülerleştirdiği bir soyutlama. Bileşen ağacının her render döngüsünde bellekte bir kopya oluşturulur, önceki kopyayla karşılaştırılır ve fark gerçek DOM'a yansıtılır. Vue 3 bu süreci @vue/runtime-core içindeki patch fonksiyonuyla yürütüyor.

Problem şu: Vue zaten derleme zamanında template'i analiz edip hangi ifadelerin reaktif olduğunu biliyor. Compiler, v-bind, {{ }} ve v-if gibi direktifleri ayrıştırdığında statik düğümleri (hiç değişmeyen HTML) ve dinamik düğümleri ayırt edebiliyor. Vue 3.0'dan beri patchFlag sistemiyle bu ayrım yapılıyor, ama sonuç yine bir vnode ağacına dönüyor. Yani compiler "bu düğüm hiç değişmeyecek" diyor, runtime yine de o düğümün vnode'unu bellekte tutuyor.

Vapor Mode bu gereksiz ara katmanı tamamen çıkarıyor.

Vapor Mode nasıl çalışıyor

Vapor Mode, Vue compiler'ın (@vue/compiler-vapor) ürettiği kodu değiştiriyor. Normal modda compiler şöyle bir çıktı üretir:

// Normal Vue 3 compiler çıktısı (basitleştirilmiş)
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString } from "vue"

export function render(_ctx) {
  return _createElementVNode("div", null, [
    _createElementVNode("span", null, _toDisplayString(_ctx.count))
  ])
}

Bu kod her render'da yeni vnode nesneleri üretir. Vapor Mode'da aynı template şuna dönüşür:

// Vapor Mode compiler çıktısı (basitleştirilmiş)
import { template, setText, renderEffect } from "@vue/runtime-vapor"

const t0 = template("<div><span></span></div>")

export function render(_ctx) {
  const root = t0()
  const span = root.firstChild

  renderEffect(() => {
    setText(span, _ctx.count)
  })

  return root
}

Fark açık: template fonksiyonu HTML string'inden gerçek DOM düğümlerini bir kez oluşturuyor (innerHTML veya cloneNode ile). Sonra renderEffect ile reaktif bağımlılık takibi yapılıyor. _ctx.count değiştiğinde sadece setText(span, ...) çağrılıyor. Vnode yok, diff yok, patch yok.

Bu yaklaşım Svelte 5 ve Solid.js'in derleme stratejisiyle benzer bir felsefeyi paylaşıyor. Svelte 5'in runes sistemi de benzer şekilde fine-grained reaktiviteye geçiş yaptı.

Derleme stratejisinin detayları

Vapor compiler birkaç aşamada çalışıyor:

  1. Template parse edilir ve AST oluşturulur (bu adım normal compiler ile aynı).
  2. AST üzerinde statik analiz yapılır. Her düğüm "statik" veya "dinamik" olarak işaretlenir.
  3. Statik düğümler tek bir HTML string'ine birleştirilir ve template() çağrısına sarılır.
  4. Dinamik düğümler için DOM traversal talimatları üretilir: root.firstChild, el.nextSibling gibi.
  5. Her dinamik ifade bir renderEffect içine sarılır.

Üçüncü adımdaki template() fonksiyonu runtime'da document.createElement yerine <template> elementi kullanıyor. Tarayıcılar <template> elementinin innerHTML atamasını parse edip content property'sinde bir DocumentFragment olarak saklıyor. Sonraki kullanımlarda cloneNode(true) ile kopyalama yapılıyor, bu da tekrarlanan bileşenler (örneğin v-for listeleri) için oldukça hızlı.

Kontrol akışı: v-if ve v-for

v-if direktifi Vapor Mode'da bir createIf helper'ına dönüşüyor:

import { createIf, template, setText, renderEffect } from "@vue/runtime-vapor"

const t0 = template("<span></span>")
const t1 = template("<p>Fallback</p>")

export function render(_ctx) {
  return createIf(
    () => _ctx.show,
    () => {
      const el = t0()
      renderEffect(() => setText(el, _ctx.message))
      return el
    },
    () => t1()
  )
}

createIf, koşul değiştiğinde eski DOM düğümünü çıkarıp yenisini ekliyor. v-for için benzer şekilde createFor helper'ı var; bu helper, key tabanlı bir diffing algoritması kullanıyor, ama bu diff vnode'lar üzerinde değil, doğrudan DOM düğümleri üzerinde çalışıyor.

Performans farkı ne kadar

Vue ekibinin 2024 VueConf sunumunda paylaştığı dahili benchmark sonuçlarına göre, Vapor Mode'un bellek kullanımı normal Vue 3'e kıyasla %40-60 daha düşük. Bunun sebebi vnode nesnelerinin hiç oluşturulmaması. Her vnode en az 15-20 property içeren bir JavaScript nesnesi; büyük listelerde binlerce vnode bellekte tutuluyor.

Render süresi tarafında ise kazanım senaryoya bağlı. Statik ağırlıklı template'lerde (blog sayfası, pazarlama sitesi gibi) fark dramatik değil çünkü Vue 3 zaten statik düğümleri hoist ediyor. Asıl fark, sık güncellenen dinamik UI'larda ortaya çıkıyor: tablolar, gerçek zamanlı dashboard'lar, büyük v-for listeleri.

js-framework-benchmark (Stefan Krause'nin sürdürdüğü benchmark seti) üzerinde Vapor Mode prototipi, Solid.js ve vanilla JavaScript ile benzer skorlar veriyor. Normal Vue 3 ise bu benchmark'ta genellikle 1.3-1.5x vanilla baseline civarında kalıyordu.

Bundle boyutu da değişiyor. @vue/runtime-vapor paketi @vue/runtime-core + @vue/runtime-dom kombinasyonundan daha küçük çünkü vnode oluşturma, patch, diff gibi fonksiyonlar tamamen çıkarılmış durumda. Evan You'nun paylaştığı rakamlara göre Vapor-only bir uygulamanın runtime boyutu yaklaşık 6-7 KB (gzip), normal Vue runtime'ı ise yaklaşık 16 KB (gzip).

Mevcut Vue koduyla uyumluluk

Vapor Mode opt-in bir özellik. Vue 3.6'da tüm uygulamayı Vapor'a geçirmek zorunlu değil. İki farklı strateji var:

Birincisi, tüm uygulamayı Vapor Mode ile oluşturmak. Bu durumda createVaporApp kullanılıyor:

import { createVaporApp } from "vue/vapor"
import App from "./App.vue"

createVaporApp(App).mount("#app")

İkincisi, normal Vue uygulaması içinde belirli bileşenleri Vapor Mode'da derlemek. Bu "hybrid" mod, tedricen geçiş yapmak isteyenler için tasarlanmış. Bir .vue dosyasının Vapor Mode'da derlenmesi için Vite plugin konfigürasyonunda bileşen bazlı ayar yapılabiliyor.

Ama her şey uyumlu değil. Vapor Mode'un şu anki haliyle desteklemediği veya farklı çalıştığı alanlar var:

Render fonksiyonları ve JSX desteği sınırlı. Vapor Mode template-first bir yaklaşım; h() fonksiyonuyla yazılmış render fonksiyonları Vapor derleme hattından geçemiyor. Bu bileşenler normal modda kalmalı.

Üçüncü parti kütüphaneler sorunlu olabilir. Eğer bir kütüphane vnode'a doğrudan erişim yapıyorsa (custom renderer, vnode manipülasyonu gibi) Vapor Mode ile çalışmaz. Vuetify, Quasar gibi büyük UI kütüphaneleri henüz Vapor uyumluluğunu duyurmadı.

<Transition> ve <KeepAlive> gibi dahili bileşenler Vapor Mode'da yeniden implemente edilmesi gereken bileşenler. Bu çalışma devam ediyor.

Pratikte nasıl denerim

Vue 3.6 henüz stabil yayınlanmadı ama alpha/beta kanalından deneysel olarak kullanılabiliyor. Vite kullanıyorsanız:

npm install vue@next @vitejs/plugin-vue@latest

vite.config.ts içinde Vapor desteğini açmak için:

import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"

export default defineConfig({
  plugins: [
    vue({
      vapor: true // tüm bileşenleri Vapor modunda derle
    })
  ]
})

Ya da bileşen bazlı:

export default defineConfig({
  plugins: [
    vue({
      vapor: {
        include: [/VaporComponent\.vue$/]
      }
    })
  ]
})

Basit bir Vapor bileşeni normal bir .vue dosyasından farksız görünüyor:

<script setup>
import { ref } from "vue"

const count = ref(0)
</script>

<template>
  <button @click="count++">Count: {{ count }}</button>
</template>

Derlenmiş çıktıyı görmek istiyorsanız Vue Vapor Playground (vapor.vuejs.org) üzerinde template'inizi yazıp compiler çıktısını inceleyebilirsiniz.

Diğer framework'lerle karşılaştırma

Vapor Mode'un derleme stratejisi Svelte ve Solid.js ile yakın. Ama önemli bir fark var: Vue'nun reaktivite sistemi (ref, reactive, computed, watch) değişmiyor. Svelte 5 yeni bir reaktivite modeli (runes) getirdi, Solid.js başından beri signal tabanlıydı. Vue ise mevcut Composition API'yi aynen koruyor, sadece compiler'ın çıktısı değişiyor.

Bu, geçiş maliyetini düşürüyor. Mevcut <script setup> kodu, Vapor Mode'a geçildiğinde hiçbir değişiklik gerektirmiyor. Aynı ref, aynı computed, aynı watch. Değişen sadece template'in nasıl derlediği.

Vite 8 ve Rolldown ile birleştirildiğinde build süreci de hızlanıyor. Rolldown'ın Rust tabanlı bundling'i ile Vapor Mode'un küçük runtime boyutu bir arada kullanıldığında, özellikle edge deploy senaryolarında avantaj sağlıyor. Hono + Cloudflare Workers gibi bir edge stack üzerinde Vue Vapor ile SSR yapmak, cold start sürelerini düşürebilir.

React tarafında ise Virtual DOM hâlâ merkezi bir parça. React Compiler farklı bir strateji izliyor: Virtual DOM'u kaldırmak yerine, gereksiz re-render'ları otomatik memoization ile azaltmaya çalışıyor. İki yaklaşım temelden farklı.

Ne zaman Vapor Mode kullanmalı

Eğer yeni bir Vue projesi başlıyorsanız ve template-first yazıyorsanız (JSX kullanmıyorsanız), Vapor Mode'u denemeye değer. Özellikle performans hassas uygulamalarda (veri tabloları, gerçek zamanlı göstergeler, mobil web) bellek ve render süresi kazanımları somut.

Ama mevcut büyük bir Vue 3 projesini bugün Vapor'a taşımak erken. Üçüncü parti kütüphane uyumluluğu henüz olgunlaşmadı, <Transition> gibi dahili bileşenler tam çalışmıyor ve production-ready olduğuna dair resmi bir duyuru yok. Bileşen bazlı hybrid mod, kritik performans darboğazlarını çözmek için mantıklı bir ara adım. Uygulamanın geneli normal modda kalırken, en çok güncellenen birkaç bileşeni Vapor'a geçirmek, riski düşük tutarak kazanım sağlamanın yolu.