すべての記事エンジニアリング

Next.js 16 を本番投入して分かったこと

React Compiler、新しいキャッシュの挙動、そしてクライアント案件で踏んだ3つの移行の落とし穴。

公開日
2026年7月11日
読了時間
7 分
カテゴリ
エンジニアリング
著者
Mohammed Banani

Most framework upgrades are a version bump and a coffee. Next.js 16 needed a plan and a rollback branch.

React Compiler earns its keep

The headline feature is the one you stop noticing fastest. With the React Compiler turned on, most of the useMemo and useCallback calls we used to write by hand became noise the compiler now handles for us. We deleted a pile of memoization, the app got faster, and the code got shorter, because the compiler is better at this than a tired engineer at 6pm.

The change is quiet and it is everywhere. You stop reasoning about dependency arrays and start writing the obvious version of the component, and the build memoizes it correctly on your behalf.

components/WorkIndex.tsx
// Before: hand-rolled memoization, dependency arrays to keep in sync
const sorted = useMemo(() => sortProjects(projects), [projects]);
const onSelect = useCallback((id: string) => setActive(id), []);

// After: write the obvious version. The compiler memoizes it for you.
const sorted = sortProjects(projects);
const onSelect = (id: string) => setActive(id);

It is not free everywhere. The compiler reasons about pure render logic, and our WebGL layer is the opposite of that: refs, imperative animation loops, and Three.js objects that live outside React's world. Those components need a clear boundary so the compiler leaves the imperative parts alone. The escape hatch is one line.

The escape hatch for imperative code
function OrbCanvas() {
  "use no memo"; // opt this component out of the compiler

  // imperative Three.js: refs, a rAF loop, objects React never sees
  const ref = useRef<HTMLCanvasElement>(null);
  useEffect(() => startRenderLoop(ref.current), []);
  return <canvas ref={ref} />;
}
On our site
The imperative half of this site

The molten orb and the WebGL work that had to stay outside the compiler's reach.

The TypeScript snag that broke the build

This is the one that cost us an afternoon. Next.js runs its built-in type check by loading TypeScript as a JavaScript module and calling into its compiler API. That worked for years. Then TypeScript 7 shipped.

TypeScript 7 is the Go rewrite, and the typescript package now ships the Go compiler in place of the old JavaScript one. The JavaScript compiler API that Next reaches for is gone from the package, so on a fresh install the production build decided TypeScript was not installed at all, and failed at the type-check step with an error that pointed at neither our code nor an obvious fix.

The workaround is boring and correct. Tell Next to stop type-checking during the build, and run the TypeScript CLI as its own step in the pipeline instead.

next.config.ts
const nextConfig: NextConfig = {
  // Next 16 type-checks via a JS compiler API that TS 7 removed.
  // Defer type-checking to the CLI until Next ships native support.
  typescript: { ignoreBuildErrors: true },
};
package.json
{
  "scripts": {
    "build": "pnpm typecheck && next build",
    "typecheck": "tsc --project tsconfig.typecheck.json"
  }
}

This one is temporary. Native type-checking with the Go compiler is on the way, and when it lands this section becomes a paragraph of history. Until then, if your build breaks on the upgrade with a cryptic compiler error, start here.

Caching is opt-in now, and that is good

Older Next.js cached aggressively and asked you to opt out when it guessed wrong. Sixteen flips the default. Less is cached automatically, and you reach for caching on purpose, in code you can see.

Next.js 15 & earlier
Cached by default

Aggressive caching you had to notice and opt out of when it guessed wrong.

default: cache
Next.js 16
Explicit by default

Renders fresh unless you opt in, on purpose, where the caching is visible in the code.

"use cache"
The caching default, flipped

The mechanism is the use cache directive. You mark a function, a component, or a whole route as cacheable, and the compiler derives the cache key for you. Lifetime and invalidation are explicit, sitting right next to the thing they govern.

Explicit caching with use cache
import { cacheLife, cacheTag } from "next/cache";

async function getPlans() {
  "use cache";
  cacheLife("hours");   // how long it stays fresh
  cacheTag("pricing");  // how you invalidate it later
  return db.query.plans.findMany();
}

For a marketing site that is mostly static, this was close to a non-event. For anything with per-request data it meant walking every route and deciding, out loud, what renders once and what renders per request. That audit is a cost, but a one-time one, and the result is a mental model you can actually hold. On a site like ours, seven locales across every route, being explicit beats a clever default we would have had to remember to fight.

The trap in the other direction is easy to hit too. Mark a route use cache while it quietly reads a cookie or a header, and you have cached something that was meant to be per-user. The upside of the explicit model is that this now shows up in a diff and is obvious in review, instead of an emergent behaviour you discover in production after someone sees another account's data.

Would we do it again

Yes, and we already have, on client work. The compiler alone pays for the upgrade in code you no longer write. The rest is manageable if you go in with a plan instead of a hope.

  1. Budget an afternoon for the TypeScript step, and wire type-checking into your build script so nothing slips through.
  2. Read the caching notes before you touch anything with a database, then walk your routes one at a time.
  3. Keep a rollback branch open until the first production deploy is green.

The site you are reading this on runs Next.js 16 with the compiler on, across seven locales, with a WebGL layer that had to be taught to coexist with it. None of the three snags were dealbreakers. They were the parts the changelog is too polite to warn you about.

On our site
What we ship on this stack

The client projects running on the same tools, in production.

次のエッセイ

MVPのスコープという罠(と、そこから抜け出す方法)

プロダクト · 6 分
考えているプロジェクトはありますか?

あなたが つくっているものについて、話しましょう。

一緒に取り組む →