The Next.js 16 stuff teams still skip:
- cacheComponents + 'use cache'
- cacheLife / cacheTag / updateTag
- Suspense holes for PPR
- Server Actions instead of form route handlers
- await params and searchParams
- proxy.ts instead of middleware.ts
- forbidden() / unauthorized()
- next/font + Image priority on the LCP
- after() for non-blocking work
- generateMetadata
Ship one of these this week. Stop wrapping a fetch in useEffect.
#NextJS #WebDev
Parallel routes are slots, not nested pages you hope will line up.
app/@modal/(.)photo/[id]/page.tsx
app/@feed/page.tsx
app/layout.tsx // receives { modal, feed, children }
default.tsx is required or a hard refresh 404s the slot. Intercepting routes (.) keep the list mounted while the photo opens as a modal.
#NextJS #WebDev
generateStaticParams is the static set, not a cache hint.
export async function generateStaticParams() {
const posts = await db.post.listSlugs()
return posts.map((p) => ({ slug: p.slug }))
}
Build knows those URLs. Unknown slugs can still render dynamically. Empty array means "do not prerender this segment."
#NextJS #WebDev
connection() opts a subtree into request time. On purpose.
import { connection } from 'next/server'
export async function Personalized() {
await connection()
const session = await cookies()
return
{session.get('uid')?.value}
} Calling cookies() or headers() already does this. connection() is the explicit version when the dynamic bit is not a request API. Do not sprinkle it on the layout. #NextJS #WebDevafter() is the work that should not block the response.
import { after } from 'next/server'
export async function POST(request: Request) {
const payload = await request.json()
after(() => logEvent(payload))
return Response.json({ ok: true })
}
Analytics, audit rows, email enqueue. The client already has the answer. Do not await the side effect on the critical path.
#NextJS #WebDev
proxy.ts replaced middleware.ts.
// proxy.ts
export function proxy(request: NextRequest) {
const token = request.cookies.get('session')
if (!token) return NextResponse.redirect(new URL('/login', request.url))
return NextResponse.next()
}
Auth gate and rewrites live here. Do not fetch your database in the proxy. Check a cookie, then do the work in the server component.
#NextJS #WebDev
forbidden() and unauthorized() are the status, not a redirect to /login disguised as a 200.
import { forbidden, unauthorized } from 'next/navigation'
if (!session) unauthorized()
if (!session.isAdmin) forbidden()
Pair with unauthorized.tsx and forbidden.tsx. A redirect() to the login page is navigation. These are HTTP.
#NextJS #WebDev
params and searchParams are promises. Await them.
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>
searchParams: Promise<{ q?: string }>
}) {
const { slug } = await params
const { q } = await searchParams
return
}
Sync access is the Next 15 warning that became a break. Await at the edge of the page.
#NextJS #WebDev
generateMetadata is the head tag.
export async function generateMetadata({ params }: Props): Promise {
const { slug } = await params
const post = await getPost(slug)
return { title: post.title, description: post.excerpt }
}
Do not hand-roll in a client component. Open Graph and canonical live here too.
#NextJS #WebDev
next/font kills the layout shift.
import { Geist } from 'next/font/google'
const geist = Geist({ subsets: ['latin'] })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return {children}
}
Self-hosted at build. No Google CSS request on the critical path. Variable fonts go in the same call.
#NextJS #WebDev
cacheTag plus updateTag is read-your-writes.
async function getUser(id: string) {
'use cache'
cacheTag(`user-${id}`)
return db.user.find(id)
}
'use server'
export async function save(id: string, input: Profile) {
await db.user.update(id, input)
updateTag(`user-${id}`)
}
revalidateTag waits. updateTag expires and reads fresh in the same action.
#NextJS #WebDev
'use cache' is how you mark data cached in Next.js 16.
async function Projects({ userId }: { userId: string }) {
'use cache'
cacheLife('hours')
return db.projects.list(userId)
}
Enable cacheComponents. Arguments are the cache key. cookies() and headers() do not belong inside the scope. Pass the id in.
#NextJS #WebDev
Server Components are the default. 'use client' is the opt-in.
// app/price.tsx — no directive, runs on the server
export async function Price({ sku }: { sku: string }) {
const row = await db.product.find(sku)
return
{row.amount}
} Fetch in the component. Do not lift every leaf into a client bundle so a button can be clickable. #NextJS #WebDev



















