DEV Community

Cover image for Nuxt DevTools Observatory: the observability panels I wished Nuxt DevTools had
Victor Neves
Victor Neves

Posted on

Nuxt DevTools Observatory: the observability panels I wished Nuxt DevTools had

Nuxt DevTools is great. But at some point, while debugging real apps, I kept running into the same questions it couldn't answer:

  • Which useFetch calls are actually in the critical path of this page?
  • Where does this inject() get its value from, and why is it undefined here?
  • 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
Enter fullscreen mode Exit fullscreen mode
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-devtools-observatory'],
  devtools: { enabled: true },
})
Enter fullscreen mode Exit fullscreen mode

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.

useFetch Dashboard

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 to undefined
  • jump-to-editor from the detail panel

provide/inject Graph

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.

Composable Tracker

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.

Render Heatmap

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).

Pinia Tracker

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.

Transition Tracker

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?"

Trace Viewer

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 / useAsyncData are wrapped with a thin shim that records timing and payload.
  • provide() / inject() are wrapped with versions that carry file and line metadata. At runtime, a findProvider() walks the instance.parent chain 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 bypass app.component() entirely. So the module serves a virtual proxy module for vue that overrides the Transition export 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: instrumentServer defaults to true for SSR apps and false for 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')
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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.

⭐ github.com/victorlmneves/nuxt-devtools-observatory

Top comments (0)