Back to Next Js

instrumentation-client.js

docs/01-app/03-api-reference/03-file-conventions/instrumentation-client.mdx

16.3.09.4 KB
Original Source

The instrumentation-client.js|ts file allows you to add monitoring, analytics code, and other side-effects that run before your application becomes interactive. This is useful for setting up performance tracking, error monitoring, polyfills, or any other client-side observability tools.

To use it, place the file in the root of your application or inside a src folder.

Usage

Unlike server-side instrumentation, you do not need to export any specific functions. You can write your monitoring code directly in the file:

ts
// Set up performance monitoring
performance.mark('app-init')

// Initialize analytics
console.log('Analytics initialized')

// Set up error tracking
window.addEventListener('error', (event) => {
  // Send to your error tracking service
  reportError(event.error)
})
js
// Set up performance monitoring
performance.mark('app-init')

// Initialize analytics
console.log('Analytics initialized')

// Set up error tracking
window.addEventListener('error', (event) => {
  // Send to your error tracking service
  reportError(event.error)
})

Error handling: Implement try-catch blocks around your instrumentation code to ensure robust monitoring. This prevents individual tracking failures from affecting other instrumentation features.

Router navigation tracking

You can export onRouterTransitionStart to observe the start of App Router navigations:

ts
export function onRouterTransitionStart(
  url: string,
  navigationType: 'push' | 'replace' | 'traverse'
) {
  console.log(url, navigationType)
}

Additional router transition information is experimental. Enable it to receive a third event argument:

ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    instrumentationClientRouterTransitionEvents: true,
  },
}

export default nextConfig

The event includes the transition metadata and source context known when the navigation is dispatched:

ts
import type { RouterTransitionStartEvent, RouterTransitionType } from 'next'

export function onRouterTransitionStart(
  url: string,
  navigationType: RouterTransitionType,
  { id, timestamp, fromRoutes, prefetchIntent }: RouterTransitionStartEvent
) {
  console.log(id, timestamp, url, navigationType, fromRoutes, prefetchIntent)
}
js
export function onRouterTransitionStart(url, navigationType, event) {
  console.log(
    event.id,
    event.timestamp,
    url,
    navigationType,
    event.fromRoutes,
    event.prefetchIntent
  )
}

onRouterTransitionStart receives:

  • url: string - The URL being navigated to
  • navigationType: 'push' | 'replace' | 'traverse' - The type of navigation
  • event.id - An opaque ID shared by events for this transition
  • event.timestamp - A framework-captured Unix timestamp in milliseconds
  • event.fromRoutes - Route patterns visible before navigation. The primary children route is first, followed by parallel slots in deterministic order
  • event.prefetchIntent - For link navigations, whether the clicked link requested full prefetching (full), used automatic prefetching (auto), or did not request prefetching (none). For navigations with no associated link (programmatic router.push()/router.replace(), or browser back/forward) this is null, since no link prefetch intent applies

Route entries use filesystem-style patterns, so a navigation away from /blog/hello may report /blog/[slug].

Hook errors are isolated and do not affect navigation or other hooks.

Performance considerations

Keep instrumentation code lightweight.

Next.js monitors initialization time in development and will log warnings if it takes longer than 16ms, which could impact smooth page loading.

Execution timing

The instrumentation-client.js file executes at a specific point in the application lifecycle:

  1. After the HTML document is loaded
  2. Before React hydration begins
  3. Before user interactions are possible

This timing makes it ideal for setting up error tracking, analytics, and performance monitoring that needs to capture early application lifecycle events.

Only synchronous, top-level code is guaranteed to complete before hydration. Asynchronous work started here (a Promise, import(), or top-level await) is not awaited and may resolve after hydration has begun, so treat it as fire-and-forget. When something must be in place before your components run, use one of the synchronous patterns in Polyfills.

See also

next.config.js plugins (for example, wrappers like withSentry) can register their own client instrumentation module via the instrumentationClientInject option. Injected modules run before this file, in array order, and may export the same router transition start hook. Application code should continue to use this file convention directly.

Examples

Error tracking

Initialize error tracking before React starts and add navigation breadcrumbs for better debugging context.

ts
import Monitor from './lib/monitoring'

Monitor.initialize()

export function onRouterTransitionStart(url: string) {
  Monitor.pushEvent({
    message: `Navigation to ${url}`,
    category: 'navigation',
  })
}
js
import Monitor from './lib/monitoring'

Monitor.initialize()

export function onRouterTransitionStart(url) {
  Monitor.pushEvent({
    message: `Navigation to ${url}`,
    category: 'navigation',
  })
}

Analytics tracking

Initialize analytics and track navigation events with detailed metadata for user behavior analysis.

ts
import { analytics } from './lib/analytics'

analytics.init()

export function onRouterTransitionStart(url: string, navigationType: string) {
  analytics.track('page_navigation', {
    url,
    type: navigationType,
    timestamp: Date.now(),
  })
}
js
import { analytics } from './lib/analytics'

analytics.init()

export function onRouterTransitionStart(url, navigationType) {
  analytics.track('page_navigation', {
    url,
    type: navigationType,
    timestamp: Date.now(),
  })
}

Performance monitoring

Track Time to Interactive and navigation performance using the Performance Observer API and performance marks.

ts
const startTime = performance.now()

const observer = new PerformanceObserver(
  (list: PerformanceObserverEntryList) => {
    for (const entry of list.getEntries()) {
      if (entry instanceof PerformanceNavigationTiming) {
        console.log('Time to Interactive:', entry.loadEventEnd - startTime)
      }
    }
  }
)

observer.observe({ entryTypes: ['navigation'] })

export function onRouterTransitionStart(url: string) {
  performance.mark(`nav-start-${url}`)
}
js
const startTime = performance.now()

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry instanceof PerformanceNavigationTiming) {
      console.log('Time to Interactive:', entry.loadEventEnd - startTime)
    }
  }
})

observer.observe({ entryTypes: ['navigation'] })

export function onRouterTransitionStart(url) {
  performance.mark(`nav-start-${url}`)
}

Polyfills

As noted in Execution timing, only synchronous, top-level code here runs before hydration, so a conditional import() or top-level await may resolve after hydration has already started.

To guarantee a polyfill is applied before your components run, statically import it and apply it synchronously after feature detection. Because the import is static, the polyfill ships to every visitor:

ts
import ResizeObserverPolyfill from './lib/polyfills/resize-observer'

if (!window.ResizeObserver) {
  window.ResizeObserver = ResizeObserverPolyfill
}

Avoid loading a polyfill with a conditional import() here. The import is fire-and-forget, so the polyfill may be applied after hydration has already begun, which can be too late for your components:

ts
// Avoid: the dynamic import is fire-and-forget, so `ResizeObserver`
// may still be undefined when your components run.
if (!window.ResizeObserver) {
  import('./lib/polyfills/resize-observer').then((mod) => {
    window.ResizeObserver = mod.default
  })
}

For anything loaded on demand, prefer polyfilling in the code that uses the feature. See Custom Polyfills for that and other strategies.

Next.js already injects a baseline of widely used polyfills (such as fetch, URL, and Object.assign) for the browsers that need them, so you only need to add polyfills for features outside that baseline.

Version history

VersionChanges
v16.3.0Experimental router transition start event introduced
v15.3instrumentation-client introduced