Đã gần 4 năm kể từ khi mình viết bài blog đầu tiên về React.js Tôi cố học React, hồi đấy mình mới bắt đầu học React.js và bài blog thì khá đơn giản (đa phần là dịch từ docs ra), học được vài ngày thì mình như cảm thấy mình đã đủ "trình" để làm gì đó nghiêm túc rồi (Dunning–Kruger effect =)) và thế là mình dựng lên cái blog này trong phút nông nổi đó. Nhưng đó là chuyện 4 năm trước rồi, nhiều thứ giờ đã thay đổi chỉ có mình là vẫn như xưa thôi, nhưng "trình" thì chắc chắn hơn trước rồi =)).

Hôm nay mình sẽ cho các bạn nghe bài "Trình", à nhầm mình sẽ cho các bạn thấy nếu mình viết lại bài blog nội dung tương tự cho beginner thì nó sẽ như thế nào. Let's get started!

React.js cơ bản

React.js là một thư viện Javascript để xây dựng UI bằng cách lắp ghép từ các components - vốn là building block của một React page... bla.bla thành thật mà nói chắc đa số mọi người đều lướt qua mấy cái lời giới thiệu kiểu như văn mẫu này, chúng ta đang sống ở năm 2025 cơ mà Tiktok, Youtube Shorts đang trending mà, AI tóm tắt nội dung được coi là điều bình thường rồi, giờ chúng ta sống vội nên là let's move to the fun part!

Khi nào React re-render?

Mình đi từ cái căn bản nhất của React đó chính là cái tên "React" của nó, cho các bạn ko biết thì React có nghĩa là "phản ứng" - hành động xảy ra sau khi chứng kiến hoặc bị kích thích bởi điều gì đó. Này là khái niệm trong đầu mình thôi nhưng mà chắc tra từ điển cũng nghĩa tương đương - bạn biết ko những người mà hay nghĩ ra khái niệm thì người ta hay gọi là triết gia đấy? Anyway, bạn có thể thấy "phản ứng" chỉ xảy ra khi chứng kiến hoặc bị kích thích bởi 1 tác nhân gì đó (trigger). Ví dụ bạn nghe bài "Trình" hay quá xong bạn khóc chẳng hạn, thì tác nhân (trigger) chính là việc bạn nghe bài "Trình" còn phản ứng chính là bạn khok (react). Vui vậy thôi, khi bạn sử dụng một ứng dụng web bạn nhấn một cái nút trên web và bạn cũng mong có điều gì đó hiện ra khi bạn nhấn cái nút ấy, ví dụ bạn nhấn nút Like và cái nút like ấy sáng lên màu xanh, thì việc bạn nhấn nút Like là trigger và nút ấy sáng lên là react. Trong React.js để cái nút đấy sáng lên được thì nó phải trải qua bước re-render - hay có thể nói re-render chính là trái tim của React.

[?] Vậy khi nào thì React.js re-render?
Well hãy nhìn vào demo bên dưới, bạn có thể tương tác với nó thông qua InteractiveControls cho nó trực quan hơn. Mỗi lần component nào bị re-render thì cái outline của nó sẽ sáng lên 1s và số lượng renders cũng được tăng lên ở bên góc phải của component đó.

Bạn có thể thấy khi ta thay đổi states của Parent component - thay đổi count, theme thì Parent component re-render để apply các cái thay đổi đó, nhưng ta cũng thấy thằng ChildComponent cũng bị re-render, lý do là vì nó nhận props từ state của ParentComponent nên khi state đó thay đổi thì thằng ChildComponent cũng bị re-render. Vậy ta có 2 thứ cơ bản trigger re-render ở đây:

  • State thay đổi
  • Props thay đổi

Vẫn còn 2 thứ khác khiến component re-render đó là: component cha re-render và context thay đổi. Nói qua về component cha re-render dẫn đến thằng con cũng bị re-render theo, cái này chỉ xảy ra nếu component được dùng trực tiếp trong component cha xem đoạn code bên dưới:

Ví dụ 1: ChildComponent bị re-render khi ParentComponent re-render

// ChildComponent sẽ bị re-render trong case này // mặc dù props hay state của nó chả thay đổi gì function ParentComponent() { return ( <div> <h1>Below is my child</h1> <ChildComponent /> {/* thằng này sẽ bị re-render khi mà ParentComponent re-render */} </div> ) }

Ví dụ 2: ChildComponent ko bị re-render vì mình ko sử dụng trực tiếp ChildComponent trong ParentComponent, mình dùng nó thông qua một cái props đặc biệt children.

function ParentComponent({ children }) { return ( <div> <h1>Below is my child</h1> {children} {/* thằng này ko bị re-render khi mà ParentComponent re-render */} </div> ) } function SomewhereInTheApp() { <ParentComponent> <ChildComponent /> </ParentComponent> }

Bạn có thể thay đổi message trong Child Component State trong ví dụ trực quan bên trên để kiểm chứng, khi bạn thay đổi message rồi nhấn "Update" button thằng ChildComponent bị re-render nhưng thằng GrandChildComponent thì ko bị re-render. Đó là vì mình truyền child components qua props children. Take a look at this simple code:

function ComponentNode({ name, children }) { // states return ( <div> <p>{name}</p> {children} </div> ) } function ComponentTree() { return ( <ComponentNode name="App (Parent)"> <ComponentNode name="ChildComponent"> <ComponentNode name="GrandChildComponent" /> </ComponentNode> </ComponentNode> ) }

Okay, vậy là chúng ta đã điểm qua 3/4 lý do chính khiến component của bạn "nhảy múa" re-render rồi. Cái cuối cùng, cũng là cái đôi khi là phần khó hiểu nhất đối với người mới: Context thay đổi.

Context thay đổi

Nhớ lại cái ví dụ triết gia ở trên, "phản ứng" xảy ra khi bị "kích thích" bởi một tác nhân. Context trong React giống như một cái "loa phát thanh" chung vậy. Bạn tạo ra một Context (giống như lập một kênh phát thanh), cho nó một giá trị (ví dụ: thông tin user đang đăng nhập, cài đặt theme tối/sáng).

Bất kỳ component nào "dò đài" kênh phát thanh này bằng cách sử dụng useContext (tức là nghe xem cái loa nói gì) đều sẽ "phản ứng" (re-render) mỗi khi giá trị từ cái loa thay đổi. Kể cả khi props hay state của chính component đó không có gì mới, chỉ cần cái loa Context "khụ một tiếng" là cả đám "thính giả" đang dò đài đấy sẽ giật mình re-render.

Ví dụ, bạn có một ThemeContext chứa giá trị 'light' hoặc 'dark'. Hàng trăm component con cháu chắt chút chít trong cây component của bạn đều đang sử dụng useContext(ThemeContext) để biết màu theme mà hiển thị. Khi bạn chuyển theme từ 'light' sang 'dark', giá trị của ThemeContext thay đổi. BÙM! Tất cả hàng trăm component đang sử dụng ThemeContext đấy sẽ re-render, bất kể chúng có đang hiển thị trên màn hình hay không, bất kể props của chúng có thay đổi hay không. Lý do đơn giản là chúng "đã nghe thấy" tín hiệu thay đổi từ cái loa Context.

Điều này có vẻ tiện lợi, đúng không? Chỉ cần thay đổi ở một chỗ, cả hệ thống cập nhật theo. Nhưng cũng chính nó là nguồn cơn của những vụ re-render "khủng bố", đặc biệt trong các ứng dụng lớn với nhiều Context và giá trị thay đổi thường xuyên.

Vậy tóm lại, 4 lý do (trigger) chính khiến React component re-render là:

  1. State của component thay đổi.
  2. Props của component thay đổi.
  3. Component cha bị re-render (trừ khi dùng children prop).
  4. Context mà component đó đang sử dụng thay đổi.

Hiểu được khi nào re-render là bước đầu tiên.

[?] Tại sao cần tối ưu re-render?
Nếu ứng dụng của bạn chỉ có vài component đơn giản thì việc re-render thoải mái không phải là vấn đề lớn. Nhưng với các ứng dụng phức tạp, có nhiều component lồng nhau, re-render không cần thiết có thể dẫn đến:

  • Giảm hiệu năng: CPU phải làm việc nhiều hơn để chạy lại code render và so sánh Virtual DOM, gây chậm trễ, đặc biệt trên các thiết bị cấu hình thấp.
    • Tăng tiêu thụ pin: Trên thiết bị di động, việc render quá nhiều có thể làm hao pin nhanh chóng.
    • Trải nghiệm người dùng tệ: Ứng dụng giật lag, phản hồi chậm khi có tương tác.

Bước tiếp theo là làm sao để "thuần hóa" nó, tránh những pha re-render không cần thiết gây tốn tài nguyên và chậm ứng dụng. Đến với phần được mong chờ nhất:

Tips để Optimize React Rendering

Okay, giờ bạn đã biết "thủ phạm" gây ra những vụ re-render rồi. Vấn đề là đôi khi React nó "nhạy cảm" quá mức cần thiết, re-render cả những thứ không cần re-render, đặc biệt là mấy anh em component ở dưới sâu trong cây UI mà chả có gì thay đổi. Nói chung là cố gắng làm cho thằng nào thay đổi thì thằng đấy re-render lại thôi, còn những thằng khác ko bị thay đổi thì ko phải re-render làm gì.

Mục tiêu của tối ưu re-render là giảm thiểu số lượng component phải chạy lại function render của nó một cách không cần thiết. Đây là vài bí kíp bỏ túi:

1. React.memo() - "Nhớ nhé, chỉ khi có gì mới thì mới làm lại!"

Đây là một Higher-Order Component (HOC) mà bạn bọc quanh component của mình. Nó giống như một cái "bảo vệ cửa" cho component vậy. React.memo sẽ kiểm tra props mới nhận được với props cũ. Nếu tất cả props đều giống hệt (shallow comparison - so sánh nông), nó sẽ nói "Thôi, ông vào đi, không cần phải render lại đâu, cái kết quả render lần trước vẫn dùng được!".

const ExpensiveComponent = React.memo(function ExpensiveComponent(props) { // Component này tốn tài nguyên để render // ... return (/* JSX */); });

Dùng React.memo cho các component "phức tạp" (tốn nhiều thời gian render) hoặc các component nhận nhiều props mà chỉ một số ít trong đó thay đổi là rất hiệu quả.

LƯU Ý

const a = {}; const b = {}; const c = "val"; const d = "val"; a === b; // false c === d; // true

Cẩn thận khi truyền propsobject hoặc function nhé. Do shallow comparison, nếu object hoặc function đó được tạo mới trên mỗi lần component cha re-render (dù nội dung giống nhau), React.memo sẽ nghĩ là props đã thay đổi và vẫn re-render component con. Đây là lúc cần đến useMemouseCallback.

2. useMemo() - "Tính toán mệt lắm, bao giờ đầu vào đổi thì mới tính lại!"

Hook này dùng để "ghi nhớ" (memoize) một giá trị được tính toán. Bạn truyền vào một hàm tính toán và một mảng dependencies (các "đầu vào" cho hàm tính toán đó). useMemo sẽ chỉ chạy lại hàm tính toán và trả về giá trị mới khi một trong các dependency trong mảng thay đổi. Nếu dependencies không đổi, nó trả về giá trị đã "nhớ" từ lần trước.

function MyComponent({ list }) { // Giả sử việc lọc list này tốn thời gian const filteredList = useMemo(() => { console.log('Calculating filteredList...'); // Check xem khi nào chạy lại return list.filter(item => item.isActive); }, [list]); // Chỉ chạy lại khi list thay đổi return ( <ul> {filteredList.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> ); }

Dùng useMemo khi bạn có một phép tính tốn tài nguyên (lọc mảng lớn, tính toán phức tạp) mà kết quả của nó chỉ phụ thuộc vào một vài giá trị nhất định. Nó giúp tránh việc tính toán lại không cần thiết trên mỗi lần re-render. Ngoài ra, useMemo cũng giúp đảm bảo rằng các object hoặc mảng được tạo ra bên trong component có "tham chiếu" (reference) ổn định giữa các lần render nếu dependencies không đổi. Cái này quan trọng khi dùng các object/array này làm props cho component con được React.memo bọc.

3. useCallback() - "Hàm này quan trọng, đừng tạo bản sao mới mỗi lần nhé!"

Tương tự useMemo, nhưng useCallback dùng để "ghi nhớ" (memoize) một hàm. Bạn truyền vào một hàm và mảng dependencies. useCallback sẽ trả về một phiên bản "ghi nhớ" của hàm đó, và phiên bản này sẽ chỉ được tạo lại khi một trong các dependency thay đổi.

import React, { useState, useCallback, memo } from 'react'; const ChildComponent = memo(({ onClick }) => { console.log('ChildComponent re-rendered'); return <button onClick={onClick}>Click me</button>; }); function ParentComponent() { const [count, setCount] = useState(0); // Hàm này sẽ được "ghi nhớ" và chỉ tạo lại khi count thay đổi // Nếu count không đổi, React.memo ở ChildComponent sẽ thấy prop onClick không đổi const handleClick = useCallback(() => { setCount(count + 1); }, [count]); // Dependency là count // Nếu dùng như này thì mỗi lần ParentComponent re-render, hàm này sẽ được tạo lại // const handleClickWithoutCallback = () => { // setCount(count + 1); // }; return ( <div> <p>Count: {count}</p> {/* ChildComponent được memo, nên nó sẽ chỉ re-render khi props thay đổi */} {/* Truyền hàm đã dùng useCallback */} <ChildComponent onClick={handleClick} /> {/* Nếu truyền hàm chưa dùng useCallback vào đây, ChildComponent sẽ re-render mỗi lần ParentComponent re-render */} {/* <ChildComponent onClick={handleClickWithoutCallback} /> */} </div> ); }

useCallback cực kỳ hữu ích khi bạn truyền các hàm xử lý sự kiện xuống các component con được bọc bởi React.memo. Nếu không dùng useCallback, mỗi lần component cha re-render, hàm đó sẽ được tạo lại (có tham chiếu mới), và React.memo ở component con sẽ nghĩ rằng prop đó đã thay đổi, dẫn đến re-render không cần thiết.

4. Sử dụng children prop (Nhắc lại lần nữa vì nó quan trọng)

Như đã nói ở trên, việc truyền JSX thông qua prop children giúp ngăn component con bị re-render khi component cha re-render, miễn là props của component con đó không thay đổi. Đây là một kỹ thuật đơn giản nhưng hiệu quả để "cô lập" một phần cây UI khỏi re-render của cha nó.

5. Nâng State lên cao (Lifting State Up)

Nguyên tắc này không trực tiếp là hook tối ưu, nhưng là một chiến lược kiến trúc component giúp giảm thiểu phạm vi re-render. Nếu nhiều component cần cùng một state hoặc một state chỉ ảnh hưởng đến một phần nhỏ của cây component, hãy đặt state đó ở component cha chung gần nhất hoặc thậm chí cao hơn nữa nếu cần chia sẻ giữa các nhánh độc lập. Việc này giúp tránh re-render toàn bộ cây chỉ vì một state nhỏ ở các component dưới sâu thay đổi.

import React, { useState, useCallback } from "react"; function CounterDisplay({ count }) { console.log("CounterDisplay re-rendered"); return <p>Current Count: {count}</p>; } const CounterButton = React.memo(({ onClick }) => { console.log("CounterButton re-rendered"); return <button onClick={onClick}>Increment</button>; }); export default function CounterSection() { // Component cha chung const [count, setCount] = useState(0); // State được đặt ở đây const incrementCount = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []); return ( <div> <h2>Simple Counter</h2> {/* Truyền state và hàm xử lý state xuống component con */} <CounterDisplay count={count} /> <CounterButton onClick={incrementCount} /> </div> ); }

6. Cẩn thận với Context - Chia nhỏ nếu cần

Nếu Context của bạn chứa nhiều loại giá trị có tần suất thay đổi khác nhau, hãy cân nhắc chia nó thành nhiều Context nhỏ hơn. Ví dụ, thay vì một AppContext chứa cả thông tin người dùng (thay đổi khi đăng nhập/đăng xuất) và cài đặt theme (thay đổi khi user chuyển theme), bạn có thể tạo UserContext riêng và ThemeContext riêng. Bằng cách này, các component chỉ sử dụng ThemeContext sẽ không bị re-render khi thông tin người dùng thay đổi, và ngược lại. Việc này giúp tránh re-render không cần thiết trên diện rộng khi chỉ một phần nhỏ dữ liệu trong Context thay đổi.

// Chia nhỏ Context import React, { createContext, useContext, useState } from 'react'; const UserContext = createContext(); const ThemeContext = createContext(); function AppProviders({ children }) { const [user, setUser] = useState({ name: 'Guest', id: null }); const [theme, setTheme] = useState('light'); const login = (userData) => setUser(userData); const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light'); return ( <UserContext.Provider value={{ user, login }}> <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> </UserContext.Provider> ); } // Component sử dụng Context mới function UserNameDisplay() { const { user } = useContext(UserContext); // Chỉ dùng UserContext console.log('UserNameDisplay re-rendered'); // Chỉ re-render khi user thay đổi return <p>Logged in as: {user.name}</p>; } function ThemeSwitcher() { const { theme, toggleTheme } = useContext(ThemeContext); // Chỉ dùng ThemeContext console.log('ThemeSwitcher re-rendered'); // Chỉ re-render khi theme thay đổi return ( <button onClick={toggleTheme}> Switch to {theme === 'light' ? 'dark' : 'light'} theme </button> ); } // Component cha sử dụng Provider mới function App() { return ( <AppProviders> <h1>My App</h1> <UserNameDisplay /> <ThemeSwitcher /> </AppProviders> ); } // Bây giờ, khi bạn gọi toggleTheme(), chỉ state theme thay đổi. // UserContext không đổi, nên UserNameDisplay không bị re-render. // ThemeContext thay đổi, nên ThemeSwitcher bị re-render. // Việc chia nhỏ Context giúp giới hạn phạm vi re-render hiệu quả hơn.

Có thể nhiều bạn đã biết

Làm sao để biết component nào đang re-render và tại sao? Hãy dùng React Developer Tools (extension). Mở DevTools, chuyển sang tab "Profiler". Nhấn nút record màu xanh, thực hiện các thao tác trên ứng dụng của bạn, rồi nhấn record lần nữa để dừng. Profiler sẽ cho bạn thấy timeline của các lần render, component nào re-render, và thậm chí lý do re-render trong một số trường hợp. Đây là công cụ đắc lực nhất của bạn để xác định "điểm nóng" cần tối ưu.

Kết bài

React rendering là một chủ đề rộng và có nhiều khía cạnh sâu hơn (như Concurrent Mode, Server Components). Nhưng với những kiến thức cơ bản về khi nào re-render và các kỹ thuật tối ưu kể trên, bạn đã có đủ hành trang để bắt đầu xây dựng các ứng dụng React hiệu quả hơn rồi.

Nhớ rằng, tối ưu là một quá trình. Đừng cố gắng tối ưu tất cả mọi thứ từ đầu (over optimization). Hãy tập trung vào những phần mà bạn thấy (hoặc dùng Profiler) là đang chậm hoặc gây ra nhiều re-render không cần thiết.

Hy vọng bài viết này giúp bạn hiểu rõ hơn về trái tim 💝 của React. Hẹn gặp lại ở những bài blog tiếp theo!