Next.js 15.3: Turbopack in production, instrumentation‑client.ts, and navigation hooks that solve real pain points
Turbopack Builds (alpha)
The biggest drawback of Turbopack (in my opinion) has been fixed — it is now available in production.
Previously there was no point in maintaining two different bundlers: one for dev and another for production. Starting with version 15.3 everything changes: an alpha command has appeared:
next build --turbopack
The Next.js team still does not recommend using this for mission‑critical apps, but for new projects I’m already switching to it myself and encourage you to consider migrating. It really speeds up builds, reduces bundle size, and boosts performance.
instrumentation‑client.js|ts
New file: This file lets you run scripts before your web‑app code starts executing.
Place it in the project root
folder or in the app/
/ pages/
directories.
Typical use cases: integrating monitoring or logging tools. For example, Vercel OpenTelemetry:
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel('next-app')
}
Because this file runs in every runtime, you can specify which code runs in which environment:
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation-node')
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./instrumentation-edge')
}
}
onNavigate
and useLinkStatus
New navigation hooks: Their goal is to give you finer control over routing and client‑side side effects.
onNavigate
— a new prop for the <Link>
component from next/link
.
It’s similar to onClick
, but does not fire on:
Ctrl/Cmd + Click
- external URLs
- links with the
download
attribute
You can cancel navigation with e.preventDefault()
.
Recommended for events that must happen in the actual client session: animations, navigation guards, analytics.
Example: blocking a page change if the user has unsaved form data:
'use client'
import Link from 'next/link'
import { useNavigationBlocker } from '../contexts/navigation-blocker'
interface CustomLinkProps extends React.ComponentProps<typeof Link> {
children: React.ReactNode
}
export function CustomLink({ children, ...props }: CustomLinkProps) {
const { isBlocked } = useNavigationBlocker()
return (
<LinkonNavigate={(e) => {
if (
isBlocked &&
!window.confirm('You have unsaved changes. Leave anyway?')
) {
e.preventDefault()
}
}}
{...props}
>
{children}
</Link>
)
}
useLinkStatus
A hook that returns { pending }
for local loading indicators during page transitions (modelled after React’s useFormStatus
).
Conditions for proper operation:
- the component using
useLinkStatus
must be inside a<Link>
prefetching
is disabled or already in progress, meaning navigation is blocked- the destination route is dynamic and does not contain
loading.js
, which would make navigation instantaneous - not supported in the pages router
Example with a custom link:
import Link, { LinkProps } from 'next/link'
import LoadingIndicator from './loading-indicator'
const CustomLinkWithLoader = (
props: LinkProps & { children: React.ReactNode }
) => (
<Link {...props}>
{props.children}
<LoadingIndicator />
</Link>
)
export default CustomLinkWithLoader
'use client'
import { useLinkStatus } from 'next/link'
export default function LoadingIndicator() {
const { pending } = useLinkStatus()
return pending ? (
<div role="status" aria-label="Loading" className="loader" />
) : null
}
I’ve already added useLinkStatus
to my project: instead of a loader next to the link I now show a thin progress bar at the top of the page — the UX feels noticeably better.
There are other minor changes as well; see the official Next.js 15.3 release post and the linked PRs for details.
Conclusion
The update is genuinely useful, and I’ve already started using several features in my projects — Turbopack for production builds and the useLinkStatus
hook are especially delightful.