If you've tried to set up code coverage for a Next.js 13+ application with the App Router, you've likely hit a wall. The traditional approach — using Istanbul with babel-plugin-istanbul — simply doesn't work anymore. Here's why, and what you can do about it.
The Traditional Approach: Istanbul + Babel
For years, Istanbul has been the standard for JavaScript code coverage. It works by instrumenting your code at build time — injecting counter variables that track which lines, branches, and functions are executed.
// Original code
function add(a, b) {
return a + b;
}
// After Istanbul instrumentation
function add(a, b) {
cov_1234.f[0]++; // Function counter
cov_1234.s[0]++; // Statement counter
return a + b;
}
This approach requires a Babel plugin (babel-plugin-istanbul) to transform your code during the build process. It worked great with Create React App, older Next.js versions, and any project using Babel as its compiler.
The Problem: Next.js Moved to SWC
Starting with Next.js 12, and becoming the default in Next.js 13+, the framework switched from Babel to SWC (Speedy Web Compiler) for transpilation. SWC is written in Rust and is significantly faster than Babel.
But here's the catch: Istanbul's instrumentation plugin only works with Babel.
When you try to add a .babelrc file to enable Istanbul, Next.js detects it and falls back to Babel — but this:
- Disables SWC's performance benefits
- Can break other SWC-dependent features
- Often causes configuration conflicts with Next.js's internal setup
The App Router Makes It Worse
Next.js 13 introduced the App Router with React Server Components (RSC). You might think: "I'll just add a .babelrc and force Babel mode." But the App Router creates a critical problem:
Server Actions Break with Babel
When you enable Babel in a Next.js project that uses Server Actions, the build fails. Server Actions require special compiler handling that SWC provides but Babel doesn't support correctly.
// app/actions.ts
'use server'
export async function submitForm(formData: FormData) {
await db.insert(formData);
}
Try to build this with a .babelrc file present:
$ npm run build
# Build fails with Server Action compilation errors
This isn't a configuration issue — it's a fundamental incompatibility. Next.js's Server Action transformation relies on SWC-specific features that babel-plugin-istanbul can't work with.
The Catch-22
You're stuck in an impossible situation:
-
Istanbul requires Babel → Add
.babelrcto enable instrumentation - Server Actions require SWC → Build fails with Babel enabled
- No coverage for Server Actions → Can't measure what your E2E tests exercise
The Common Workaround: Skip E2E Coverage Entirely
Many teams simply give up on E2E coverage and rely only on unit tests:
Unit tests (Vitest/Jest) → Istanbul coverage ✓
E2E tests (Playwright/Cypress) → No coverage ✗
Problems:
- Server Components remain completely untested for coverage
- No visibility into what E2E tests actually exercise
- Coverage reports show artificially low numbers
The Solution: V8 Native Coverage
The fundamental issue is that Istanbul requires pre-instrumentation of source code. But there's another approach: runtime coverage collection.
V8, the JavaScript engine that powers Node.js and Chrome, has built-in coverage support. It tracks code execution at the engine level — no instrumentation needed.
How V8 Coverage Works
-
For Node.js (server): Set
NODE_V8_COVERAGE=./coverageenvironment variable - For Chrome (browser): Use Chrome DevTools Protocol (CDP) to enable coverage
- After tests: Read the coverage data from V8's native format
This approach works with any bundler, any compiler (Babel, SWC, esbuild), and any framework. The code runs exactly as it would in production — V8 just tracks what gets executed.
The Critical Role of Source Maps
There's one catch: V8 coverage reports line/column positions in the bundled code, not your original TypeScript/JSX files. To get meaningful coverage reports, you need source maps.
// next.config.ts
const nextConfig = {
productionBrowserSourceMaps: !!process.env.E2E_MODE,
webpack: (config, { isServer }) => {
if (process.env.E2E_MODE) {
config.devtool = 'source-map'
}
return config
},
}
export default nextConfig
Then build with the environment variable:
E2E_MODE=true npm run build
With source maps, the coverage tool can:
- Map bundled code positions back to original source files
- Show coverage for your TypeScript/JSX, not minified JavaScript
- Produce reports that reference your actual codebase
This is why nextcov requires source maps in your production build — without them, you'd only see coverage for unreadable bundled code.
V8 Coverage with Next.js
For Next.js applications, you need to collect coverage from multiple processes:
| Process | Coverage Method |
|---|---|
| Next.js Server (RSC, Server Actions) |
NODE_V8_COVERAGE env var |
| Browser (Client Components) | Playwright CDP integration |
| Test Runner | Orchestrates collection |
This is exactly what nextcov does. It:
- Collects V8 coverage from both server and browser during Playwright E2E tests
- Uses source maps to map bundled code back to original TypeScript/JSX
- Converts V8 format to Istanbul format for familiar reports
- Merges with unit test coverage for a complete picture
Getting Started with V8 Coverage
If you're ready to move beyond Istanbul for your Next.js App Router application:
npm install nextcov --save-dev
npx nextcov init
The init command sets up everything you need:
- Global setup/teardown for Playwright
- CDP connection to Next.js server
- Source map processing
- Report generation
Then run your E2E tests:
# Build with source maps
E2E_MODE=true npm run build
# Start server with coverage and run tests
NODE_V8_COVERAGE=.v8-coverage NODE_OPTIONS='--inspect=9230' npm start &
npx playwright test
Conclusion
Istanbul served us well for over a decade, but the JavaScript ecosystem has evolved. With:
- SWC replacing Babel as the default compiler
- Server Actions requiring SWC-specific transformations that break Babel
...it's time for a new approach.
V8 native coverage works with modern tooling, not against it. It collects accurate coverage data from both client and server without requiring any code transformation.
If you're building with Next.js App Router and want complete coverage visibility, check out nextcov.
This is part 2 of a series on test coverage for modern React applications:
- nextcov - Collecting Test Coverage for Next.js Server Components
- Why Istanbul Coverage Doesn't Work with Next.js App Router (this article)
- V8 Coverage vs Istanbul: Performance and Accuracy (coming soon)
- V8 Coverage Limitations and How to Work Around Them (coming soon)
- How to Merge Vitest Unit and Component Test Coverage (coming soon)
- E2E Coverage in Next.js: Dev Mode vs Production Mode (coming soon)
Top comments (0)