Optimizing performance in a React Next.js application can greatly enhance user experience and ensure your app runs smoothly. Utilizing utility functions can help you achieve this goal by handling common tasks efficiently and effectively. In this article, we’ll explore 10 TypeScript utility functions that can improve the performance of your Next.js application using the App Router, along with examples of how to use each function.
1. Debounce Function
Debouncing is useful for limiting the rate at which a function is executed, such as handling user input events.
Utility Function
export const debounce = <T extends (...args: any[]) => void>(func: T, wait: number): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
How to Use
import React, { useState } from 'react';
import { debounce } from './utils/debounce';
const SearchComponent = () => {
const [query, setQuery] = useState('');
const handleSearch = debounce((event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
// Perform search operation
}, 300);
return <input type="text" onChange={handleSearch} placeholder="Search..." />;
};
2. Throttle Function
Throttling ensures a function is called at most once within a specified period, which is useful for scroll or resize events.
Utility Function
export const throttle = <T extends (...args: any[]) => void>(func: T, limit: number): ((...args: Parameters<T>) => void) => {
let lastFunc: NodeJS.Timeout;
let lastRan: number;
return (...args: Parameters<T>) => {
if (!lastRan) {
func(...args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func(...args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
};
How to Use
import React, { useEffect } from 'react';
import { throttle } from './utils/throttle';
const ScrollComponent = () => {
useEffect(() => {
const handleScroll = throttle(() => {
console.log('Scroll event');
}, 200);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return <div style={{ height: '2000px' }}>Scroll down</div>;
};
3. Memoize Function
Memoization helps avoid expensive calculations by caching the results of function calls.
Utility Function
export const memoize = <T extends (...args: any[]) => any>(func: T): T => {
const cache = new Map<string, ReturnType<T>>();
return function(...args: Parameters<T>): ReturnType<T> {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = func(...args);
cache.set(key, result);
return result;
} as T;
};
How to Use
import React, { useState, useMemo } from 'react';
import { memoize } from './utils/memoize';
const expensiveCalculation = memoize((num: number) => {
console.log('Calculating...');
return num * num;
});
const MemoizeComponent = () => {
const [number, setNumber] = useState(0);
const result = useMemo(() => expensiveCalculation(number), [number]);
return (
<div>
<input type="number" value={number} onChange={(e) => setNumber(parseInt(e.target.value))} />
<div>Result: {result}</div>
</div>
);
};
4. Deep Clone Function
Deep cloning ensures a complete copy of an object, avoiding reference issues.
Utility Function
export const deepClone = <T>(obj: T): T => {
return JSON.parse(JSON.stringify(obj));
};
How to Use
import React, { useState } from 'react';
import { deepClone } from './utils/deepClone';
const DeepCloneComponent = () => {
const [object, setObject] = useState({ name: 'John', age: 30 });
const clonedObject = deepClone(object);
const updateObject = () => {
const newObject = deepClone(object);
newObject.age = 31;
setObject(newObject);
};
return (
<div>
<div>Original: {JSON.stringify(object)}</div>
<div>Cloned: {JSON.stringify(clonedObject)}</div>
<button onClick={updateObject}>Update Age</button>
</div>
);
};
5. Async Storage
Asynchronous storage utility functions for session and local storage can help manage data efficiently.
Utility Function
export const asyncSetItem = async (key: string, value: any): Promise<void> => {
await Promise.resolve().then(() => localStorage.setItem(key, JSON.stringify(value)));
};
export const asyncGetItem = async (key: string): Promise<any> => {
const value = await Promise.resolve().then(() => localStorage.getItem(key));
return value ? JSON.parse(value) : null;
};
How to Use
import React, { useEffect, useState } from 'react';
import { asyncSetItem, asyncGetItem } from './utils/asyncStorage';
const AsyncStorageComponent = () => {
const [data, setData] = useState<any>(null);
useEffect(() => {
asyncGetItem('userData').then((data) => {
setData(data);
});
}, []);
const saveData = () => {
const userData = { name: 'Jane', age: 25 };
asyncSetItem('userData', userData);
setData(userData);
};
return (
<div>
<div>Data: {JSON.stringify(data)}</div>
<button onClick={saveData}>Save Data</button>
</div>
);
};
6. Fetch with Timeout
Handling fetch requests with a timeout ensures your app doesn’t hang on slow network responses.
Utility Function
export const fetchWithTimeout = async (url: string, options: RequestInit, timeout: number): Promise<Response> => {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(id);
return response;
};
How to Use
import React, { useEffect, useState } from 'react';
import { fetchWithTimeout } from './utils/fetchWithTimeout';
const FetchComponent = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetchWithTimeout('https://api.example.com/data', {}, 5000);
const result = await response.json();
setData(result);
} catch (err) {
setError('Request timed out or failed');
}
};
fetchData();
}, []);
return (
<div>
{error ? <div>Error: {error}</div> : <div>Data: {JSON.stringify(data)}</div>}
</div>
);
};
7. Lazy Load Images
Lazy loading images improves page load performance by deferring the loading of off-screen images.
Utility Function
export const lazyLoadImage = (img: HTMLImageElement, src: string): void => {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
img.src = src;
observer.disconnect();
}
});
});
observer.observe(img);
} else {
img.src = src; // Fallback for unsupported browsers
}
};
How to Use
import React, { useEffect, useRef } from 'react';
import { lazyLoadImage } from './utils/lazyLoadImage';
const LazyLoadImageComponent = () => {
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
if (imgRef.current) {
lazyLoadImage(imgRef.current, 'https://via.placeholder.com/300');
}
}, []);
return <img ref={imgRef} alt="Lazy loaded example" style={{ minHeight: '300px', minWidth: '300px' }} />;
};
8. Load Script Dynamically
Loading scripts dynamically can help defer non-critical scripts, improving initial load performance.
Utility Function
export const loadScript = (src: string): Promise<void> => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
document.head.appendChild(script);
});
};
How to Use
import React, { useEffect } from 'react';
import { loadScript } from './utils/loadScript';
const LoadScriptComponent = () => {
useEffect(() => {
loadScript('https://example.com/some-script.js')
.then(() => {
console.log('Script loaded successfully');
})
.catch((err) => {
console.error(err);
});
}, []);
return <div>Loading external script...</div>;
};
9. Smooth Scroll
Smooth scrolling enhances user experience by providing a smooth transition to target elements.
Utility Function
export const smoothScroll = (target: HTMLElement, duration: number): void => {
const targetPosition = target.getBoundingClientRect().top;
const startPosition = window.pageYOffset;
const distance = targetPosition - startPosition;
let startTime: number | null = null;
const animation = (currentTime: number) => {
if (startTime === null) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const run = easeInOutCubic(timeElapsed, startPosition, distance, duration);
window.scrollTo(0, run);
if (timeElapsed < duration) requestAnimationFrame(animation);
};
const easeInOutCubic = (t: number, b: number, c: number, d: number) => {
t /= d / 2;
if (t < 1) return c / 2 * t * t * t + b;
t -= 2;
return c / 2 * (t * t * t + 2) + b;
};
requestAnimationFrame(animation);
};
How to Use
import React, { useRef } from 'react';
import { smoothScroll } from './utils/smoothScroll';
const SmoothScrollComponent = () => {
const targetRef = useRef<HTMLDivElement>(null);
const handleScroll = () => {
if (targetRef.current) {
smoothScroll(targetRef.current, 1000);
}
};
return (
<div>
<button onClick={handleScroll}>Scroll to Target</button>
<div style={{ height: '1000px' }}>Scroll down</div>
<div ref={targetRef} style={{ height: '100px', backgroundColor: 'lightblue' }}>
Target Element
</div>
</div>
);
};
10. Efficient State Management with useReducer
Using useReducer
for complex state management can enhance performance by reducing unnecessary re-renders.
Utility Function
import { useReducer } from 'react';
type State = {
count: number;
};
type Action = { type: 'increment' | 'decrement' };
const initialState: State = { count: 0 };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
};
export const useCounter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return { state, dispatch };
};
How to Use
import React from 'react';
import { useCounter } from './utils/useCounter';
const CounterComponent = () => {
const { state, dispatch } = useCounter();
return (
<div>
<div>Count: {state.count}</div>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
};
Conclusion
Optimizing performance in a React Next.js application involves using various strategies and utilities to handle common tasks efficiently. The utility functions provided in this article can help you improve your application’s performance by managing events, caching results, handling async storage, and more. Integrate these functions into your Next.js app to create a smoother, faster user experience.
Leave a Reply