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.

379 lines (361 loc) 13.8 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 wait = (callback) => new Promise((resolve) => pushTask(() => { callback(); resolve(undefined); })); exports.__getDisplayName = getDisplayName; exports.__getPatchedComponent = getPatchedComponent; exports.__shouldTrack = shouldTrack; exports.__store = store; exports.cleanup = cleanup; exports.perf = perf; exports.wait = wait;