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
JavaScript
;
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;