vite-mastery

12.2 · difficulty 2/4 · 14 min read

Vite DevTools in Practice

The official Vite 8 debugging framework `@vitejs/devtools` — enabling modes, visualization capabilities, how it relates to vite-plugin-inspect, and how to extend it with your own Dock panel.

Vite 8.1Experimental

1. What is Vite DevTools, really?

@vitejs/devtools isn't yet another "floating widget injected into your page." It's an extensible debugging framework the Vite team built for the v8 era — a shell plus a set of integrations.

Internalizing that distinction prevents confusion with vite-plugin-inspect:

Dimensionvite-plugin-inspect@vitejs/devtools
ScopeSingle-purpose pluginFramework + integration collection
Phasesdev onlydev and build
ExtensibilityNot extensibleAny Vite plugin can host a Dock panel
Core powerPer-file transform chain + diffModule graph / plugin insights / chunk views / dup pkgs / compare
DependenciesStandaloneVite 8+, deep Rolldown integration

One-liner: inspect tells you "what did this file become?"; DevTools tells you "what does the whole dev/build ecosystem look like, and where is it expensive?" They're complementary, not competing.

2. How to enable it

DevTools offers three modes. Pick by need.

2.1 Install

bash
pnpm add -D @vitejs/devtools

Requires Vite 8.0+. On older versions even the install can succeed but it won't work — devtools and build.rolldownOptions.devtools are both Vite 8 additions.

2.2 Standalone mode: analyze after a build

The lightest setup — a single line in defineConfig:

ts
import { defineConfig } from "vite"

export default defineConfig({
  devtools: {
    enabled: true,
  },
})

After pnpm build, the terminal prints a DevTools URL (a local analysis server). Open it in a browser for the full build report. Best for CI: surface an artifact link after each build without bothering anyone during dev.

2.3 Embedded mode: a Dock during dev

When you want a live view in dev, go embedded:

ts
import { DevTools } from "@vitejs/devtools"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [DevTools()],
})

Run pnpm dev and a Dock surfaces in the bottom corner of your app (it looks like the macOS Dock). Every available DevTools integration appears as an icon. This is the default recommendation for day-to-day development.

If you want the DevTools app to be browsable as a static site after production builds (rather than only via the temporary server), enable withApp:

ts
DevTools({
  build: {
    withApp: true,
    // outDir: "custom-dir",
  },
})

2.4 Rolldown integration: where the build analysis lives

The killer feature is the Rolldown integration. Flip a switch under build.rolldownOptions:

ts
import { DevTools } from "@vitejs/devtools"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [DevTools()],
  build: {
    rolldownOptions: {
      devtools: {},
    },
  },
})

Now every pnpm build leaves a full build snapshot inside DevTools. Session Compare diffs across those snapshots.

3. The Rolldown integration's five panels

This is the part of DevTools actually worth studying. Each capability maps to one panel.

3.1 Module analysis

Visualizes the full module graph: nodes are files, edges are imports. Click any node to see the transform chain — which plugins processed it in what order, and what each one emitted.

The sequence diagram below shows the actual hook firing order when Vite/Rolldown loads a module; the DevTools transform panel surfaces this same sequence:

Hook timing browser

Click a hook on the left to inspect details

In practice this panel answers two questions over and over:

  1. "Why did this file end up in the bundle?" Walk importers backward; you'll reach the entry within a few hops.
  2. "Why isn't my transform hook running?" If your plugin name doesn't appear in the chain at all, your apply config or enforce order is probably wrong.

3.2 Plugin insights

Aggregated by plugin: how many times each was called, cumulative cost, which modules it touched. This is the first stop for "dev server feels slow."

DevTools groups the data into three columns — pre / default / post — by inspecting each plugin's enforce field. This diagram builds the intuition:

Plugin execution orderhooks:

pre

enforce: "pre"

normal

no enforce

post

enforce: "post"

Hover plugin cards to inspect shared hook relationships

When you read the Plugin insights panel, watch for two anti-patterns:

  • A enforce: "pre" plugin dominates total time — it gates every file at the very front and is doing unnecessary work. Add id filtering, or move to on-demand load.
  • A enforce: "post" plugin's transform count is much higher than expected — usually post plugins are re-walking upstream sourcemaps. Narrow the trigger.

3.3 Chunk & asset analysis (four views of the same output)

The same artifact, viewed four ways:

  • List — a table sorted by size / gzip size
  • Graph — node graph of chunk-to-chunk dependencies
  • Treemap — square tree, at-a-glance "which chunk swallowed the bulk?"
  • Flamegraph — attribution view; great for "which npm package contributed how many bytes?"

Recipe: Treemap to find the largest chunk, Flamegraph to find contributors, List to check gzipped sizes. That order keeps you from panicking at a 1MB uncompressed file that turns out to be 80KB gzipped.

3.4 Package detection

Automatically flags any npm package that ended up in the bundle multiple times — either version mismatches (lodash@4.17.21 and lodash@4.17.20 both present) or path mismatches (node_modules/foo/node_modules/lodash vs node_modules/lodash).

In monorepos this panel pays for itself: a misconfigured pnpm hoisting policy can silently cause duplicate bundling.

3.5 Session compare

Diff "the last main-branch build" against "the current PR build": which chunks grew, which dependencies are new, what got pulled in unexpectedly.

In CI, save the baseline session as an artifact, then run compare after the PR build. You catch size regressions during review instead of in production.

4. When you still reach for vite-plugin-inspect

Two scenarios DevTools doesn't cover but inspect handles well:

  1. "What did each plugin do to this one file?" — inspect's diff view is the fastest way. DevTools' Module analysis gives you the timeline, but for raw before/after text comparison on a single file, inspect wins.
  2. "I want a URL I can paste in chat to jump straight to a transform chain"/__inspect?module=... is trivial to share.

You can run both side by side — no conflict:

ts
import { DevTools } from "@vitejs/devtools"
import { defineConfig } from "vite"
import Inspect from "vite-plugin-inspect"

export default defineConfig({
  plugins: [DevTools(), Inspect()],
})

5. Adding your own Dock panel to a Vite plugin

The most interesting part of DevTools: any Vite plugin can drop an icon into the Dock. The mechanism is a new devtools field on the plugin object, whose setup(ctx) is called when DevTools is active.

ts
/// <reference types="@vitejs/devtools-kit" />
import { fileURLToPath } from "node:url"
import { defineRpcFunction } from "@vitejs/devtools-kit"
import type { Plugin } from "vite"

export default function myPlugin(): Plugin {
  return {
    name: "my-analyzer",
    devtools: {
      setup(ctx) {
        // 1. Host the panel's static assets (your own SPA bundled with Vite/Vue/React)
        const clientPath = fileURLToPath(new URL("../dist/client", import.meta.url))
        ctx.views.hostStatic("/__my-analyzer/", clientPath)

        // 2. Register a Dock entry
        ctx.docks.register({
          id: "my-analyzer",
          title: "My Analyzer",
          icon: "ph:puzzle-piece-duotone",
          type: "iframe",
          url: "/__my-analyzer/",
        })

        // 3. Expose an RPC the frontend panel can call type-safely
        ctx.rpc.register(
          defineRpcFunction({
            name: "my-analyzer:get-modules",
            type: "query",
            setup: () => ({
              handler: async () => {
                // ctx.viteServer is available in dev — read the module graph
                return Array.from(ctx.viteServer?.moduleGraph.idToModuleMap.keys() ?? [])
              },
            }),
          })
        )
      },
    },
  }
}

Key fields on ctx:

  • ctx.docks — Dock registration
  • ctx.views — static asset hosting
  • ctx.rpc — bidirectional RPC registration
  • ctx.viteConfigResolvedConfig
  • ctx.viteServerViteDevServer in dev mode (undefined in build mode)
  • ctx.mode"dev" | "build"
  • ctx.cwd / ctx.workspaceRoot

Branch on ctx.mode when writing custom panels: in build mode you don't get viteServer, so hot-path data has to come from the build snapshot instead.

6. Workflow: a funnel for "slow page" → root cause

Treat DevTools like a funnel, coarse to fine:

  1. Embedded Dock → Plugin insights: start with "top 5 plugins by cumulative time." If 90% of time goes to one plugin, you've already localized the problem.
  2. Same panel → Module analysis: see which modules that plugin actually processed. Common anti-pattern: a plugin meant for .svg is firing on every .ts file.
  3. Hand off to vite-plugin-inspect: diff the slowest single file to decide whether it's sourcemap regen, AST re-parsing, or heavy string concat.
  4. If the build is large rather than slow → switch to Rolldown integration's Chunk treemap; find the largest chunk.
  5. Drilling into a big chunkFlamegraph for attribution by npm package.
  6. Suspect duplicate bundlingPackage detection pinpoints it in one glance.
  7. Before opening a PRSession compare against main to catch size regressions.

Walk this funnel and the vast majority of Vite performance problems land at a specific line number or chunk.

Self-check

  1. What is @vitejs/devtools best at vs. vite-plugin-inspect? Can they coexist?
  2. What's the difference between Standalone and Embedded mode? When is each more appropriate?
  3. Inside the Rolldown integration's chunk views, what question do Treemap and Flamegraph each answer?
  4. What are the minimum three steps a plugin must take to add its own Dock panel?
  5. In which mode is ctx.viteServer undefined? What does that mean for how your RPC handler must behave?
ts
// Add DevTools Dock integration to this minimal transform-timer plugin:
// - Dock id: "transform-timer"
// - Panel title: "Transform Timer"
// - Expose an RPC `transform-timer:list` that returns the top 10 modules by cumulative time
//
// You don't need to write the frontend UI — just fill in the Node-side setup().

/// <reference types="@vitejs/devtools-kit" />
import { defineRpcFunction } from "@vitejs/devtools-kit"
import type { Plugin } from "vite"

interface ModuleTiming {
  id: string
  totalMs: number
  count: number
}

export function transformTimerPlugin(): Plugin {
  const timings = new Map<string, ModuleTiming>()

  return {
    name: "transform-timer",
    transform(_code, id) {
      const start = performance.now()
      // In a real plugin you'd wrap the downstream transform — omitted for brevity
      const elapsed = performance.now() - start
      const prev = timings.get(id) ?? { id, totalMs: 0, count: 0 }
      timings.set(id, { id, totalMs: prev.totalMs + elapsed, count: prev.count + 1 })
    },
    devtools: {
      setup(ctx) {
        // TODO: 1. Register the Dock entry
        // TODO: 2. Register an RPC `transform-timer:list` returning the top-10 timings
      },
    },
  }
}