Tất tần tật về re-render trong React: vấn đề và cách xử lý
Việc re-render trong React là gì?
Khi nói về hiệu suất của React, có hai giai đoạn chính mà chúng ta cần quan tâm:
- render initial - xảy ra khi một component đầu tiên xuất hiện trên màn hình
- re-render - lần render thứ hai và bất kỳ render liên tiếp nào của một component đã có trên màn hình
Re-render xảy ra khi React cần cập nhật ứng dụng với một số dữ liệu mới. Thông thường, điều này xảy ra do người dùng tương tác với ứng dụng hoặc một số dữ liệu bên ngoài được cung cấp thông qua một yêu cầu bất đồng bộ hoặc một số mô hình đăng ký.
Ứng dụng không tương tác không có bất kỳ cập nhật dữ liệu bất đồng bộ nào sẽ không bao giờ re-render, và do đó không cần quan tâm đến tối ưu hóa hiệu suất re-render.
Re-render cần thiết và không cần thiết là gì?
Re-render cần thiết - khi source của component có thay đổi, hoặc khi trực tiếp sử dụng data mới. Ví dụ, nếu một người dùng gõ vào ô input, component quản lý trạng thái của nó cần cập nhật chính nó sau mỗi lần nhấn phím, tức là re-render.
Re-render không cần thiết - re-render của một component được truyền qua ứng dụng thông qua các cơ chế re-render khác nhau do lỗi hoặc kiến trúc ứng dụng không hiệu quả. Chẳng hạn, nếu một người dùng gõ vào ô input, và toàn bộ trang re-render sau mỗi lần nhấn phím, trang đã được re-render không cần thiết.
Re-render không cần thiết không phải là vấn đề: React rất nhanh và thường có thể xử lý chúng mà người dùng không nhận thấy bất kỳ điều gì.
Tuy nhiên, nếu re-render xảy ra quá thường xuyên và/hoặc trên các component rất nặng, điều này có thể dẫn đến trải nghiệm người dùng xuất hiện "chậm chạp", các độ trễ rõ ràng sau mỗi tương tác, hoặc ngay cả ứng dụng trở nên hoàn toàn không phản hồi.
Khi nào component React re-render chính nó?
Có bốn lý do khiến một component tự re-render: state thay đổi, re-render của parent (hoặc children), thay đổi context và thay đổi hook. Cũng có một điều rất sai lầm: rằng re-render xảy ra khi props của component thay đổi. Điều này không đúng (xem phần giải thích dưới đây).
Lý do re-render: State thay đổi
Khi state của một component thay đổi, nó sẽ tự re-render. Thông thường, điều này xảy ra hoặc trong một callback hoặc trong hook useEffect
.
Thay đổi state là nguồn "gốc" của tất cả các re-render.
Ví dụ:
Lý do re-render: Parent re-render
Một component sẽ tự re-render nếu cha của nó re-render. Hoặc, nếu chúng ta nhìn vào vấn đề này từ hướng ngược lại: khi một component re-render, nó cũng re-render tất cả các children component.
Việc re-render luôn diễn ra từ trên xuống: việc re-render của children không kích hoạt việc re-render của Parent. (Có một số hạn chế và trường hợp ngoại lệ ở đây).
Lý do re-render: context thay đổi
Khi giá trị trong Context Provider thay đổi, tất cả các component sử dụng Context này sẽ re-render, ngay cả khi chúng không sử dụng phần dữ liệu đã thay đổi một cách trực tiếp. Những re-render này không thể ngăn chặn bằng memoization trực tiếp, nhưng có một số giải pháp đối phó có thể mô phỏng nó.
Lý do re-render: hook thay đổi
Mọi thứ đang diễn ra bên trong một hook "thuộc về" component sử dụng nó. Cùng một quy tắc liên quan đến Context và Thay đổi trạng thái áp dụng ở đây:
- state thay đổi bên trong hook sẽ kích hoạt re-render không thể ngăn chặn của component dùng nó
- nếu hook sử dụng Context và giá trị Context thay đổi, nó sẽ kích hoạt re-render không thể ngăn chặn của component dùng nó
Các hook có thể được nối liền mạch. Mỗi hook duy nhất bên trong chuỗi vẫn "thuộc về" component dùng nó, và cùng một quy tắc áp dụng cho bất kỳ trong số chúng.
Lý do re-render: thay đổi props (Dễ gặp)
Không quan trọng props của component có thay đổi hay không khi nói về re-render của các component không được memo hoá.
Để props có thể thay đổi, chúng cần được cập nhật bởi component cha. Điều này có nghĩa là cha sẽ phải re-render, điều này sẽ kích hoạt re-render của component con bất kể props của nó.
Chỉ khi sử dụng các kỹ thuật memo hoá (React.memo
, useMemo
), thì việc thay đổi props trở nên quan trọng.
Ngăn chặn re-render với composition
Không nên: Tạo Component trong hàm render hoặc functional Component
Việc tạo component bên trong hàm render của một component khác là một anti-pattern có thể là nguyên nhân chính làm giảm hiệu suất. Trên mỗi lần re-render, React sẽ re-mount component này (tức là hủy nó và tạo lại từ đầu), điều này sẽ chậm hơn rất nhiều so với việc re-render bình thường. Thêm vào đó, điều này sẽ dẫn đến các lỗi như:
- có thể có "flash" nội dung trong quá trình re-render
- trạng thái được đặt lại trong component sau mỗi lần re-render
- useEffect không có phụ thuộc nào được kích hoạt sau mỗi lần re-render
- nếu một component đang được focus, focus sẽ bị mất
Nên: Ngăn chặn re-render với composition: Move State Down
Mô hình này có thể hữu ích khi một Component quản lý nhiều state, và state này chỉ được sử dụng trên một phần nhỏ và tách biệt của cây render. Một ví dụ điển hình sẽ là open/close một modal với một nút nhấn trong một thành phần phức tạp mà render một phần đáng kể của một trang.
Trong trường hợp này, state điều khiển xuất hiện modal, và nút kích hoạt cập nhật có thể được đóng gói trong Component nhỏ hơn. Kết quả là, thành phần lớn hơn sẽ không re-render khi trạng thái thay đổi.
Nên: Ngăn chặn việc re-render với composition: children as Props
Điều này cũng có thể được gọi là "Wrap state around children". Mẫu này tương tự như "Move state down": nó đổi component lớn thành nhiều component nhỏ hơn. Sự khác biệt ở đây là state được sử dụng trên một phần tử bọc một phần tử làm chậm quá trình render, vì vậy nó không thể được trích xuất một cách dễ dàng. Một ví dụ điển hình có thể là các hàm callback onScroll
hoặc onMouseMove
được gán cho phần tử gốc của một thành phần.
Trong tình huống này, quản lý trạng thái và các thành phần sử dụng state đó có thể được trích xuất thành một component nhỏ hơn, và Slow Component có thể được chuyển đến nó dưới dạng children
. Từ góc nhìn của component nhỏ hơn, children
chỉ là props, vì vậy chúng sẽ không bị ảnh hưởng bởi sự thay đổi trạng thái và do đó sẽ không tái render.
Nên: Ngăn chặn việc re-render với composition: components as props
Khá giống với mẫu trước, với cùng hành vi: nó đóng gói trạng thái bên trong một thành phần nhỏ hơn, và các thành phần nặng được chuyển đến nó dưới dạng props. Props không bị ảnh hưởng bởi sự thay đổi trạng thái, vì vậy các thành phần nặng sẽ không tái render.
Có thể hữu ích khi một số thành phần nặng không phụ thuộc vào trạng thái, nhưng không thể được trích xuất thành children như một nhóm.
Ngăn chặn Re-render với React.memo
Đóng gói một thành phần trong React.memo
sẽ dừng chuỗi render lại phía dưới được kích hoạt ở đâu đó trên cây render, trừ khi props của thành phần này đã thay đổi.
Điều này có thể hữu ích khi render một thành phần nặng mà không phụ thuộc vào nguồn của việc render lại (tức là trạng thái, dữ liệu đã thay đổi).
1. React.memo: Component với props
Tất cả props không phải là giá trị nguyên thủy phải được ghi nhớ để React.memo hoạt động
2. React.memo: Component as Props hoặc children
React.memo
phải được áp dụng cho các phần tử được truyền như children/props. Việc memo Component cha sẽ không hoạt động: children và props sẽ là đối tượng, vì vậy chúng sẽ thay đổi với mỗi lần render lại.
Cải thiện hiệu suất re-render với useMemo/useCallback
Không nên: useMemo/useCallback không cần thiết trên props
Việc ghi nhớ props bởi chính nó sẽ không ngăn chặn việc render lại của một component con. Nếu một component cha render lại, nó sẽ kích hoạt việc render lại của một component con bất kể props của nó.
Nên: useMemo/useCallback cần thiết
Nếu một Component con được bọc trong React.memo
, tất cả props không phải là giá trị nguyên thủy phải được ghi nhớ
Nếu một component sử dụng giá trị không nguyên thủy như một dependency trong hooks như useEffect
, useMemo
, useCallback
, nó nên được ghi nhớ.
Nên: useMemo dành cho các phép tính tốn kém
Một trong những trường hợp sử dụng cho useMemo
là tránh việc tính toán tốn kém trên mỗi lần render lại.
useMemo
có cái giá của nó (tiêu tốn một chút bộ nhớ và làm chậm quá trình render ban đầu một chút), vì vậy nó không nên được sử dụng cho mọi phép tính. Trong React, việc gắn kết và cập nhật các component sẽ là phép tính tốn kém nhất trong hầu hết các trường hợp (trừ khi bạn thực sự đang tính toán số nguyên tố, điều mà bạn không nên làm ở phía frontend).
Do đó, trường hợp sử dụng thông thường cho useMemo
sẽ là ghi nhớ các phần tử React. Thông thường là các phần của cây render hiện có hoặc kết quả của cây render được tạo ra, như một hàm map trả về các phần tử mới.
Cái giá của các hoạt động "thuần" javascript như sắp xếp hoặc lọc một mảng thường là không đáng kể, so với việc cập nhật các Component.
Cải thiện hiệu suất Re-render của lists
Ngoài các quy tắc và mô hình re-render thông thường, thuộc tính key
có thể ảnh hưởng đến hiệu suất của danh sách trong React.
Quan trọng: chỉ cung cấp thuộc tính key
sẽ không cải thiện hiệu suất của danh sách. Để ngăn chặn re-render của các phần tử danh sách, bạn cần bao gồm chúng trong React.memo
và tuân theo tất cả các thực hành tốt nhất của nó.
Giá trị trong key
nên là một chuỗi, đó là nhất quán giữa các lần re-render cho mỗi phần tử trong danh sách. Thông thường, id
của mục hoặc index
của mảng được sử dụng cho điều đó.
Việc sử dụng index
của mảng như key là ổn, nếu danh sách là tĩnh, tức là các phần tử không được thêm/xóa/chèn/sắp xếp lại.
Sử dụng index của mảng trên các danh sách động có thể dẫn đến:
- lỗi nếu các mục có trạng thái hoặc bất kỳ phần tử không được kiểm soát nào (như đầu vào form)
- hiệu suất giảm nếu các mục được bao gồm trong React.memo
Không nên: gán giá trị ngẫu nhiên cho key trong danh sách
Giá trị được tạo ngẫu nhiên không bao giờ nên được sử dụng cho giá trị trong thuộc tính key
trong danh sách. Chúng sẽ dẫn đến việc React tái gắn kết các mục trên mỗi lần re-render, điều này sẽ dẫn đến:
- hiệu suất rất kém của danh sách
- lỗi nếu các mục có trạng thái hoặc bất kỳ phần tử không được kiểm soát nào (như đầu vào form)
Ngăn chặn re-render do Context
1. Ngăn chặn re-render của Context: memoizing giá trị Provider
Nếu Context Provider không được đặt ngay ở file root/main của ứng dụng, và có khả năng nó có thể re-render chính nó khi các giá trị trong root khác thay đổi, giá trị của nó nên được ghi nhớ.
2. Ngăn chặn re-render của Context: phân chia dữ liệu và API
Nếu trong Context có sự kết hợp của dữ liệu và API (các getter và setter), chúng có thể được chia thành các provider nhỏ hơn khác nhau dưới cùng một Component. Theo cách đó, các thành phần chỉ sử dụng API sẽ không re-render khi dữ liệu thay đổi.
3. Ngăn chặn re-render của Context: chia dữ liệu thành từng khối
Nếu Context quản lý một số khối dữ liệu độc lập, chúng có thể được chia thành các provider nhỏ hơn dưới cùng một Provider. Theo cách đó, chỉ những người tiêu dùng của khối đã thay đổi sẽ re-render.
4. Ngăn chặn re-render của Context: Context Selector
Không có cách nào để ngăn chặn một component sử dụng một phần của giá trị Context từ việc re-render, ngay cả khi phần dữ liệu đã sử dụng chưa thay đổi, ngay cả với hook useMemo
.
Tuy nhiên, Context Selector có thể được giả mạo với việc sử dụng các High Order Component và React.memo
.
Bài viết được tham khảo thêm ở: developerway.com