DEV Community

Cover image for Migrating Yarn 1 to 4 in an Nx Monorepo
Kévin Huang for Wecasa

Posted on

Migrating Yarn 1 to 4 in an Nx Monorepo

With just one migration, we reduced the execution time of yarn install on our CI from ~110s to ~25s. While also removing legacy patching with postinstall and by also keeping behaviour close enough to Yarn 1 to ensure a safe migration.


At Wecasa, our frontend lives in a large Nx monorepo (web + mobile apps) and we were still running on Yarn 1.22.0. It worked well for a while, but at some point the dependency installation became a bottleneck on our CI. And our patch workflow (patch-package + postinstall-postinstall) was adding maintenance overhead a well.

So we decided to migrate to Yarn 4, but with one strict goal: minimize risk. Instead of changing everything at once, we focused on a compatibility-first setup (especially important because React Native / Expo still requires node_modules) and planned optimizations for later iterations.

1) Migration Strategy: Stability First, Optimization Later

We followed the official guide: Yarn migration guide , with a progressive approach:

corepack enable
yarn set version berry
yarn install
Enter fullscreen mode Exit fullscreen mode

Key decision: keep behavior as close as possible to Yarn 1 during the first step.

That meant postponing advanced hoisting changes and avoiding Plug’n’Play for now.

Plug’n’Play makes Yarn resolve packages via a .pnp.cjs map instead of a full node_modules folder but not always compatible with every toolchain (e.g. React Native / Expo still require using typical node_modules install).

Our .yarnrc.yml:

nmHoistingLimits: none
nodeLinker: node-modules
patchFolder: .yarn/patches
yarnPath: .yarn/releases/yarn-4.12.0.cjs
Enter fullscreen mode Exit fullscreen mode

Why this setup:

  • nodeLinker: node-modules keeps compatibility with React Native / Expo.
  • nmHoistingLimits: none avoids introducing too many resolution differences on day one.
  • patchFolder and yarnPath make Yarn behavior explicit and reproducible across environments.

As documentation by Yarn

A notable exception is React Native / Expo, which require using typical node_modules installs.

2) Lockfile and Repository Changes

After switching versions, we ran yarn install to regenerate the yarn.lock file with a Yarn 4 format while preserving dependency versions.

This gave us a clean, deterministic baseline before any deeper dependency cleanup.

We also updated git ignore rules by adding this file: .yarn/install-state.gz

As documented by Yarn , this file is only an optimization artifact and should not be committed.

.yarn/install-state.gz is an optimization file that you shouldn't ever have to commit. It simply stores the exact state of your project so that the next commands can boot without having to resolve your workspaces all over again.

3) Migrating Existing Patches from patch-package to Yarn Native Patches

Before migration, our patches lived under <root>/patches and were applied via postinstall scripts.

With Yarn 4, we moved to the native patch workflow, and new patches now live in .yarn/patches.

Step-by-step for each existing patch

  1. Start an editable patch session:
yarn patch <package-name>
Enter fullscreen mode Exit fullscreen mode
  1. Move to the temporary folder shown in the command output:
cd <temp-folder>
Enter fullscreen mode Exit fullscreen mode
  1. Apply your existing patch file:
patch -p<number> < /absolute/path/to/patches/<package>.patch
Enter fullscreen mode Exit fullscreen mode
  1. Commit the patch back into Yarn:
yarn patch-commit --save <temp-folder>
Enter fullscreen mode Exit fullscreen mode

About -p<number>

The -p value depends on how many path segments must be stripped from your patch paths.

Example patch path:

diff --git a/node_modules/@types/react/path/to/file.js
Enter fullscreen mode Exit fullscreen mode

In this case, -p4 strips a/node_modules/@types/react, so paths match files in the temp patch directory.

4) What Changed in package.json

Once committed, dependencies patched by Yarn are rewritten like this:

- "my-package-name": "52.0.6",
+ "my-package-name": "patch:my-package-name@npm%3A52.0.6#~/.yarn/patches/my-package-name-npm-52.0.6-20053456a7.patch",
Enter fullscreen mode Exit fullscreen mode

This removes the need for patch-package + postinstall patch execution and centralizes patch handling in Yarn itself.

5) Results and Next Iterations

Immediate gains after the migration:

  • CI install time dropped from ~110s to ~25s
  • patching workflow simplified (no more postinstall patch mechanism)
  • safer long-term setup with modern Yarn tooling

What we’ll iterate on next:

  • refine hoisting strategy (nmHoistingLimits)
  • audit dependency constraints and deduplication opportunities
  • progressively adopt more Yarn 4 capabilities where ecosystem compatibility allows

Conclusion

Migrating Yarn 1 to Yarn 4 in a large Nx monorepo can be low-risk if you prioritize compatibility first.

We got a 4x CI install speed-up and cleaner patch management in one pass, while keeping mobile compatibility intact.

If you enjoy pragmatic monorepo migration stories and performance wins, follow Wecasa’s engineering journey.

Top comments (0)