Logo

Upgrading to Next.js 16 with Turbopack: Three Upstream Bugs I Hit (and Filed)

☕ 4 min read

Table of Contents

TL;DR

Next.js 16 promotes Turbopack to the default bundler for both next dev and next build. The build speedup is real (my compile dropped from around 39s on webpack to under 12s). But Turbopack is newer than webpack, and the rough edges are concentrated exactly where build time and runtime disagree, or where a package does something too dynamic to trace statically.

Here are the three genuine upstream bugs I hit, each with the issue tracking it. Two were already reported. One was not, so I filed it with a minimal reproduction.

Problem 1: Turbopack ignores a runtime assetPrefix

This one cost me the most time because it fails completely silently.

A very common deployment pattern is to build the app once into an immutable image, then inject the CDN URL at deploy time through an environment variable:

1// next.config.ts
2const assetPrefix = process.env.CDN_URL ?? ''
3
4export default { assetPrefix }

On webpack this works: the client runtime reads the prefix at runtime and loads chunks from the CDN. On Turbopack it does not. Turbopack bakes the assetPrefix into the client bundle at build time and ignores a different value supplied only at runtime.

The failure mode is nasty because nothing looks broken:

Under the hood, webpack's client runtime reassigns __webpack_public_path__ from the runtime value, so it honors it. Turbopack's equivalent hook is effectively a no-op, so the runtime value is dropped and the chunk loader keeps using the build-time prefix.

The fix is to make the prefix known at build time (pass CDN_URL to next build, not only to the running container), so the baked value and the runtime value match. If you cannot do that, next build --webpack still works the old way.

I could not find this reported, so I built a minimal reproduction and filed vercel/next.js#100303, plus a PR (#100306) that at least makes the mismatch fail loudly instead of silently.

Problem 2: pino breaks the Turbopack build

My production build failed with:

1Error: Cannot find module './transport-stream'
2 at .../node_modules/pino/lib/worker.js

The culprit was pino, pulled into the server bundle transitively through a logger. pino starts a worker thread and resolves its transport through a dynamic require whose path Turbopack's static tracer cannot follow. webpack tolerated the same pattern; Turbopack does not.

This is not pino-specific. It is a whole class of "externalized package does a dynamic require at runtime" failures that also hits OpenTelemetry, styled-components, and next-auth under Turbopack. The practical workarounds are to force Turbopack to include the missing file explicitly, or, if you do not actually depend on that logger's real transports in the bundled path, alias it to a lighter build:

1// next.config.ts
2export default {
3 turbopack: {
4 resolveAlias: { pino: 'pino/browser' },
5 },
6}

That second option is a trade-off, not a fix: it swaps real structured logging for console output in the bundle, so only reach for it when the bundled copy of the logger is something you do not consume.

Tracked upstream in vercel/next.js#87342, with the root cause filed against the library itself in pinojs/thread-stream#184.

Problem 3: Turbopack dev breaks Pages Router hydration

In development specifically, Turbopack 16.4 has a Pages Router regression. When _app and a page share a dependency, hydration fails with:

1module factory is not available

Production builds are unaffected, so this is a dev-only annoyance rather than a shipping blocker. Until the fix lands in a stable release, the simplest path is to keep next dev on webpack while leaving production on Turbopack. If you run a custom server, that is a one-line conditional:

1const app = next({
2 dev,
3 ...(dev && { webpack: true }),
4})

Tracked in vercel/next.js#99789; the fix (#98823) is merged for 16.5.

References

Related Articles