From b44f2f4928d597383b12409a86aba2cac49aa009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 15 Jun 2026 12:51:32 +0200 Subject: [PATCH 1/4] feat: disable owner stacks by default --- packages/measure/package.json | 3 +- .../src/__tests__/owner-stacks.test.ts | 65 +++++++++++++++++++ packages/measure/src/measure-renders.tsx | 2 + packages/measure/src/owner-stacks.ts | 12 ++++ test-apps/native/jest-setup.js | 3 - test-apps/native/package.json | 1 - yarn.lock | 2 +- 7 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 packages/measure/src/__tests__/owner-stacks.test.ts create mode 100644 packages/measure/src/owner-stacks.ts diff --git a/packages/measure/package.json b/packages/measure/package.json index 2c8b4940..45b2f6bb 100644 --- a/packages/measure/package.json +++ b/packages/measure/package.json @@ -53,7 +53,8 @@ "dependencies": { "@callstack/reassure-logger": "1.5.1", "mathjs": "^15.2.0", - "pretty-format": "^30.4.1" + "pretty-format": "^30.4.1", + "react-test-config": "^0.3.0" }, "devDependencies": { "@babel/core": "^7.29.7", diff --git a/packages/measure/src/__tests__/owner-stacks.test.ts b/packages/measure/src/__tests__/owner-stacks.test.ts new file mode 100644 index 00000000..8819542e --- /dev/null +++ b/packages/measure/src/__tests__/owner-stacks.test.ts @@ -0,0 +1,65 @@ +import * as React from 'react'; + +afterEach(() => { + jest.dontMock('react-test-config'); + jest.dontMock('@testing-library/react-native'); + jest.dontMock('@testing-library/react'); + jest.resetModules(); +}); + +function mockTestingLibraries() { + jest.doMock('@testing-library/react-native', () => ({ + cleanup: jest.fn(), + render: jest.fn(), + })); + + jest.doMock('@testing-library/react', () => ({ + cleanup: jest.fn(), + render: jest.fn(), + })); +} + +function setupMeasureRendersTest(reactTestConfigFactory: () => unknown) { + mockTestingLibraries(); + jest.doMock('react-test-config', reactTestConfigFactory); + + const { measureRenders } = require('../measure-renders') as typeof import('../measure-renders'); + const { configure } = require('../config') as typeof import('../config'); + const { setHasShownFlagsOutput } = require('../output') as typeof import('../output'); + configure({ testingLibrary: 'react-native' }); + setHasShownFlagsOutput(true); + + return measureRenders; +} + +test('disables React owner stacks before measuring renders', async () => { + jest.resetModules(); + const disableOwnerStacks = jest.fn(); + + const measureRenders = setupMeasureRendersTest(() => ({ disableOwnerStacks })); + + await measureRenders(React.createElement('View'), { + runs: 1, + warmupRuns: 0, + writeFile: false, + }); + + expect(disableOwnerStacks).toHaveBeenCalledTimes(1); +}); + +test('disables React owner stacks only once', async () => { + jest.resetModules(); + const disableOwnerStacks = jest.fn(); + + const measureRenders = setupMeasureRendersTest(() => ({ disableOwnerStacks })); + const options = { + runs: 1, + warmupRuns: 0, + writeFile: false, + }; + + await measureRenders(React.createElement('View'), options); + await measureRenders(React.createElement('View'), options); + + expect(disableOwnerStacks).toHaveBeenCalledTimes(1); +}); diff --git a/packages/measure/src/measure-renders.tsx b/packages/measure/src/measure-renders.tsx index fce7686f..fa8617e5 100644 --- a/packages/measure/src/measure-renders.tsx +++ b/packages/measure/src/measure-renders.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import * as logger from '@callstack/reassure-logger'; import { config } from './config'; import { RunResult, processRunResults } from './measure-helpers'; +import { disableOwnerStacksIfNeeded } from './owner-stacks'; import { showFlagsOutputIfNeeded, writeTestStats } from './output'; import { applyRenderPolyfills, revertRenderPolyfills } from './polyfills'; import { ElementJsonTree, detectRedundantUpdates } from './redundant-renders'; @@ -63,6 +64,7 @@ async function measureRendersInternal( const { render, cleanup } = resolveTestingLibrary(); const testingLibrary = getTestingLibrary(); + disableOwnerStacksIfNeeded(); showFlagsOutputIfNeeded(); applyRenderPolyfills(); diff --git a/packages/measure/src/owner-stacks.ts b/packages/measure/src/owner-stacks.ts new file mode 100644 index 00000000..35270aef --- /dev/null +++ b/packages/measure/src/owner-stacks.ts @@ -0,0 +1,12 @@ +import { disableOwnerStacks } from 'react-test-config'; + +let hasDisabledOwnerStacks = false; + +export function disableOwnerStacksIfNeeded() { + if (hasDisabledOwnerStacks) { + return; + } + + disableOwnerStacks(); + hasDisabledOwnerStacks = true; +} diff --git a/test-apps/native/jest-setup.js b/test-apps/native/jest-setup.js index 7d2ca3d7..614b8ad7 100644 --- a/test-apps/native/jest-setup.js +++ b/test-apps/native/jest-setup.js @@ -1,9 +1,6 @@ import { configure } from 'reassure'; -import { disableOwnerStacks } from 'react-test-config'; configure({ testingLibrary: 'react-native', verbose: true, }); - -disableOwnerStacks(); diff --git a/test-apps/native/package.json b/test-apps/native/package.json index acaffe83..fa071a81 100644 --- a/test-apps/native/package.json +++ b/test-apps/native/package.json @@ -32,7 +32,6 @@ "eslint-plugin-oxfmt": "^0.10.0", "jest": "^30.4.2", "oxfmt": "^0.54.0", - "react-test-config": "^0.3.0", "reassure": "workspace:^", "test-renderer": "1.2.0", "typescript": "^6.0.3" diff --git a/yarn.lock b/yarn.lock index 06c90294..503941be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1666,6 +1666,7 @@ __metadata: react: "npm:19.2.3" react-native: "npm:0.85.3" react-native-builder-bob: "npm:^0.41.0" + react-test-config: "npm:^0.3.0" strip-ansi: "npm:^6.0.1" test-renderer: "npm:1.2.0" typescript: "npm:^6.0.3" @@ -10582,7 +10583,6 @@ __metadata: oxfmt: "npm:^0.54.0" react: "npm:19.2.3" react-native: "npm:0.85.3" - react-test-config: "npm:^0.3.0" reassure: "workspace:^" test-renderer: "npm:1.2.0" typescript: "npm:^6.0.3" From d863216352fad89033b99a244082a4df8e407781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 15 Jun 2026 12:53:27 +0200 Subject: [PATCH 2/4] simplify --- .../src/__tests__/owner-stacks.test.ts | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/measure/src/__tests__/owner-stacks.test.ts b/packages/measure/src/__tests__/owner-stacks.test.ts index 8819542e..b033c971 100644 --- a/packages/measure/src/__tests__/owner-stacks.test.ts +++ b/packages/measure/src/__tests__/owner-stacks.test.ts @@ -1,5 +1,11 @@ import * as React from 'react'; +const measureOptions = { + runs: 1, + warmupRuns: 0, + writeFile: false, +}; + afterEach(() => { jest.dontMock('react-test-config'); jest.dontMock('@testing-library/react-native'); @@ -32,34 +38,14 @@ function setupMeasureRendersTest(reactTestConfigFactory: () => unknown) { return measureRenders; } -test('disables React owner stacks before measuring renders', async () => { - jest.resetModules(); - const disableOwnerStacks = jest.fn(); - - const measureRenders = setupMeasureRendersTest(() => ({ disableOwnerStacks })); - - await measureRenders(React.createElement('View'), { - runs: 1, - warmupRuns: 0, - writeFile: false, - }); - - expect(disableOwnerStacks).toHaveBeenCalledTimes(1); -}); - -test('disables React owner stacks only once', async () => { +test('disables React owner stacks once before measuring renders', async () => { jest.resetModules(); const disableOwnerStacks = jest.fn(); const measureRenders = setupMeasureRendersTest(() => ({ disableOwnerStacks })); - const options = { - runs: 1, - warmupRuns: 0, - writeFile: false, - }; - await measureRenders(React.createElement('View'), options); - await measureRenders(React.createElement('View'), options); + await measureRenders(React.createElement('View'), measureOptions); + await measureRenders(React.createElement('View'), measureOptions); expect(disableOwnerStacks).toHaveBeenCalledTimes(1); }); From b240a9c72b7944e900a39c5d24003bf619af3564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 15 Jun 2026 12:54:01 +0200 Subject: [PATCH 3/4] simplify --- .../src/__tests__/owner-stacks.test.ts | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 packages/measure/src/__tests__/owner-stacks.test.ts diff --git a/packages/measure/src/__tests__/owner-stacks.test.ts b/packages/measure/src/__tests__/owner-stacks.test.ts deleted file mode 100644 index b033c971..00000000 --- a/packages/measure/src/__tests__/owner-stacks.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from 'react'; - -const measureOptions = { - runs: 1, - warmupRuns: 0, - writeFile: false, -}; - -afterEach(() => { - jest.dontMock('react-test-config'); - jest.dontMock('@testing-library/react-native'); - jest.dontMock('@testing-library/react'); - jest.resetModules(); -}); - -function mockTestingLibraries() { - jest.doMock('@testing-library/react-native', () => ({ - cleanup: jest.fn(), - render: jest.fn(), - })); - - jest.doMock('@testing-library/react', () => ({ - cleanup: jest.fn(), - render: jest.fn(), - })); -} - -function setupMeasureRendersTest(reactTestConfigFactory: () => unknown) { - mockTestingLibraries(); - jest.doMock('react-test-config', reactTestConfigFactory); - - const { measureRenders } = require('../measure-renders') as typeof import('../measure-renders'); - const { configure } = require('../config') as typeof import('../config'); - const { setHasShownFlagsOutput } = require('../output') as typeof import('../output'); - configure({ testingLibrary: 'react-native' }); - setHasShownFlagsOutput(true); - - return measureRenders; -} - -test('disables React owner stacks once before measuring renders', async () => { - jest.resetModules(); - const disableOwnerStacks = jest.fn(); - - const measureRenders = setupMeasureRendersTest(() => ({ disableOwnerStacks })); - - await measureRenders(React.createElement('View'), measureOptions); - await measureRenders(React.createElement('View'), measureOptions); - - expect(disableOwnerStacks).toHaveBeenCalledTimes(1); -}); From bfa4cac644b1b9353384ae79f21bd6d4b4b0144f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 15 Jun 2026 13:45:35 +0200 Subject: [PATCH 4/4] changeset --- .changeset/smooth-baths-decide.md | 7 +++++++ docusaurus/docs/api.md | 4 ++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/smooth-baths-decide.md diff --git a/.changeset/smooth-baths-decide.md b/.changeset/smooth-baths-decide.md new file mode 100644 index 00000000..b94e0e2b --- /dev/null +++ b/.changeset/smooth-baths-decide.md @@ -0,0 +1,7 @@ +--- +"@callstack/reassure-measure": minor +"reassure-test-app": minor +"reassure": minor +--- + +feat: disable owner stacks to improve render duration stability diff --git a/docusaurus/docs/api.md b/docusaurus/docs/api.md index 663f6108..5157392d 100644 --- a/docusaurus/docs/api.md +++ b/docusaurus/docs/api.md @@ -19,6 +19,9 @@ Custom wrapper for the RNTL/RTL's `render` function responsible for rendering th measuring its performance and writing results to the output file. You can use optional `options` object allows customizing aspects of the testing. +During render measurements, Reassure disables React owner-stack collection to reduce React test overhead and improve measurement stability. +Set `RTC_SKIP_DISABLE_OWNER_STACKS` to any truthy value to skip this behavior and leave owner stacks enabled. + ```ts async function measureRenders( ui: React.ReactElement, @@ -234,6 +237,7 @@ The `reassure` CLI can be parametrized using available environmental variables: - `TEST_RUNNER_PATH`: an alternative path for your test runner. Defaults to `'node_modules/.bin/jest'` or on Windows `'node_modules/jest/bin/jest'` - `TEST_RUNNER_ARGS`: a set of arguments fed to the runner. Defaults to `'--runInBand --testMatch "**/__perf__/**/*.[jt]s?(x)", "**/*.(perf|perf-test).[jt]s?(x)"` +- `RTC_SKIP_DISABLE_OWNER_STACKS`: set to any truthy value to skip disabling React owner-stack collection during render measurements Example: