Nuxt DevTools is great. But at some point, while debugging real apps, I kept running into the same questions it couldn't answer:
- Which
useFetchcalls are actually in the critical path of this page? - Where does this
inject()get its value from, and why is itundefinedhere? - Is that composable leaking a watcher or an interval after the component unmounts?
- Why does this component re-render 40 times?
- What exactly happened between the route change and the page being ready?
So I built Nuxt DevTools Observatory, a module that adds deeper runtime observability on top of the official DevTools. It complements Nuxt DevTools, it doesn't replace it.
👉 Repo: victorlmneves/nuxt-devtools-observatory
👉 Docs: nuxt-devtools-observatory.vercel.app
Getting started
pnpm add nuxt-devtools-observatory
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-devtools-observatory'],
devtools: { enabled: true },
})
Open Nuxt DevTools and the new tabs show up. That's it. Everything is enabled by default in development, and you can turn individual features off via the observatory key in nuxt.config.ts.
What you get
1. useFetch Dashboard
A central view of every useFetch / useAsyncData call: key, URL, status, origin (SSR or CSR), payload size, duration, and a waterfall timeline. Server-side timing is captured by a Nitro plugin and sent to the client over the HMR WebSocket, so you see SSR fetches too.
2. provide/inject Graph
An interactive graph of your injection topology. Nodes are colour-coded (provides, injects, both, missing provider), and you get:
- inline value inspection for each provided key
- scope badges (
global,layout,component) - shadow detection when a child re-provides a key an ancestor already provides
- immediate flagging of any
inject()that resolves toundefined - jump-to-editor from the detail panel
3. Composable Tracker
This is the one I use most. It tracks your useXxx() composables live: returned ref, computed and reactive values, a change history, and global (shared) state detection by comparing object identity across instances.
Most importantly, it flags leaks: any watcher or interval still active after onUnmounted fires.
You can also edit a writable ref inline and the new value is applied to the live ref in the running app, which is handy for reproducing edge cases without touching code.
4. Render Heatmap
A component tree colour-coded by render frequency and duration, with a per-component render timeline, trigger keys and a route filter. You can set thresholds (heatmapThresholdCount, heatmapThresholdTime) to highlight the offenders, and export/import snapshots as JSON.
5. Pinia Tracker
For each Pinia store: state snapshots, an action/mutation timeline with before/after diffs, dependency edges (which components and composables touch the store) and hydration attribution (nuxt-payload, persistedstate or runtime).
6. Transition Tracker
A live timeline of every <Transition> lifecycle event with phase, duration, parent component and cancellation state. It's surprisingly useful for finding transitions that never finish because of missing CSS classes, or ones cancelled by rapid toggling.
7. Trace Viewer
Per-route traces that capture everything that happened after a navigation, in order: component mounts, real render durations, fetch timing, composable setup, server phases and transitions. You can inspect them as a Flamegraph or a Waterfall, and compare render behaviour across traces to spot regressions.
It helps answer things like "is this page slow because of setup(), or because of DOM patching?" and "which useFetch is blocking the critical path?"
How it works (the interesting part)
All instrumentation happens at build time through Vite transforms that wrap calls at the AST/module level before compilation:
-
useFetch/useAsyncDataare wrapped with a thin shim that records timing and payload. -
provide()/inject()are wrapped with versions that carry file and line metadata. At runtime, afindProvider()walks theinstance.parentchain to find who provided each key. - Any
useX()call matching Vue's naming convention is wrapped with a tracking shim that watches for new effects and intervals created during setup. -
<Transition>is trickier. The Vue 3 template compiler emits direct named imports (import { Transition as _Transition } from "vue"), which bypassapp.component()entirely. So the module serves a virtual proxy module forvuethat overrides theTransitionexport with an instrumented wrapper.
One detail I'm happy with: the transition wrapper deliberately does not wrap onEnter / onLeave. Vue inspects their .length to choose between CSS and JS timing modes, and wrapping them would change that length and silently alter behaviour.
Zero cost in production
Instrumentation is dev-only. When import.meta.dev is false, the transforms are skipped entirely, so there's no runtime overhead in production.
Things to know
-
Performance in dev: since it wraps fetches, composables and lifecycle hooks, very large apps may see a slower DevTools UI or higher memory usage. Disable what you don't need, and use the
max*options (maxFetchEntries,maxComposableEntries, etc.) to cap memory. -
SSR vs SPA:
instrumentServerdefaults totruefor SSR apps andfalsefor SPAs, to avoid double registration from the transform running on both builds. -
Opt out per call: add
/* @devtools-ignore */before any call you don't want instrumented.
/* @devtools-ignore */
const { data } = useFetch('/api/sensitive')
- Known gap: the Render Heatmap route filter can't hide persistent components (layouts), since they appear on every route by definition.
Try it
The repo includes a playground with intentionally broken scenarios: leaky composables, a missing provider, a heavy list that re-renders constantly, and transitions that are broken or cancelled. Clone it and you can see every panel light up without writing any code:
pnpm install
pnpm dev
Feedback welcome
The project is MIT-licensed and still young. If you try it, I'd love to hear which panels are useful, what's missing, and where it gets in your way. Issues, discussions and PRs are all welcome.







Top comments (0)