Review
- 2024-08-20 08:06
[!Summary] 检测性能的工具
- React Scan https://github.com/aidenybai/react-scan
- why-did-you-render https://github.com/welldone-software/why-did-you-render
- React DevTools (Chrome Extension)
- Lighthouse (Chrome Extension)
- Web Vitals (Chrome Extension) https://github.com/GoogleChrome/web-vitals
- WebPageTest https://webpagetest.org/
- PageSpeed Insights https://pagespeed.web.dev/
一、Introduction #
Unnecessary re-renders #
Unnecessary re-renders in components can occur due to several reasons, and it’s important to optimize your code to minimize them for better performance.
Here are some common reasons for unnecessary re-renders in functional components:
- Using inline functions in JSX props: If you pass an inline function as a prop to child components, those components will get re-rendered every time the parent component re-renders. This is because a new function is created on every render. You can optimize this by using
useCallbackhook to memoize the function. - ==Using
useStatehook with objects==: If you useuseStatehook with objects, you need to make sure that you are not mutating the object. If you mutate the object, React will not be able to detect the change and will not re-render the component. You can optimize this by usinguseReducerhook instead ofuseStatehook. - Using
useEffecthook without dependencies: If you useuseEffecthook without dependencies, it will run on every render. You can optimize this by passing an empty array as the second argument touseEffecthook. - Parent Component Re-renders: If a parent component re-renders, all its child components will also re-render. You can optimize this by using
React.memoto memoize the child component where possible.memoensures that a component only re-renders when its props have changed, not simply because its parent re-rendered. - Global State Changes: If you use global state management libraries like Redux, MobX, etc., and the global state changes, all the components that use that state will re-render. You can optimize this by using
useSelectorhook to select only the state that you need in a component. - Misusing Context: If you use Context API to pass data to child components, and the data changes, all the child components will re-render. You can optimize this by using
useContexthook to select only the data that you need in a component.
You can also use React.StrictMode to detect potential problems in your code that could cause unnecessary re-renders.
Server Components #
Server Components allow developers to write components that render on the server instead of the client. Unlike traditional components, Server Components do not have a client-side runtime, meaning they result in a smaller bundle size and faster loads. They can seamlessly integrate with client components and can fetch data directly from the backend without the need for an API layer. This enables developers to build rich, interactive apps with less client-side code, improving performance and developer experience.
Server Components can directly access backend resources, databases, or filesystems to fetch data during rendering, eliminating the need for a separate API layer for data fetching.
The use client directive marks source files whose components are intended to execute only on the client. Conversely, ==use server marks server-side functions that can be invoked from client-side code.==
Hydration is the process of using client-side JavaScript to add interactivity to the markup generated by the server. When you use server-side rendering, the server returns a static HTML representation of the component tree. Once this reaches the browser, in order to make it interactive, React “hydrates” the static content, turning it into a fully interactive application.
Lazy load components in React #
Suspense lazy
use React’s lazy() function in conjunction with dynamic import() to lazily load a component. This is often combined with Suspense to display fallback content while the component is being loaded.
import { lazy, Suspense } from 'react';
const LazyRoadmapRender = lazy(() => delay(import('./RoadmapRender')));
export function App() {
const [showRoadmapRender, setShowRoadmapRender] = useState(false);
return (
<>
<button onClick={() => setShowRoadmapRender(true)}>
Show RoadmapRender
</button>
{showRoadmapRender && (
<Suspense fallback={<div>Loading...</div>}>
<LazyRoadmapRender />
</Suspense>
)}
</>
);
}
// Helper function to simulate a 2 seconds delay
function delay(promise) {
return new Promise((resolve) => setTimeout(resolve, 2000)).then(
() => promise
);
}unique index #
Using index as a key can negatively impact performance and may cause issues with the component state. When the list items change due to additions, deletions, or reordering, using indexes can lead to unnecessary re-renders or even incorrect UI updates. React uses keys to identify elements in the list, and if the key is just an index, it might reuse component instances and state inappropriately. Especially in cases where the list is dynamic or items can be reordered, it’s recommended to use unique and stable identifiers as keys to ensure consistent behavior.
Strict Mode #
Strict Mode is a tool in React for highlighting potential problems in an application. By wrapping a component tree with StrictMode, React will activate additional checks and warnings for its descendants. This doesn’t affect the production build but provides insights during development.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);In Strict Mode, React does a few extra things during development:
- It renders components twice to catch bugs caused by impure rendering.
- It runs side-effects (like data fetching) twice to find mistakes in them caused by missing effect cleanup.
- It checks if deprecated APIs are used, and logs a warning message to the console if so.
Use the React DevTools Profiler #
The React DevTools Profiler helps you visualize how components render and identify costly renderings. It can also help you identify unnecessary re-renders.
Check for Unnecessary Renders #
Ensure that components don’t render more often than needed. Be clear about the useEffect dependencies and avoid creating new objects or arrays every render, as these can trigger unnecessary child component renders. Tools like
why-did-you-render can help spot unnecessary re-renders.
Check Network Requests #
Slow API calls or fetching large amounts of data can affect performance. Optimize your backend, paginate data, or cache results. You can also use tools like @tanstack/react-query or swr to help manage data fetching and caching.
Leverage the children props for cleaner code (and performance benefits)
#
Using the children props has several benefits:
- Benefit #1: You can avoid prop drilling by passing props directly to children components instead of routing them through the parent.
- Benefit #2: Your code is more extensible since you can easily modify children without changing the parent component.
- Benefit #3: You can use this trick to avoid re-rendering “slow” components (see in the example below 👇).
function App() {
return (
<Dashboard >
<MyVerySlowComponent />
</Dashboard>
);
}
function Dashboard({ children }) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setCurrentTime(new Date());
}, 1_000);
return () => clearInterval(intervalId);
}, []);
return (
<>
<h1>{currentTime.toTimeString()}</h1>
{children}
</>
);
}When dealing with different cases, use value === case && <Component /> to avoid holding onto old state
#
Render a component based on the selectedType or use a key to force a reset when the type changes.
function App() {
const [selectedType, setSelectedType] = useState<ResourceType>("posts");
return (
<>
<Navbar selectedType={selectedType} onSelectType={setSelectedType} />
{selectedType === "posts" && <Resource type="posts" />}
{selectedType === "snippets" && <Resource type="snippets" />}
</>
);
}function App() {
const [selectedType, setSelectedType] = useState<ResourceType>("posts");
return (
<>
<Navbar selectedType={selectedType} onSelectType={setSelectedType} />
<Resource type={selectedType} key={selectedType} />
</>
);
}Strategically use the key attribute to trigger component re-renders
#
Want to force a component to re-render from scratch? Just change its key.
Use a ref callback function for tasks such as monitoring size changes and managing multiple node elements.
#
function App() {
const ref = useCallback((inputNode) => {
inputNode?.focus();
}, []);
return <input ref={ref} type="text" />;
}Prefer named exports over default exports #
Keep the state at the lowest level necessary to minimize re-renders #
Update state based on the previous state, especially when memoizing with useCallback
#
I use this behavior whenever I need to update the state based on the previous state, especially inside functions wrapped with useCallback. In fact, this approach prevents the need to have the state as one of the hook dependencies.
function App() {
const [todos, setToDos] = useState([]);
const handleAddTodo = useCallback((todo) => {
setToDos((prevTodos) => [...prevTodos, todo]);
}, []);
const handleRemoveTodo = useCallback((id) => {
setToDos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
}, []);
return (
<div className="App">
<TodoInput onAddTodo={handleAddTodo} />
<TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
</div>
);
}Use functions in useState for lazy initialization and performance gains, as they are invoked only once.
#
Using a function in useState ensures the initial state is computed only once.
This can improve performance, especially when the initial state is derived from an “expensive” operation like reading from local storage.
function PageWrapper({ children }) {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
);
const handleThemeChange = (theme) => {
setTheme(theme);
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
};
return (
<div
className="page-wrapper"
style={{ background: theme === "dark" ? "black" : "white" }}
>
<div className="header">
<button onClick={() => handleThemeChange("dark")}>Dark</button>
<button onClick={() => handleThemeChange("light")}>Light</button>
</div>
<div>{children}</div>
</div>
);
}React Context: Split your context into parts that change frequently and those that change infrequently to enhance app performance #
One challenge with React context is that all components consuming the context re-render whenever the context data changes, even if they don’t use the part of the context that changed 🤦♀️.
A solution? Use separate contexts.
React Context: Introduce a Provider component when the value computation is not straightforward
#
Consider using the useReducer hook as a lightweight state management solution
#
Whenever I have too many values in my state or a complex state and don’t want to rely on external libraries, I will reach for useReducer.
It’s especially effective when combined with context for broader state management needs.
Specify an equality function with memo to instruct React on how to compare the props.
#
By default, memouses
Object.is to compare each prop with its previous value.
However, specifying a custom equality function can be more efficient than default comparisons or re-rendering for more complex or specific scenarios.
const ExpensiveList = memo(
({ posts }) => {
return <div>{JSON.stringify(posts)}</div>;
},
(prevProps, nextProps) => {
// Only re-render if the last post or the list size changes
const prevLastPost = prevProps.posts[prevProps.posts.length - 1];
const nextLastPost = nextProps.posts[nextProps.posts.length - 1];
return (
prevLastPost.id === nextLastPost.id &&
prevProps.posts.length === nextProps.posts.length
);
}
)Prefer named functions over arrow functions when declaring a memoized component #
When defining memoized components, using named functions instead of arrow functions can improve clarity in React DevTools.
Arrow functions often result in generic names like _c2, making debugging and profiling more difficult.
const ExpensiveList = memo(
function ExpensiveListFn({ posts }) {
/// Rest of implementation
}
);Cache expensive computations or preserve references with useMemo
#
I will generally useMemo:
- When I have expensive computations that should not be repeated on each render.
- If the computed value is a non-primitive value that is used as a dependency in hooks like
useEffect. - The computed non-primitive value will be passed as a prop to a component wrapped in
memo; otherwise, this will break the memoization since React uses Object.is to detect whether props changed.
Use react-window or react-virtuoso to efficiently render lists
#
Never render a long list of items all at once—such as chat messages, logs, or infinite lists.
Doing so can cause the browser to freeze.
Instead, virtualize the list. This means rendering only the subset of items likely to be visible to the user.
Libraries like react-window, react-virtuoso or @tanstack/react-virtual are designed for this purpose.
Leverage useDebugValue in your custom hooks for better visibility in React DevTools
#
useDebugValue can be a handy tool for adding descriptive labels to your custom hooks in React DevTools.
This makes it easier to monitor their states directly from the DevTools interface.
Use the why-did-you-render library to track component rendering and identify potential performance bottlenecks
#
Sometimes, a component re-renders, and it’s not immediately clear why 🤦♀️.
While React DevTools is helpful, in large apps, it might only provide vague explanations like “hook #1 rendered,” which can be useless.
In such cases, you can turn to the why-did-you-render library. It offers more detailed insights into why components re-render, helping to pinpoint performance issues more effectively.
Prefer named functions over arrow functions within hooks such as useEffect to easily find them in React Dev Tools
#
If you have many hooks, finding them in React DevTools can be challenging.
One trick is to use named functions so you can quickly spot them.
function HelloWorld() {
useEffect(function logOnMount() {
console.log("🚀 ~ Hello, I just got mounted");
}, []);
return <>Hello World</>;
}Prefer functions over custom hooks #
Never put logic inside a hook when a function can be used 🛑.
In effect:
- Hooks can only be used inside other hooks or components, whereas functions can be used everywhere.
- Functions are simpler than hooks.
- Functions are easier to test.
- Etc.
Use ReactNode instead of JSX.Element | null | undefined | ... to keep your code more compact
#
Reference #
https://dev.to/_ndeyefatoudiop/101-react-tips-tricks-for-beginners-to-experts-4m11