Bạn đã bao giờ trải nghiệm cảm giác khó chịu khi một ứng dụng React bỗng dưng 'đứng hình' vài giây chỉ vì một thao tác nhỏ? Đó chính là lúc bạn đang đối mặt với 'Expensive Computation' – những phép tính nặng đô đang chiếm dụng luồng chính của JavaScript và khiến UI của bạn 'thở dốc'. Trong bài viết này, chúng ta sẽ cùng tìm hiểu nguyên nhân và những 'chiêu' hiệu quả để giải cứu UI của bạn khỏi cơn 'đột quỵ' này.
Expensive Computation là gì và tại sao nó lại 'đáng sợ'?
Expensive Computation là bất kỳ tác vụ nào tiêu tốn nhiều tài nguyên CPU hoặc thời gian xử lý. Ví dụ điển hình bao gồm:
- Duyệt qua các mảng dữ liệu cực lớn, thực hiện tính toán phức tạp (như lọc, sắp xếp, biến đổi hàng nghìn, hàng triệu phần tử).
- Thao tác DOM trực tiếp, phức tạp hoặc số lượng lớn.
- Tính toán đồ họa, xử lý hình ảnh phức tạp.
- Mã hóa/giải mã dữ liệu.
Tại sao nó lại 'đáng sợ'? Bởi vì JavaScript trong trình duyệt là đơn luồng (single-threaded). Điều này có nghĩa là tại một thời điểm, chỉ có thể có một tác vụ được thực thi trên luồng chính. Khi một tác vụ nặng chạy, nó sẽ chiếm toàn bộ luồng, ngăn cản trình duyệt cập nhật UI, xử lý sự kiện người dùng (như click, nhập liệu), dẫn đến hiện tượng treo, đơ, và trải nghiệm người dùng tệ hại.
Các 'chiêu' giải cứu UI khỏi cơn 'đột quỵ'
1. useMemo và useCallback: Bộ đôi hoàn hảo cho React
Đây là hai React Hooks giúp tối ưu hóa hiệu suất bằng cách ghi nhớ (memoize) kết quả hoặc định nghĩa hàm, tránh các tính toán hoặc tạo hàm lại không cần thiết.
useMemo(Ghi nhớ giá trị): Hook này sẽ chỉ thực hiện lại phép tính bên trong nó khi một trong các dependency (phụ thuộc) của nó thay đổi. Nếu các dependency không đổi, nó sẽ trả về kết quả đã được ghi nhớ trước đó. Điều này cực kỳ hữu ích cho các hàm trả về giá trị tốn kém.import React, { useMemo } from 'react';const MyComponent = ({ data }) => { // Giả sử computeExpensiveValue là một hàm tính toán nặng const expensiveValue = useMemo(() => { console.log('Performing expensive computation...'); return data.map(item => item * 2).reduce((sum, val) => sum + val, 0); }, [data]); // Chỉ tính lại khi 'data' thay đổi return ( <div> <p>Kết quả tính toán nặng: {expensiveValue}</p> </div> );};useCallback(Ghi nhớ hàm): Tương tựuseMemonhưng dùng để ghi nhớ định nghĩa của một hàm. Điều này quan trọng khi bạn truyền các hàm làm props xuống các component con đã được tối ưu hóa bằngReact.memo, giúp tránh re-render không cần thiết của component con.import React, { useState, useCallback } from 'react';import ChildComponent from './ChildComponent'; // Giả sử ChildComponent dùng React.memoconst ParentComponent = () => { const [count, setCount] = useState(0); // Hàm này sẽ chỉ được tạo lại khi 'count' thay đổi const handleClick = useCallback(() => { setCount(prevCount => prevCount + 1); }, []); // [] nghĩa là hàm này chỉ tạo 1 lần và không thay đổi return ( <div> <p>Count: {count}</p> <ChildComponent onClick={handleClick} /> </div> );};
Lưu ý quan trọng: Chỉ dùng useMemo và useCallback khi bạn đã xác định được có vấn đề về hiệu suất. Lạm dụng có thể gây tốn bộ nhớ và tăng overhead không cần thiết.
2. React.memo: Giảm tải render không cần thiết
React.memo là một Higher-Order Component (HOC) bọc quanh các functional component. Nó sẽ chỉ re-render component nếu các props của nó thay đổi. Đây là cách tuyệt vời để ngăn chặn các component con re-render khi props của chúng không thực sự thay đổi.
import React from 'react';// ChildComponent sẽ chỉ re-render khi prop 'data' thay đổiconst MemoizedChildComponent = React.memo(({ data }) => { console.log('ChildComponent re-rendered'); return <div>Dữ liệu: {data.length} phần tử</div>;});export default MemoizedChildComponent;Kết hợp React.memo với useCallback (cho các hàm props) và useMemo (cho các giá trị props) sẽ tạo nên một hệ thống tối ưu hóa hiệu quả.
3. Web Workers: Đẩy việc nặng ra 'thế giới' khác
Khi các tác vụ tính toán thực sự rất nặng và kéo dài (vài trăm mili giây trở lên), các giải pháp trên có thể không đủ. Lúc này, Web Workers là vị cứu tinh. Web Workers cho phép bạn chạy các script trong một luồng nền riêng biệt, hoàn toàn độc lập với luồng chính của trình duyệt. Điều này có nghĩa là UI của bạn sẽ không bao giờ bị chặn, dù cho tác vụ tính toán có nặng đến đâu.
- Khi nào dùng? Xử lý ảnh, mã hóa/giải mã dữ liệu, tính toán AI, xử lý dữ liệu lớn, hoặc bất kỳ tác vụ nào không cần truy cập DOM và mất nhiều thời gian.
- Cách dùng cơ bản:
Bước 1: Tạo file worker (e.g., my-worker.js)
// my-worker.jsonmessage = (e) => { const { data } = e.data; // Thực hiện tính toán nặng ở đây let result = 0; for (let i = 0; i < data; i++) { result += i; } postMessage(result); // Gửi kết quả về luồng chính};Bước 2: Sử dụng trong React Component
import React, { useEffect, useState } from 'react';const ExpensiveComputationComponent = () => { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); useEffect(() => { let worker; const doHeavyTask = () => { setLoading(true); worker = new Worker(new URL('./my-worker.js', import.meta.url)); worker.postMessage({ data: 1000000000 }); // Gửi dữ liệu cần tính toán worker.onmessage = (e) => { setResult(e.data); setLoading(false); worker.terminate(); // Kết thúc worker sau khi hoàn thành }; worker.onerror = (error) => { console.error('Worker error:', error); setLoading(false); worker.terminate(); }; }; // Khởi tạo worker khi component mount (hoặc khi cần) doHeavyTask(); // Cleanup function để đóng worker khi component unmount return () => { if (worker) { worker.terminate(); } }; }, []); // Chạy một lần khi component mount return ( <div> <h3>Sử dụng Web Worker</h3> <p>Status: {loading ? '<strong>Đang tính toán...</strong>' : 'Hoàn tất!'}</p> {result !== null && <p>Kết quả: {result}</p>} </div> );};export default ExpensiveComputationComponent;Ưu điểm: UI mượt mà tuyệt đối, không bị gián đoạn.Nhược điểm: Không thể truy cập trực tiếp DOM, giao tiếp giữa luồng chính và worker phức tạp hơn (qua postMessage và onmessage).
Khi nào thì dùng 'chiêu' nào?
useMemo/useCallback/React.memo: Là lựa chọn hàng đầu cho các tối ưu hóa nội bộ trong React. Dùng khi bạn muốn tránh việc tính toán lại các giá trị, tạo lại các hàm, hoặc re-render các component con khi các props của chúng không thực sự thay đổi. Thích hợp cho các tác vụ tính toán 'vừa phải', thường là trong phạm vi vài mili giây.- Web Workers: Dành cho những 'ca khó', khi tác vụ tính toán quá nặng, gây tắc nghẽn đáng kể (>50ms-100ms) và cần chạy hoàn toàn độc lập với UI.
Lời kết
Việc xử lý các tính toán nặng trong React không chỉ là một kỹ năng mà còn là một nghệ thuật để mang lại trải nghiệm người dùng mượt mà và chuyên nghiệp. Đừng để ứng dụng của bạn 'đứng hình' chỉ vì những phép tính. Hãy trang bị cho mình những công cụ này, hiểu rõ khi nào cần dùng từng 'chiêu', và làm chủ hiệu suất React để tạo ra những ứng dụng không chỉ mạnh mẽ mà còn thân thiện với người dùng!