portfolio Anshul Bisen
ask my work

Upgrading to Next.js 16 inside a Payload CMS monorepo was harder than expected

Cache Components and React Compiler 1.0 are compelling, but upgrading inside a CMS-coupled codebase surfaced dependency conflicts that deserved their own sprint.

Next.js 16 shipped with Cache Components and stable React Compiler 1.0 integration. Both features are meaningful improvements. Cache Components give you fine-grained control over server-side caching at the component level. The React Compiler eliminates the need for manual memoization. On paper, the upgrade path looked clean.

Inside a Payload CMS monorepo, it was not clean. The upgrade took a full sprint instead of the two days I estimated. Here is what went wrong and what I would do differently.

The shape of the problem before the fix.

The mature version of this lesson is that architecture only matters if it survives product change, org change, and your own boredom. It also builds on what I learned earlier in “TypeScript strict mode migration: the six-month project I wish I had done in month one.” Portfolio ended up becoming a proving ground for that idea because I deliberately used the same stack disciplines I expect in real product work.

Where product decisions met implementation reality.

The Dependency Conflict

Payload CMS bundles its own Lexical-based rich text editor. Lexical has specific React version requirements. Next.js 16 ships with React 19.1 and the stable React Compiler. The Payload Lexical editor had internal assumptions about React rendering behavior that the Compiler invalidated.

The specific issue was in Lexical’s decorator nodes. Payload uses custom Lexical nodes with React components rendered inside the editor. The React Compiler optimized these components in ways that broke Lexical’s reconciliation. The result was editor crashes when switching between rich text fields in the admin panel.

This is the kind of bug that does not show up in a fresh Next.js app. It only appears when you have a CMS with a rich text editor that embeds React components inside a content-editable DOM tree. The intersection of these three things is narrow enough that it was not caught in the Next.js or Payload pre-release testing.

The Migration Steps

Here is the actual sequence we followed, after the initial “just upgrade the dependency” attempt failed:

  1. Upgrade Next.js to 16 with the React Compiler disabled. This isolates Next.js framework changes from Compiler behavior changes. The framework upgrade alone was smooth.
  2. Run the full test suite with the Compiler disabled. All tests passed. This confirmed that the Next.js framework changes were compatible with our codebase.
  3. Enable the React Compiler on the frontend application only, excluding the Payload admin panel. The Compiler config supports a filter function that can exclude specific directories.
  4. Identify and fix the Lexical compatibility issues in the admin panel. This required pinning two Lexical packages to versions that Payload had tested against the Compiler.
  5. Enable the Compiler on the admin panel with the patched Lexical packages. Run the admin panel test suite.
  6. Test Cache Components on three high-traffic pages. Verify that server-side caching behaves correctly with our CDN layer.

Steps one and two took half a day. Steps three through five took three days. Step six took another day. The total was a full sprint when you include the initial failed attempt and the debugging time.

Lessons for CMS-Coupled Codebases

Framework upgrades in CMS-coupled codebases are fundamentally different from upgrades in standalone applications. The CMS introduces a second set of framework opinions that may conflict with the new version.

  • Always upgrade the framework and the CMS separately. Never try to upgrade both in a single PR. Isolate variables.
  • The CMS admin panel and the frontend application should be independently configurable for framework features like the React Compiler. If they are not, build that separation before upgrading.
  • Rich text editors are the most fragile integration point. Lexical, ProseMirror, Slate, TipTap. Any editor that embeds React components inside a content-editable context will be sensitive to React rendering changes.
  • Budget a full sprint for major framework upgrades in CMS monorepos. Not because the work takes a sprint, but because the debugging does.

Was It Worth It

Cache Components alone justified the upgrade. We added component-level caching to our blog listing page, case study pages, and the homepage hero section. Cold render times dropped by 60% on cached pages. The React Compiler eliminated roughly 200 lines of manual useMemo and useCallback calls. The codebase is cleaner.

What changed once the system matured.

Earlier in this story I was mostly trying to survive the blast radius myself. Here I was trying to design a system where the team did not need heroics in the first place. The same philosophy now shapes portfolio, pipeline-sdk, and dotfiles.

Framework upgrades in CMS-coupled codebases deserve their own sprint. The upgrade itself is rarely hard. The dependency conflicts and integration edge cases are what consume the time.

The mistake I made was estimating this like a standalone Next.js upgrade. The Payload CMS coupling adds a multiplier that I will account for next time. If your application is tightly coupled to a CMS, ORM, or any framework that has its own React rendering opinions, budget accordingly. The compounding complexity is real, and pretending it is a two-day task creates unnecessary pressure on the engineers doing the work.

The monorepo upgrade exposed dependency conflicts that had been silently accumulating for months. Payload CMS and Next.js shared transitive dependencies with incompatible version ranges, and the monorepo structure made those conflicts visible in ways that separate repositories would have hidden. Resolving them took longer than the framework upgrade itself, but the result was a cleaner dependency tree and a build pipeline that actually reflected the true state of our dependency graph. Monorepos do not create dependency problems. They reveal them, which is exactly what you want before deploying to production. That visibility alone justified the monorepo structure for our team.