
Performance, is the most important metric for any web app, especially if there is business depending upon it. The slightest increase/decrease in it affects the whole chain. From SEO to user experience to lead conversion and ultimately sales.
React is the most popular library out there to create a web app or mobile app, desktop app, PWA, etc.
Let us see 5 hooks that will help us to improve the performance of the apps that are built in React.
useWhyDidYouUpdate() hook
Avoiding pointless re-renders is one way to accomplish performance optimization in React, and in order to track this, we must keep an eye on what has changed in the component’s props or states.
Use the useWhyDidYouUpdate() hook to find out what changed and caused the re-rendering so that if they are unnecessary, they can be mitigated.
The idea is very simple, use a useRef() hook to store the previous props and then compare it with the current props to check what has triggered the re-render.
As states are part of the component itself and controlled by it, they can be tracked separately.
function useWhyDidYouUpdate(name, props) {
// create a reference to track the previous data
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
// merge the keys of previous and current data
const keys = Object.keys({ ...previousProps.current, ...props });
// to store what has change
const changesObj = {};
// check what values have changed between the previous and current
keys.forEach((key) => {
// if both are object
if (typeof props[key] === "object" && typeof previousProps.current[key] === "object") {
if (JSON.stringify(previousProps.current[key]) !== JSON.stringify(props[key])) {
// add to changesObj
changesObj[key] = {
from: previousProps.current[key],
to: props[key],
};
}
} else {
// if both are non-object
if (previousProps.current[key] !== props[key]) {
// add to changesObj
changesObj[key] = {
from: previousProps.current[key],
to: props[key],
};
}
}
});
// if changesObj not empty, print the cause
if (Object.keys(changesObj).length) {
console.log("This is causing re-renders", name, changesObj);
}
}
// update the previous props with the current
previousProps.current = props;
});
}
useOnScreen() hook
Lazy loading can drastically boost the performance as we will be loading things as and when required rather than pulling everything in bulk.
For example, for a component that is not visible yet on the viewport, it makes no sense to load the media files like images, video, audio, or any large data.
Thus we can use the useOnScreen() hook to determine if the component is visible and is in the viewport then perform the necessary action.
We can use the Intersection observer API to implement this. Create a reference to the component using useRef() hook and then observe this reference, if it is intersecting, update the state that it is visible.
function useOnScreen(ref) {
const [isIntersecting, setIntersecting] = useState(false);
// monitor the interaction
const observer = new IntersectionObserver(
([entry]) => {
// update the state on interaction change
setIntersecting(entry.isIntersecting);
}
);
useEffect(() => {
// assign the observer
observer.observe(ref.current);
// remove the observer as soon as the component is unmounted
return () => {
observer.disconnect();
};
}, []);
return isIntersecting;
}
useScript() hook
Removing the load from the main processing thread and lazy loading the scripts dynamically is an important way of boosting performance.
Keeping the initial script as small as possible results in faster processing. Thus all other unimportant scripts can be loaded separately as and when required.
For example, the Google Adsense script is not required to be part of the main bundle, it can be loaded separately using the useScript() hook.
useScript() hook loads any given script if it is not already loaded.
We use the src and traverse the DOM and check if it exists, if it is not then we can create a new script and inject it into the body.
Also, assign the load and error listener on the script to monitor if it is properly loaded or not. Depending upon the response from the listeners, update the status.
function useScript(src) {
// keep track of script status ("idle", "loading", "ready", "error")
const [status, setStatus] = useState(src ? "loading" : "idle");
useEffect(() => {
// if not url provided, set the state to be idle
if (!src) {
setStatus("idle");
return;
}
// get the script to check if it is already sourced or not
let script = document.querySelector(`script[src="${src}"]`);
if (!script) {
// create script
script = document.createElement("script");
script.src = src;
script.async = true;
script.setAttribute("data-status", "loading");
// inject the script at the end of the body
document.body.appendChild(script);
// set the script status in a custom attribute
const setAttributeFromEvent = (event) => {
script.setAttribute("data-status", event.type === "load" ? "ready" : "error");
};
script.addEventListener("load", setAttributeFromEvent);
script.addEventListener("error", setAttributeFromEvent);
} else {
// if the script is already loaded, get its status and update.
setStatus(script.getAttribute("data-status"));
}
// update the script status
const setStateFromEvent = (event) => {
setStatus(event.type === "load" ? "ready" : "error");
};
// setup
script.addEventListener("load", setStateFromEvent);
script.addEventListener("error", setStateFromEvent);
// clean up
return () => {
if (script) {
script.removeEventListener("load", setStateFromEvent);
script.removeEventListener("error", setStateFromEvent);
}
};
}, [src]);
return status;
}
useIdle() hook
If a user is Idle or not performing any type of activity then we can stop certain actions like API calls or perform session management and log out the user from the application, especially in the banking apps.
If a user is not using interaction hardware, such as a mouse, keyboard, or touch screen on a desktop, laptop, or mobile device, then that user is said to be inactive or idle.
We can listen to events like mousemove, mousedown, keypress, DOMMouseScroll, mousewheel, touchmove, and MSPointerMove for this.
Additionally, we must deal with edge circumstances where the window or tab is out of focus, therefore we will listen for focus and blur events in these situations.
If any of these events occur, set the user to be active; otherwise, if none have occurred for a predetermined period of time, set the user to be idle or inactive.
useIdle() hook takes time as input and will notify if the user has not performed any activity for that given amount of time.
const useIdle = (delay) => {
const [isIdle, setIsIdle] = useState(false);
// create a new reference to track timer
const timeoutId = useRef();
// assign and remove the listeners
useEffect(() => {
setup();
return () => {
cleanUp();
};
});
const startTimer = () => {
// wait till delay time before calling goInactive
timeoutId.current = setTimeout(goInactive, delay);
};
const resetTimer = () => {
//reset the timer and make user active
clearTimeout(timeoutId.current);
goActive();
};
const goInactive = () => {
setIsIdle(true);
};
const goActive = () => {
setIsIdle(false);
// start the timer to track Inactiveness
startTimer();
};
const setup = () => {
document.addEventListener("mousemove", resetTimer, false);
document.addEventListener("mousedown", resetTimer, false);
document.addEventListener("keypress", resetTimer, false);
document.addEventListener("DOMMouseScroll", resetTimer, false);
document.addEventListener("mousewheel", resetTimer, false);
document.addEventListener("touchmove", resetTimer, false);
document.addEventListener("MSPointerMove", resetTimer, false);
//edge case
//if tab is changed or is out of focus
window.addEventListener("blur", startTimer, false);
window.addEventListener("focus", resetTimer, false);
};
const cleanUp = () => {
document.removeEventListener("mousemove", resetTimer);
document.removeEventListener("mousedown", resetTimer);
document.removeEventListener("keypress", resetTimer);
document.removeEventListener("DOMMouseScroll", resetTimer);
document.removeEventListener("mousewheel", resetTimer);
document.removeEventListener("touchmove", resetTimer);
document.removeEventListener("MSPointerMove", resetTimer);
//edge case
//if tab is changed or is out of focus
window.removeEventListener("blur", startTimer);
window.removeEventListener("focus", resetTimer);
// memory leak
clearTimeout(timeoutId.current);
};
// return previous value (happens before update in useEffect above)
return isIdle;
};
useResponsive() hook
DOM parsing and painting is a very expensive operation and should be avoided as much as possible for faster loading of application.
Generating the DOM and hiding it with the CSS for different screen sizes is still costly, rather than using CSS with React you can dynamically render the components.
Using useResponsive() hook we can determine the device screen size and accordingly render the components.
For this, we are listening to the window resize event using a debounce call and updating the state if the size changes.
const useResponsive = () => {
// screen resolutions
const [state, setState] = useState({
isMobile: false,
isTablet: false,
isDesktop: false,
});
useEffect(() => {
// update the state on the initial load
onResizeHandler();
// assign the event
Setup();
return () => {
// remove the event
Cleanup();
};
}, []);
// update the state on window resize
const onResizeHandler = () => {
const isMobile = window.innerWidth <= 768;
const isTablet = window.innerWidth >= 768 && window.innerWidth <= 990;
const isDesktop = window.innerWidth > 990;
setState({ isMobile, isTablet, isDesktop });
};
// debounce the resize call
const debouncedCall = useDebounce(onResizeHandler, 500);
// add event listener
const Setup = () => {
window.addEventListener("resize", debouncedCall, false);
};
// remove the listener
const Cleanup = () => {
window.removeEventListener("resize", debouncedCall, false);
};
return state;
};