From 56e54baaf1c457b07fa6054ba81a63fcb5de2032 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 10:10:30 +0200 Subject: [PATCH 1/6] perf: build Jest's file map in-process and stop using watchman --- .github/workflows/e2e-tests.yml | 6 ------ .nx/version-plans/version-plan-1792138223000.md | 5 +++++ packages/bundler-metro/src/types.ts | 2 +- packages/bundler-metro/src/withRnHarness.ts | 2 +- packages/jest/src/jest-preset.ts | 7 +++++++ website/src/docs/guides/ci-cd.md | 3 --- 6 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 .nx/version-plans/version-plan-1792138223000.md diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d4a5f360..94a354f5 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -152,9 +152,6 @@ jobs: with: xcode-version: '26.4.1' - - name: Install Watchman - run: brew install watchman - - name: Install dependencies run: | pnpm install @@ -452,9 +449,6 @@ jobs: with: xcode-version: '26.4.1' - - name: Install Watchman - run: brew install watchman - - name: Install dependencies run: | pnpm install diff --git a/.nx/version-plans/version-plan-1792138223000.md b/.nx/version-plans/version-plan-1792138223000.md new file mode 100644 index 00000000..df55fbe4 --- /dev/null +++ b/.nx/version-plans/version-plan-1792138223000.md @@ -0,0 +1,5 @@ +--- +__default__: patch +--- + +Harness runs use less CPU and no longer need watchman. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change, and both Jest and Metro find files with Node's file system APIs, so CI jobs can skip installing watchman. To use watchman again, set `watchman: true` in your Jest config and `RN_HARNESS_DEBUG_USE_WATCHMAN=1` for Metro. diff --git a/packages/bundler-metro/src/types.ts b/packages/bundler-metro/src/types.ts index 897e1432..01a95ee7 100644 --- a/packages/bundler-metro/src/types.ts +++ b/packages/bundler-metro/src/types.ts @@ -63,7 +63,7 @@ export type MetroOptions = { /** * Whether Jest is running in watch mode (`--watch` / `--watchAll`). Only * then does Metro need a file watcher; a one-shot run bundles once and - * exits, so watching just costs a watchman subscription and the file-map + * exits, so watching just costs a file watcher and the file-map * auto-save timer. */ watchMode?: boolean; diff --git a/packages/bundler-metro/src/withRnHarness.ts b/packages/bundler-metro/src/withRnHarness.ts index e163fbdf..abefd6b6 100644 --- a/packages/bundler-metro/src/withRnHarness.ts +++ b/packages/bundler-metro/src/withRnHarness.ts @@ -127,7 +127,7 @@ export const withRnHarness = ( ...metroConfig.resolver, blockList: harnessBlockList, resolveRequest: harnessResolver, - useWatchman: process.env.RN_HARNESS_DEBUG_USE_WATCHMAN !== '0', + useWatchman: process.env.RN_HARNESS_DEBUG_USE_WATCHMAN === '1', }, transformer: { ...metroConfig.transformer, diff --git a/packages/jest/src/jest-preset.ts b/packages/jest/src/jest-preset.ts index 8725af39..7c2f6534 100644 --- a/packages/jest/src/jest-preset.ts +++ b/packages/jest/src/jest-preset.ts @@ -1,3 +1,10 @@ export default { runner: '@react-native-harness/jest', + // Tests run serially on the device, so Jest only uses its workers to build + // the haste map. On a cold cache or after any file change that starts a pool + // of `availableParallelism() - 1` processes, which roughly doubles Jest's + // CPU time without making the run faster. + maxWorkers: 1, + // Harness doesn't depend on watchman; Jest and Metro crawl with Node instead. + watchman: false, }; diff --git a/website/src/docs/guides/ci-cd.md b/website/src/docs/guides/ci-cd.md index 657fef02..ca22be6e 100644 --- a/website/src/docs/guides/ci-cd.md +++ b/website/src/docs/guides/ci-cd.md @@ -172,9 +172,6 @@ jobs: node-version: '24' cache: 'pnpm' - - name: Install Watchman - run: brew install watchman - - name: Install dependencies run: pnpm install From 3f64951321acc8f4cf0ab01b8e3f2fd863a00fac Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 10:40:13 +0200 Subject: [PATCH 2/6] revert: keep watchman enabled in Jest and Metro --- .github/workflows/e2e-tests.yml | 6 ++++++ .nx/version-plans/version-plan-1792138223000.md | 2 +- packages/bundler-metro/src/types.ts | 2 +- packages/bundler-metro/src/withRnHarness.ts | 2 +- packages/jest/src/jest-preset.ts | 2 -- website/src/docs/guides/ci-cd.md | 3 +++ 6 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 94a354f5..d4a5f360 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -152,6 +152,9 @@ jobs: with: xcode-version: '26.4.1' + - name: Install Watchman + run: brew install watchman + - name: Install dependencies run: | pnpm install @@ -449,6 +452,9 @@ jobs: with: xcode-version: '26.4.1' + - name: Install Watchman + run: brew install watchman + - name: Install dependencies run: | pnpm install diff --git a/.nx/version-plans/version-plan-1792138223000.md b/.nx/version-plans/version-plan-1792138223000.md index df55fbe4..300acc0b 100644 --- a/.nx/version-plans/version-plan-1792138223000.md +++ b/.nx/version-plans/version-plan-1792138223000.md @@ -2,4 +2,4 @@ __default__: patch --- -Harness runs use less CPU and no longer need watchman. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change, and both Jest and Metro find files with Node's file system APIs, so CI jobs can skip installing watchman. To use watchman again, set `watchman: true` in your Jest config and `RN_HARNESS_DEBUG_USE_WATCHMAN=1` for Metro. +Harness runs use less CPU. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change. diff --git a/packages/bundler-metro/src/types.ts b/packages/bundler-metro/src/types.ts index 01a95ee7..897e1432 100644 --- a/packages/bundler-metro/src/types.ts +++ b/packages/bundler-metro/src/types.ts @@ -63,7 +63,7 @@ export type MetroOptions = { /** * Whether Jest is running in watch mode (`--watch` / `--watchAll`). Only * then does Metro need a file watcher; a one-shot run bundles once and - * exits, so watching just costs a file watcher and the file-map + * exits, so watching just costs a watchman subscription and the file-map * auto-save timer. */ watchMode?: boolean; diff --git a/packages/bundler-metro/src/withRnHarness.ts b/packages/bundler-metro/src/withRnHarness.ts index abefd6b6..e163fbdf 100644 --- a/packages/bundler-metro/src/withRnHarness.ts +++ b/packages/bundler-metro/src/withRnHarness.ts @@ -127,7 +127,7 @@ export const withRnHarness = ( ...metroConfig.resolver, blockList: harnessBlockList, resolveRequest: harnessResolver, - useWatchman: process.env.RN_HARNESS_DEBUG_USE_WATCHMAN === '1', + useWatchman: process.env.RN_HARNESS_DEBUG_USE_WATCHMAN !== '0', }, transformer: { ...metroConfig.transformer, diff --git a/packages/jest/src/jest-preset.ts b/packages/jest/src/jest-preset.ts index 7c2f6534..a337c55c 100644 --- a/packages/jest/src/jest-preset.ts +++ b/packages/jest/src/jest-preset.ts @@ -5,6 +5,4 @@ export default { // of `availableParallelism() - 1` processes, which roughly doubles Jest's // CPU time without making the run faster. maxWorkers: 1, - // Harness doesn't depend on watchman; Jest and Metro crawl with Node instead. - watchman: false, }; diff --git a/website/src/docs/guides/ci-cd.md b/website/src/docs/guides/ci-cd.md index ca22be6e..657fef02 100644 --- a/website/src/docs/guides/ci-cd.md +++ b/website/src/docs/guides/ci-cd.md @@ -172,6 +172,9 @@ jobs: node-version: '24' cache: 'pnpm' + - name: Install Watchman + run: brew install watchman + - name: Install dependencies run: pnpm install From 5872824201b3bd9520a17c97c52389d492726746 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 10:47:36 +0200 Subject: [PATCH 3/6] perf(jest): skip Jest's dependency extraction --- .nx/version-plans/version-plan-1792138223000.md | 2 +- packages/jest/src/dependency-extractor.ts | 7 +++++++ packages/jest/src/jest-preset.ts | 5 +++++ website/src/docs/api/cli.mdx | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 packages/jest/src/dependency-extractor.ts diff --git a/.nx/version-plans/version-plan-1792138223000.md b/.nx/version-plans/version-plan-1792138223000.md index 300acc0b..1b54f505 100644 --- a/.nx/version-plans/version-plan-1792138223000.md +++ b/.nx/version-plans/version-plan-1792138223000.md @@ -2,4 +2,4 @@ __default__: patch --- -Harness runs use less CPU. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change. +Harness runs use less CPU. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change, and it no longer parses every project file for imports. As a result, `--watch` reruns test files when they change, but changes to other source files no longer trigger a rerun. diff --git a/packages/jest/src/dependency-extractor.ts b/packages/jest/src/dependency-extractor.ts new file mode 100644 index 00000000..6487fa76 --- /dev/null +++ b/packages/jest/src/dependency-extractor.ts @@ -0,0 +1,7 @@ +// Jest reads and parses every file in the project to build a dependency graph +// for `--watch`, `--onlyChanged` and `--findRelatedTests`. Harness only reruns +// test files that change themselves (native changes need an app rebuild +// anyway), so the graph is never needed and extraction is skipped. +export const extract = (): Set => new Set(); + +export const getCacheKey = (): string => 'react-native-harness-no-dependencies'; diff --git a/packages/jest/src/jest-preset.ts b/packages/jest/src/jest-preset.ts index a337c55c..02dc6bd0 100644 --- a/packages/jest/src/jest-preset.ts +++ b/packages/jest/src/jest-preset.ts @@ -1,3 +1,5 @@ +import { fileURLToPath } from 'node:url'; + export default { runner: '@react-native-harness/jest', // Tests run serially on the device, so Jest only uses its workers to build @@ -5,4 +7,7 @@ export default { // of `availableParallelism() - 1` processes, which roughly doubles Jest's // CPU time without making the run faster. maxWorkers: 1, + dependencyExtractor: fileURLToPath( + new URL('./dependency-extractor.js', import.meta.url), + ), }; diff --git a/website/src/docs/api/cli.mdx b/website/src/docs/api/cli.mdx index 940e2d4d..512b3137 100644 --- a/website/src/docs/api/cli.mdx +++ b/website/src/docs/api/cli.mdx @@ -71,7 +71,7 @@ npx react-native-harness --harnessRunner ios React Native Harness supports the most commonly used Jest flags: -- `--watch`: Watch files for changes and rerun tests related to changed files. +- `--watch`: Watch test files for changes and rerun the ones that changed. Changes to other source files don't trigger a rerun. - `--coverage`: Indicates that test coverage information should be collected and reported in the output. - `--testNamePattern, -t`: Run only tests with a name that matches the regex. - `--testPathPattern`: A regexp string that is matched against all tests paths before executing the test. From b13e5560c86e6376859a58b8d025c21e8640653f Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 11:42:29 +0200 Subject: [PATCH 4/6] Revert "perf(jest): skip Jest's dependency extraction" This reverts commit 5872824201b3bd9520a17c97c52389d492726746. --- .nx/version-plans/version-plan-1792138223000.md | 2 +- packages/jest/src/dependency-extractor.ts | 7 ------- packages/jest/src/jest-preset.ts | 5 ----- website/src/docs/api/cli.mdx | 2 +- 4 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 packages/jest/src/dependency-extractor.ts diff --git a/.nx/version-plans/version-plan-1792138223000.md b/.nx/version-plans/version-plan-1792138223000.md index 1b54f505..300acc0b 100644 --- a/.nx/version-plans/version-plan-1792138223000.md +++ b/.nx/version-plans/version-plan-1792138223000.md @@ -2,4 +2,4 @@ __default__: patch --- -Harness runs use less CPU. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change, and it no longer parses every project file for imports. As a result, `--watch` reruns test files when they change, but changes to other source files no longer trigger a rerun. +Harness runs use less CPU. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change. diff --git a/packages/jest/src/dependency-extractor.ts b/packages/jest/src/dependency-extractor.ts deleted file mode 100644 index 6487fa76..00000000 --- a/packages/jest/src/dependency-extractor.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Jest reads and parses every file in the project to build a dependency graph -// for `--watch`, `--onlyChanged` and `--findRelatedTests`. Harness only reruns -// test files that change themselves (native changes need an app rebuild -// anyway), so the graph is never needed and extraction is skipped. -export const extract = (): Set => new Set(); - -export const getCacheKey = (): string => 'react-native-harness-no-dependencies'; diff --git a/packages/jest/src/jest-preset.ts b/packages/jest/src/jest-preset.ts index 02dc6bd0..a337c55c 100644 --- a/packages/jest/src/jest-preset.ts +++ b/packages/jest/src/jest-preset.ts @@ -1,5 +1,3 @@ -import { fileURLToPath } from 'node:url'; - export default { runner: '@react-native-harness/jest', // Tests run serially on the device, so Jest only uses its workers to build @@ -7,7 +5,4 @@ export default { // of `availableParallelism() - 1` processes, which roughly doubles Jest's // CPU time without making the run faster. maxWorkers: 1, - dependencyExtractor: fileURLToPath( - new URL('./dependency-extractor.js', import.meta.url), - ), }; diff --git a/website/src/docs/api/cli.mdx b/website/src/docs/api/cli.mdx index 512b3137..940e2d4d 100644 --- a/website/src/docs/api/cli.mdx +++ b/website/src/docs/api/cli.mdx @@ -71,7 +71,7 @@ npx react-native-harness --harnessRunner ios React Native Harness supports the most commonly used Jest flags: -- `--watch`: Watch test files for changes and rerun the ones that changed. Changes to other source files don't trigger a rerun. +- `--watch`: Watch files for changes and rerun tests related to changed files. - `--coverage`: Indicates that test coverage information should be collected and reported in the output. - `--testNamePattern, -t`: Run only tests with a name that matches the regex. - `--testPathPattern`: A regexp string that is matched against all tests paths before executing the test. From 5cacf94ba5ba2c4d8fb2d1ce2a69d4fefd9c9e96 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 11:43:16 +0200 Subject: [PATCH 5/6] perf(jest): skip build and cache output when indexing files --- .nx/version-plans/version-plan-1792138223000.md | 2 +- packages/jest/src/jest-preset.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.nx/version-plans/version-plan-1792138223000.md b/.nx/version-plans/version-plan-1792138223000.md index 300acc0b..f07de327 100644 --- a/.nx/version-plans/version-plan-1792138223000.md +++ b/.nx/version-plans/version-plan-1792138223000.md @@ -2,4 +2,4 @@ __default__: patch --- -Harness runs use less CPU. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change. +Harness runs use less CPU. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change, and it no longer indexes Harness's `.harness` cache or iOS and Android build output (`ios/Pods`, `ios/build`, `android/build`, `android/.gradle`, `android/app/build`, `android/app/.cxx`). diff --git a/packages/jest/src/jest-preset.ts b/packages/jest/src/jest-preset.ts index a337c55c..6b2c4522 100644 --- a/packages/jest/src/jest-preset.ts +++ b/packages/jest/src/jest-preset.ts @@ -5,4 +5,13 @@ export default { // of `availableParallelism() - 1` processes, which roughly doubles Jest's // CPU time without making the run faster. maxWorkers: 1, + // Build and cache output never contains tests, but it can hold tens of + // thousands of files. Jest checks these patterns before descending into a + // directory, so its crawl skips them entirely. Jest appends a project's own + // patterns to the preset's. + modulePathIgnorePatterns: [ + '/\\.harness(/|$)', + '/ios/(Pods|build)(/|$)', + '/android/(build|\\.gradle|app/build|app/\\.cxx)(/|$)', + ], }; From 9f105601f54d38fc5660547835d1a9b14df757e7 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 11:54:16 +0200 Subject: [PATCH 6/6] perf(jest): find files without watchman --- .nx/version-plans/version-plan-1792138223000.md | 2 +- packages/jest/src/jest-preset.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.nx/version-plans/version-plan-1792138223000.md b/.nx/version-plans/version-plan-1792138223000.md index f07de327..2f6a9c83 100644 --- a/.nx/version-plans/version-plan-1792138223000.md +++ b/.nx/version-plans/version-plan-1792138223000.md @@ -2,4 +2,4 @@ __default__: patch --- -Harness runs use less CPU. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change, and it no longer indexes Harness's `.harness` cache or iOS and Android build output (`ios/Pods`, `ios/build`, `android/build`, `android/.gradle`, `android/app/build`, `android/app/.cxx`). +Harness runs use less CPU. Jest now builds its file map in a single process instead of starting a pool of worker processes on every cold start or file change, and it no longer indexes Harness's `.harness` cache or iOS and Android build output (`ios/Pods`, `ios/build`, `android/build`, `android/.gradle`, `android/app/build`, `android/app/.cxx`). Jest also finds files without watchman, which is now faster than querying it; Metro still uses watchman. diff --git a/packages/jest/src/jest-preset.ts b/packages/jest/src/jest-preset.ts index 6b2c4522..66a05e15 100644 --- a/packages/jest/src/jest-preset.ts +++ b/packages/jest/src/jest-preset.ts @@ -14,4 +14,8 @@ export default { '/ios/(Pods|build)(/|$)', '/android/(build|\\.gradle|app/build|app/\\.cxx)(/|$)', ], + // With the build output skipped, Jest's own crawl is cheaper than querying + // watchman, which costs two CLI process spawns per run and still lists every + // file. Metro keeps using watchman. + watchman: false, };