UNPKG

react-performance-testing

Version:

You can test React(ReactNative) runtime performance by using this lib. If you want to check the number of renders, or render time in a test environment, this lib makes sense.

402 lines (383 loc) 15.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const getDisplayName = (type) => type.displayName || type.name || (type.type && getDisplayName(type.type)) || (type.render && getDisplayName(type.render)); const isClassComponent = (Component) => Component.prototype && !!Component.prototype.isReactComponent; /** * Copied from ReactSymbols.js * https://github.com/facebook/react/blob/master/packages/shared/ReactSymbols.js */ let REACT_MEMO_TYPE = 0xead3; let REACT_FORWARD_REF_TYPE = 0xead0; const symbolFor = typeof Symbol === 'function' && Symbol.for; /* istanbul ignore else */ if (symbolFor) { REACT_MEMO_TYPE = Symbol.for('react.memo'); REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref'); } const isMemoComponent = (Component) => Component.$$typeof === REACT_MEMO_TYPE; const isForwardRefComponent = (Component) => Component.$$typeof === REACT_FORWARD_REF_TYPE; const isFunctionComponent = (type) => typeof type === 'function'; const W = (global === null || global === void 0 ? void 0 : global.window) ? window : global; const pushTask = (cb) => { W.setImmediate(cb); }; const setArray = (displayName, state, initialValue) => { const obj = state.current[displayName]; let currentIndex = -1; if (obj) { state.current[displayName] = Array.isArray(obj) ? [...obj, initialValue] : [{ ...obj }, initialValue]; currentIndex = Array.isArray(obj) ? obj.length : 1; } return currentIndex; }; const updateRenderCount = (renderCount, index, displayName) => { if (!displayName) { return; } const obj = renderCount.current; if (!obj[displayName]) { obj[displayName] = { value: 0 }; } pushTask(() => { const obj = renderCount.current; const field = obj[displayName]; if (Array.isArray(field)) { const formattedIndex = index === -1 ? 0 : index; field[formattedIndex].value += 1; return; } field.value += 1; }); }; const startMeasureRenderTime = (renderTime, index, displayName) => { if (!displayName) { return () => { }; } const obj = renderTime.current; if (!obj[displayName]) { obj[displayName] = { mount: null, updates: [] }; } const startTime = performance.now(); return () => { const duration = performance.now() - startTime; pushTask(() => { const obj = renderTime.current; const field = obj[displayName]; if (Array.isArray(field)) { const formattedIndex = index === -1 ? 0 : index; const fieldValues = field[formattedIndex]; field[formattedIndex] = { mount: fieldValues.mount || duration, updates: fieldValues.mount ? [...fieldValues.updates, duration] : [], }; return; } obj[displayName] = { mount: field.mount || duration, updates: field.mount ? [...field.updates, duration] : [], }; }); }; }; const createClassComponent = (type, { renderCount, renderTime }, { hasRenderCount, hasRenderTime }) => { const ClassComponent = type; const displayName = getDisplayName(type); if (!displayName) { console.warn("[react-performance-testing] You have anonymous component. If your component don't have display name, we can not set property to renderCount.current"); } class _PatchedClassComponent extends ClassComponent { constructor(props, context) { super(props, context); const origRender = super.render || this.render; if (hasRenderTime) { this.currentIndex = setArray(displayName, renderTime, { mount: null, updates: [], }); } if (hasRenderCount) { this.currentIndex = setArray(displayName, renderCount, { value: 0 }); } // this probably means render is an arrow function or this.render.bind(this) was called on the original class // https://github.com/welldone-software/why-did-you-render/blob/master/src/patches/patchClassComponent.js#L16 const IsBoundFunction = origRender !== ClassComponent.prototype.render; if (IsBoundFunction) { this.render = () => { _PatchedClassComponent.prototype.render.apply(this); return origRender(); }; } } componentDidMount() { if (this.endMeasureRenderTime) { this.endMeasureRenderTime(); } } componentDidUpdate() { if (this.endMeasureRenderTime) { this.endMeasureRenderTime(); } } render() { if (hasRenderCount) { updateRenderCount(renderCount, this.currentIndex, displayName); } if (hasRenderTime) { this.endMeasureRenderTime = startMeasureRenderTime(renderTime, this.currentIndex, displayName); } return super.render ? super.render() : null; } } return _PatchedClassComponent; }; const createFunctionComponent = (type, { renderCount, renderTime }, { hasRenderCount, hasRenderTime }, React) => { const FunctionComponent = type; const displayName = getDisplayName(type); if (!displayName) { console.warn("[react-performance-testing] You have anonymous component. If your component don't have display name, we can not set property to renderCount.current"); } const PatchedFunctionComponent = (...args) => { const currentIndex = React.useMemo(() => { let index = -1; if (hasRenderTime) { index = setArray(displayName, renderTime, { mount: null, updates: [] }); } if (hasRenderCount) { index = setArray(displayName, renderCount, { value: 0 }); } return index; }, []); const endMeasureRenderTime = React.useRef(null); if (hasRenderCount) { updateRenderCount(renderCount, currentIndex, displayName); } React.useLayoutEffect(() => { if (endMeasureRenderTime.current) { endMeasureRenderTime.current(); } }); if (hasRenderTime) { endMeasureRenderTime.current = startMeasureRenderTime(renderTime, currentIndex, displayName); } return FunctionComponent(...args); }; return PatchedFunctionComponent; }; const createMemoComponent = (type, tools, perfState, React) => { const { type: InnerMemoComponent } = type; const isInnerForwardRefComponent = isForwardRefComponent(InnerMemoComponent); const WrappedFunctionalComponent = isInnerForwardRefComponent ? InnerMemoComponent.render : InnerMemoComponent; const PatchedInnerComponent = isClassComponent(InnerMemoComponent) ? createClassComponent(WrappedFunctionalComponent, tools, perfState) : isMemoComponent(InnerMemoComponent) ? createMemoComponent(WrappedFunctionalComponent, tools, perfState, React) : createFunctionComponent(WrappedFunctionalComponent, tools, perfState, React); try { // @ts-ignore PatchedInnerComponent.displayName = getDisplayName(WrappedFunctionalComponent); } catch (e) { } const PatchedMemoComponent = React.memo(isInnerForwardRefComponent ? React.forwardRef(PatchedInnerComponent) : PatchedInnerComponent, type.compare); return PatchedMemoComponent; }; const createForwardRefComponent = (type, tools, perfState, React) => { const { render: InnerForwardRefComponent } = type; const isInnerMemoComponent = isMemoComponent(InnerForwardRefComponent); const WrappedFunctionalComponent = isInnerMemoComponent ? InnerForwardRefComponent.type : InnerForwardRefComponent; const PatchedInnerComponent = createFunctionComponent(WrappedFunctionalComponent, tools, perfState, React); try { // @ts-ignore PatchedInnerComponent.displayName = getDisplayName(WrappedFunctionalComponent); } catch (e) { } const PatchedForwardRefComponent = React.forwardRef(isInnerMemoComponent ? React.memo(PatchedInnerComponent, WrappedFunctionalComponent.compare) : PatchedInnerComponent); return PatchedForwardRefComponent; }; const createPatchedComponent = (type, tools, perfState, React) => { if (isMemoComponent(type)) { return createMemoComponent(type, tools, perfState, React); } if (isForwardRefComponent(type)) { return createForwardRefComponent(type, tools, perfState, React); } if (isClassComponent(type)) { return createClassComponent(type, tools, perfState); } // Here is only checking type /* istanbul ignore else */ if (isFunctionComponent(type)) { return createFunctionComponent(type, tools, perfState, React); } }; const getPatchedComponent = (componentsMap, type, tools, perfState, React) => { const PatchedComponent = createPatchedComponent(type, tools, perfState, React); try { // @ts-ignore PatchedComponent.displayName = getDisplayName(type); } catch (e) { } componentsMap.set(type, PatchedComponent); return PatchedComponent; }; const shouldTrack = (component) => isClassComponent(component) || isMemoComponent(component) || isForwardRefComponent(component) || typeof component === 'function'; const globalOption = { isDeclaredRenderTime: false, }; const checkRenderTimeDeclaring = (prop) => { if (prop === 'renderTime' && globalOption.isDeclaredRenderTime) { console.warn('[react-performance-testing] You need to execute test one by one when you use `renderTime`. Please check here: https://github.com/keiya01/react-performance-testing#renderTime'); } else { globalOption.isDeclaredRenderTime = true; } }; const getDefaultStore = () => ({ tools: { renderCount: { current: {} }, renderTime: { current: {} }, }, componentsMap: new WeakMap(), perfState: Object.defineProperties({ hasRenderCount: !Proxy, hasRenderTime: !Proxy, }, { renderCount: { set(val) { this.hasRenderCount = val; }, }, renderTime: { set(val) { this.hasRenderTime = val; }, }, }), }); const store = getDefaultStore(); const getPerfTools = () => new Proxy(store.tools, { get: (target, prop) => { checkRenderTimeDeclaring(prop); store.perfState[prop] = true; return target[prop]; }, }); let origCreateElement = null; let origCreateFactory = null; let origCloneElement = null; let origReact = null; const perf = (React) => { if (!React) { return getPerfTools(); } const { tools, perfState, componentsMap } = store; origReact = React; origCreateElement = React.createElement; origCreateFactory = React.createFactory; origCloneElement = React.cloneElement; // @ts-ignore React.createElement = function (type, ...rest) { if (!shouldTrack(type)) { return origCreateElement.apply(React, [type, ...rest]); } let PatchedComponent; if (componentsMap.has(type)) { PatchedComponent = componentsMap.get(type); } else { PatchedComponent = getPatchedComponent(componentsMap, type, tools, perfState, React); } return origCreateElement.apply(React, [PatchedComponent, ...rest]); }; Object.assign(React.createElement, origCreateElement); // @ts-ignore React.createFactory = (type) => { const factory = React.createElement.bind(null, type); // @ts-ignore factory.type = type; return factory; }; Object.assign(React.createFactory, origCreateFactory); // @ts-ignore React.cloneElement = (...args) => { const element = origCloneElement.apply(React, args); return element; }; Object.assign(React.cloneElement, origCloneElement); return getPerfTools(); }; const cleanup = () => { Object.assign(store, getDefaultStore()); if (!origReact) { return; } Object.assign(origReact, { createElement: origCreateElement, createFactory: origCreateFactory, cloneElement: origCloneElement, }); origReact = null; origCreateElement = null; origCreateFactory = null; origCloneElement = null; }; // Auto cleanup with Jest, Mocha, Jasmine or some lib that is supporting teardown(). if (!process.env.RPT_SKIP_AUTO_CLEANUP) { /* istanbul ignore else */ if (typeof afterEach === 'function') { afterEach(cleanup); // @ts-ignore } else if (typeof teardown === 'function') { // @ts-ignore teardown(cleanup); } } const perf$1 = (React) => { // Copied from https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/native/index.js#L24 // We don't need to wrap these components. const aliases = `ActivityIndicator ActivityIndicatorIOS AnimatedComponent AppContainer ART Button DatePickerIOS DrawerLayoutAndroid Image ImageBackground ImageEditor ImageStore KeyboardAvoidingView ListView MapView Modal NavigatorIOS Picker PickerIOS ProgressBarAndroid ProgressViewIOS PressabilityDebugView ScrollView SegmentedControlIOS Slider SliderIOS SnapshotViewIOS Switch RecyclerViewBackedScrollView RefreshControl SafeAreaView StatusBar SwipeableListView SwitchAndroid SwitchIOS TabBarIOS Text TextInput ToastAndroid ToolbarAndroid Touchable TouchableHighlight TouchableNativeFeedback TouchableOpacity TouchableWithoutFeedback View ViewPagerAndroid WebView YellowBox YellowBoxContainer YellowBoxList FlatList SectionList VirtualizedList Pressable RCTScrollView` .split(/\s+/m) .map((alias) => alias); const origCreateElement = React.createElement; const tools = perf(React); const patchedCreateElement = React.createElement; React.createElement = function (type, ...rest) { if (typeof type !== 'string' && (aliases.includes(getDisplayName(type)) || isForwardRefComponent(type))) { return origCreateElement.apply(React, [type, ...rest]); } return patchedCreateElement.apply(React, [type, ...rest]); }; Object.assign(React.createElement, patchedCreateElement); return tools; }; const wait = (callback) => new Promise((resolve) => pushTask(() => { callback(); resolve(undefined); })); exports.perf = perf$1; exports.wait = wait;