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. We followed the official guide: Yarn migration guide , with a progressive approach: corepack enable yarn set version berry yarn install 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 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. 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. patch-package to Yarn Native Patches Before migration, our patches lived under /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. Start an editable patch session: yarn patch Move to the temporary folder shown in the command output: cd Apply your existing patch file: patch -p .patch Commit the patch back into Yarn: yarn patch-commit --save -p 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 In this case, -p4 strips a/node_modules/@types/react, so paths match files in the temp patch directory. 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", This removes the need for patch-package + postinstall patch execution and centralizes patch handling in Yarn itself. 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 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.
