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.
397 lines (380 loc) • 15.2 kB
JavaScript
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);
}));
export { perf$1 as perf, wait };