From df300387e6ab661a0c1eb71d1c895a4ff6d220ea Mon Sep 17 00:00:00 2001 From: Luna Wei Date: Wed, 19 Mar 2025 00:01:35 -0700 Subject: [PATCH] Create IntersectionObserver benchmark (#50023) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50023 Changelog: [Internal] - Add benchmarks for Intersection Observer Reviewed By: rubennorte Differential Revision: D66847629 fbshipit-source-id: 01cb7994ddb007ecce29593b6ab4b1d359d68bd0 --- .../IntersectionObserver-benchmark-itest.js | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-benchmark-itest.js diff --git a/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-benchmark-itest.js b/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-benchmark-itest.js new file mode 100644 index 00000000000..882f498df74 --- /dev/null +++ b/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-benchmark-itest.js @@ -0,0 +1,294 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + * @fantom_flags enableAccessToHostTreeInFabric:true + */ + +import type IntersectionObserverType from '../IntersectionObserver'; +import type {Root} from '@react-native/fantom'; + +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import ScrollView from 'react-native/Libraries/Components/ScrollView/ScrollView'; +import View from 'react-native/Libraries/Components/View/View'; +import setUpIntersectionObserver from 'react-native/src/private/setup/setUpIntersectionObserver'; +import ensureInstance from 'react-native/src/private/utilities/ensureInstance'; +import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; + +import 'react-native/Libraries/Core/InitializeCore'; + +declare const IntersectionObserver: Class; + +setUpIntersectionObserver(); + +let maybeNode; +let node: ReactNativeElement; + +let maybeScrollViewNode; +let scrollViewNode: ReactNativeElement; +let observer: IntersectionObserverType; +const VIEWPORT_HEIGHT = 100; +const VIEWPORT_WIDTH = 100; +const root = Fantom.createRoot({ + viewportHeight: VIEWPORT_HEIGHT, + viewportWidth: VIEWPORT_WIDTH, +}); +let mockCallback = jest.fn(); + +function cleanup(renderedRoot: Root, testObserver: ?IntersectionObserverType) { + Fantom.runTask(() => { + if (testObserver != null) { + testObserver.disconnect(); + } + root.render(<>); + }); +} + +// Scroll yOffset 1px at a time +function scrollBy1(scrollNode: ReactNativeElement, yOffset: number) { + for (let i = 1; i <= yOffset; i++) { + Fantom.scrollTo(scrollNode, { + x: 0, + y: i, + }); + } +} + +// Render element such that it appears at `Fantom.scrollTo({x, y: yOffset})` +// A negative value `yOffset` means the element is already onscreen +function renderElementAtYScrollPosition(yOffset: number, element: React.Node) { + return ( + <> + + {element} + + ); +} + +Fantom.unstable_benchmark + .suite('IntersectionObserver') + .test( + 'Create IntersectionObserver', + () => { + Fantom.runTask(() => { + observer = new IntersectionObserver(mockCallback, {}); + }); + }, + { + beforeEach: () => { + mockCallback = jest.fn(); + }, + afterEach: () => { + expect(mockCallback).not.toHaveBeenCalled(); + cleanup(root, observer); + }, + }, + ) + .test( + 'Observe a mounted view', + () => { + Fantom.runTask(() => { + observer.observe(node); + }); + }, + { + beforeEach: () => { + mockCallback = jest.fn(); + Fantom.runTask(() => { + root.render( + { + maybeNode = receivedNode; + }} + />, + ); + observer = new IntersectionObserver(mockCallback); + }); + node = ensureInstance(maybeNode, ReactNativeElement); + }, + afterEach: () => { + expect(mockCallback).toHaveBeenCalledTimes(1); + const [entries] = mockCallback.mock.lastCall; + expect(entries.length).toBe(1); + expect(entries[0].isIntersecting).toBe(true); + + cleanup(root, observer); + }, + }, + ) + .test( + 'ScrollView no intersection, no observation', + () => { + scrollBy1(scrollViewNode, VIEWPORT_HEIGHT); + }, + { + beforeEach: () => { + Fantom.runTask(() => { + root.render( + { + maybeScrollViewNode = receivedNode; + }}> + {renderElementAtYScrollPosition( + VIEWPORT_HEIGHT + 50, + , + )} + , + ); + }); + scrollViewNode = ensureInstance( + maybeScrollViewNode, + ReactNativeElement, + ); + }, + afterEach: () => { + cleanup(root, observer); + }, + }, + ) + .test( + 'ScrollView intersection, no observation', + () => { + scrollBy1(scrollViewNode, VIEWPORT_HEIGHT); + }, + { + beforeEach: () => { + Fantom.runTask(() => { + root.render( + { + maybeScrollViewNode = receivedNode; + }}> + {renderElementAtYScrollPosition( + -5, + , + )} + , + ); + }); + scrollViewNode = ensureInstance( + maybeScrollViewNode, + ReactNativeElement, + ); + }, + afterEach: () => { + cleanup(root); + }, + }, + ) + .test( + 'ScrollView no intersection, observation', + () => { + scrollBy1(scrollViewNode, VIEWPORT_HEIGHT); + }, + { + beforeEach: () => { + mockCallback = jest.fn(); + + Fantom.runTask(() => { + root.render( + { + maybeScrollViewNode = receivedNode; + }}> + {renderElementAtYScrollPosition( + VIEWPORT_HEIGHT + 50, + { + maybeNode = receivedNode; + }} + style={{width: 100, height: 10}} + />, + )} + , + ); + }); + scrollViewNode = ensureInstance( + maybeScrollViewNode, + ReactNativeElement, + ); + node = ensureInstance(maybeNode, ReactNativeElement); + Fantom.runTask(() => { + observer = new IntersectionObserver(mockCallback, {}); + observer.observe(node); + }); + }, + afterEach: () => { + expect(mockCallback).toHaveBeenCalledTimes(1); + + const [entries] = mockCallback.mock.lastCall; + expect(entries.length).toBe(1); + expect(entries[0].isIntersecting).toBe(false); + + cleanup(root, observer); + }, + }, + ) + .test( + 'ScrollView intersection, observation', + () => { + scrollBy1(scrollViewNode, VIEWPORT_HEIGHT); + }, + { + beforeEach: () => { + mockCallback = jest.fn(); + + Fantom.runTask(() => { + root.render( + { + maybeScrollViewNode = receivedNode; + }}> + {renderElementAtYScrollPosition( + -5, + { + maybeNode = receivedNode; + }} + style={{width: 100, height: 10}} + />, + )} + , + ); + }); + scrollViewNode = ensureInstance( + maybeScrollViewNode, + ReactNativeElement, + ); + node = ensureInstance(maybeNode, ReactNativeElement); + Fantom.runTask(() => { + observer = new IntersectionObserver(mockCallback, {threshold: 1}); + observer.observe(node); + }); + }, + afterEach: () => { + expect(mockCallback).toHaveBeenCalledTimes(3); + + const [entries1] = mockCallback.mock.calls[0]; + expect(entries1.length).toBe(1); + expect(entries1[0].isIntersecting).toBe(false); + + const [entries2] = mockCallback.mock.calls[1]; + expect(entries2.length).toBe(1); + expect(entries2[0].isIntersecting).toBe(true); + + const [entries3] = mockCallback.mock.calls[2]; + expect(entries3.length).toBe(1); + expect(entries3[0].isIntersecting).toBe(false); + + cleanup(root, observer); + }, + }, + );