DEV Community

Dauren Abasov
Dauren Abasov

Posted on

I built a render debugger for Vue — now it runs on Rust/WASM

Vue tells you that a component re-rendered. It doesn't tell you why.

I kept hitting the same problem: a component re-renders, renderTriggered fires, but the payload says target: Proxy — useless. Which ref changed? Which prop got a new reference? Was it Pinia? The router? A computed three levels up?

So I built vue-why-render — a zero-dependency dev tool that names the exact reactive source behind every re-render.

Quick start

import { scan } from 'vue-why-render'

const app = createApp(App)
scan(app, { panel: true })
app.mount('#app')
✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน ✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน

That's it. Every component that re-renders now gets an overlay label:

ProductCard ×3  1.2 ms
prop badge: {text} → {text} — new reference, same value
💡 Object is rebuilt in parent — hoist to computed
✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน ✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน

You get the component name, the render count, the duration, the trigger — and an actionable hint when the render is avoidable.

The problem with Vue's built-in hooks

Vue 3 exposes renderTracked and renderTriggered on every component. In theory, they tell you what's going on. In practice:

  • target is a raw Proxy — you can't tell which ref it came from
  • There's no diffing — you don't see old vs. new value
  • Pinia stores show up as generic reactive objects
  • Route changes show the entire $route object, not which param changed
  • There's no way to see the dependency graph across components

vue-why-render hooks into these same APIs, but resolves the raw proxies back to named sources. When Pinia triggers a render, you see store cart.items: Array(1) → Array(0) · action clear(). When a route param changes, you see route.params.id: "1" → "2" — not $route: {...}.

What's in the panel

The floating panel has 5 tabs:

Tab What it shows
Top Components sorted by render count
Slow Components sorted by render duration
Tree Component hierarchy with Σ render sums
Events Chronological log of every render
Deps Reactivity graph (new in 0.8)

Reactivity graph

The Deps tab is the feature I'm most excited about. It builds a live dependency map: component ↔ reactive source.

Two views:

  • Components → which refs, props, stores, and computeds does this component read?
  • Sources → which components depend on this ref?

Every edge has a read count and a trigger count. If UserAvatar reads user.name 47 times but it only triggered 2 re-renders, you know the other 45 reads were harmless. If CartBadge gets triggered by cart.items on every keystroke — that's your problem.

It's like X-ray for Vue's reactivity system.

Why Rust/WASM

A real Vue app can have hundreds or thousands of components. Building the dependency tree, deduplicating render causes, and diffing reports across time — on every frame — adds up.

I rewrote the heavy algorithms in Rust and compiled them to WebAssembly:

  • Tree construction
  • Cause deduplication
  • Report diffing

The WASM module is lazy-loaded as a separate chunk — it doesn't bloat your app bundle. If WASM isn't available (SSR, jsdom, strict CSP), a JS fallback kicks in automatically. No configuration needed.

VS Code extension

Browser devtools are great, but I kept switching between Chrome and VS Code. So I built a WebSocket bridge.

The VS Code extension connects to the scanner running in the browser and shows:

  • Component tree in the sidebar — sorted by renders, with flame icons for hot components
  • Deps tree — same dependency graph, but in the editor
  • CodeLens on .vue files — render count and average duration, right on the component definition
  • Status bar — live render counter
  • Commands — pause, resume, reset from the command palette

No browser tab switching. The data is right where you write code.

Testing renders

The scariest thing about render optimization is regression. You fix a wasted render today, someone adds a prop next week, and it's back.

vue-why-render ships test utilities:

import { expectRenders } from 'vue-why-render/test'

it('ProductCard does not re-render on unrelated state change', async () => {
  // ... mount component, trigger action
  expectRenders(ProductCard).toBe(0, 'adding to cart')
})
✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน ✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน

This runs in Vitest / Jest. If ProductCard re-renders when it shouldn't, the test fails — with the exact trigger in the error message. The fix stays fixed.

Render budgets

For runtime monitoring, you can set budgets:

scan(app, {
  maxRenders: 10,    // per 5-second window
  maxDuration: 16,   // ms per update
})
✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน ✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน

Violations show up in the console, the panel, and the reports. Useful for catching regressions in staging before they hit production.

Reports

Export render data as JSON or Markdown:

import { report } from 'vue-why-render'

const data = report('json')   // structured data
const md = report('markdown') // ready for GitHub issues
✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน ✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน

You can diff two reports to catch regressions between builds.

Nuxt support

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['vue-why-render/nuxt']
})
✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน ✨ อัปเดตล่าสุด: dadashi/i built a render debugger for vue now it runs on rustwasm jhk - ประเด็นร้อน

Listed on nuxt.com/modules.

Try it

Zero runtime dependencies. MIT license. Vue 3.3+.

If you find it useful — a ⭐ on GitHub genuinely helps with discoverability. And if you hit a bug or want a feature, issues are open.

Top comments (0)