10 Kỹ Thuật Tối Ưu React Giảm 60% Thời Gian Render


Chào mừng các bạn quay trở lại với blog của mình! Hôm nay, chúng ta sẽ cùng nhau khám phá một chủ đề vô cùng quan trọng, là “vũ khí bí mật” để xây dựng những ứng dụng React không chỉ đẹp mắt mà còn cực kỳ mượt mà và nhanh nhạy: Tối ưu hiệu suất React.

Trong thế giới phát triển web đầy cạnh tranh ngày nay, hiệu suất ứng dụng không còn là một yếu tố tùy chọn mà đã trở thành một yêu cầu bắt buộc. Người dùng ngày càng khó tính hơn, họ mong đợi mọi thứ phải diễn ra tức thời, không giật lag. Nếu ứng dụng React của bạn chậm chạp, xin chia buồn, bạn có thể đang mất đi kha khá người dùng tiềm năng đấy!

Vậy làm thế nào để “thổi bay” những vấn đề về hiệu suất trong React? Đừng lo lắng, bài viết này sẽ trang bị cho bạn 10 mẹo “vàng” đã được kiểm chứng, giúp bạn biến ứng dụng React của mình từ “chậm như rùa” thành “nhanh như gió”! Cùng bắt đầu thôi!

1. Sử Dụng React.memo Để Tránh Render Không Cần Thiết

Đây là một trong những kỹ thuật cơ bản nhưng vô cùng hiệu quả. React.memo là một Higher-Order Component (HOC) giúp bạn “ghi nhớ” (memoize) một component. Khi các props của component đó không thay đổi, React sẽ bỏ qua việc render lại component này, tiết kiệm đáng kể tài nguyên.

Khi nào nên dùng? Khi bạn có các functional component mà các props của chúng ít thay đổi hoặc không thay đổi, nhưng chúng lại được render lại quá nhiều lần do component cha render.

Ví dụ:

// Component không dùng React.memo
function ProductItem(props) {
  console.log('Rendering ProductItem:', props.name);
  return <li>{props.name} - ${props.price}</li>;
}

// Component dùng React.memo
const MemoizedProductItem = React.memo(function ProductItem(props) {
  console.log('Rendering MemoizedProductItem:', props.name);
  return <li>{props.name} - ${props.price}</li>;
});

function ProductList({ products }) {
  const [filter, setFilter] = useState('');

  return (
    <div>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      <ul>
        {products.map(product => (
          // Nếu dùng ProductItem, mỗi lần filter thay đổi, ProductItem sẽ render lại dù props.name, props.price không đổi
          // Nếu dùng MemoizedProductItem, nó chỉ render lại khi props.name hoặc props.price thực sự thay đổi
          <MemoizedProductItem key={product.id} name={product.name} price={product.price} />
        ))}
      </ul>
    </div>
  );
}

Trong ví dụ trên, nếu không dùng React.memo, mỗi lần bạn gõ vào ô input để thay đổi filter, ProductItem sẽ render lại cho tất cả các sản phẩm, ngay cả khi tên và giá sản phẩm không hề thay đổi. Với React.memo, MemoizedProductItem sẽ chỉ render lại khi props.name hoặc props.price thực sự thay đổi.

2. Tận Dụng useCallbackuseMemo Để Tối Ưu Logic

Hai hook này là “cặp bài trùng” giúp tối ưu hóa việc tái sử dụng các hàm và giá trị đã được tính toán.

  • useCallback: Giúp bạn “ghi nhớ” một hàm. Nó trả về một phiên bản đã được memoize của callback function mà bạn truyền vào. Điều này cực kỳ hữu ích khi bạn truyền các hàm này xuống làm props cho các child component được bọc bởi React.memo. Nếu không dùng useCallback, mỗi lần component cha render, một hàm mới sẽ được tạo ra, và React.memo sẽ cho rằng props đã thay đổi, dẫn đến render lại.
  • useMemo: Giúp bạn “ghi nhớ” kết quả của một phép tính tốn kém. Nó sẽ chỉ tính toán lại giá trị khi một trong các dependencies của nó thay đổi.

Khi nào nên dùng?

  • useCallback: Khi bạn truyền callback function làm props cho các component con đã được tối ưu bằng React.memo, hoặc khi hàm đó là một dependency của useEffect hoặc một hook khác.
  • useMemo: Khi bạn có các phép tính phức tạp, tốn nhiều thời gian xử lý mà kết quả của nó không thay đổi thường xuyên.

Ví dụ useCallback:

function ParentComponent() {
  const [count, setCount] = useState(0);

  // Nếu không dùng useCallback, mỗi lần ParentComponent render, handleClick sẽ là một hàm mới
  const handleClick = useCallback(() => {
    console.log('Button clicked!');
    // Thực hiện một logic gì đó...
  }, []); // Dependency array rỗng nghĩa là hàm này chỉ được tạo một lần

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

const MemoizedChildComponent = React.memo(ChildComponent); // Giả sử ChildComponent được tối ưu bằng React.memo

function ChildComponent({ onClick }) {
  console.log('Rendering ChildComponent');
  return <button onClick={onClick}>Click Me</button>;
}

Ví dụ useMemo:

function DataProcessor({ data }) {
  const processedData = useMemo(() => {
    console.log('Processing data...');
    // Giả sử đây là một phép tính phức tạp
    return data.filter(item => item.value > 10).map(item => item.name.toUpperCase());
  }, [data]); // Chỉ tính toán lại khi mảng 'data' thay đổi

  return (
    <div>
      <h2>Processed Data:</h2>
      <ul>
        {processedData.map((item, index) => <li key={index}>{item}</li>)}
      </ul>
    </div>
  );
}

3. Chia Nhỏ Component Lớn Thành Các Component Nhỏ Hơn

Nguyên tắc “chia để trị” luôn đúng trong lập trình. Khi bạn có một component quá lớn và phức tạp, nó sẽ trở nên khó quản lý, khó debug và dễ gây ra các vấn đề về hiệu suất.

Lợi ích:

  • Dễ quản lý và tái sử dụng: Các component nhỏ có thể được tái sử dụng ở nhiều nơi khác nhau.
  • Tối ưu Render: React chỉ cần render lại những component con bị ảnh hưởng bởi sự thay đổi của state hoặc props, thay vì render lại toàn bộ component lớn.
  • Dễ dàng áp dụng React.memo: Các component nhỏ, độc lập với props rõ ràng sẽ dễ dàng được tối ưu bằng React.memo hơn.

Ví dụ: Thay vì có một component UserProfile khổng lồ chứa thông tin cá nhân, ảnh đại diện, danh sách bài viết, bạn nên tách nó thành các component nhỏ hơn như Avatar, UserInfo, PostList, và sau đó compose chúng lại trong UserProfile.

4. Sử Dụng Virtualization Cho Danh Sách Dài

Nếu ứng dụng của bạn hiển thị một danh sách với hàng trăm, hàng nghìn mục dữ liệu, việc render tất cả chúng cùng lúc sẽ khiến trình duyệt “nghẹt thở”. Virtualization (hay còn gọi là Windowing) là kỹ thuật chỉ render những gì người dùng có thể nhìn thấy trên màn hình.

Các thư viện phổ biến: react-window, react-virtualized.

Cách thức hoạt động: Các thư viện này sẽ theo dõi vị trí cuộn của người dùng. Khi người dùng cuộn, các mục dữ liệu không còn hiển thị sẽ bị gỡ bỏ khỏi DOM, và các mục mới xuất hiện sẽ được thêm vào. Điều này giúp giảm đáng kể số lượng DOM elements, từ đó cải thiện hiệu suất một cách ngoạn mục.

Ví dụ (sử dụng react-window):

import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>
    Row {index}
  </div>
);

const VirtualizedList = () => (
  <List
    height={300} // Chiều cao của khu vực hiển thị danh sách
    itemCount={1000} // Tổng số mục dữ liệu
    itemSize={35} // Chiều cao của mỗi mục
    width={300} // Chiều rộng của khu vực hiển thị danh sách
  >
    {Row}
  </List>
);

5. Lazy Loading Components và Route Splitting

Lazy loading giúp trì hoãn việc tải các component hoặc các phần của ứng dụng cho đến khi chúng thực sự cần thiết. Điều này giúp giảm kích thước ban đầu của bundle JavaScript, làm cho ứng dụng tải nhanh hơn.

Cách thực hiện:

  • Lazy loading components: Sử dụng React.lazy()Suspense.
  • Route splitting: Sử dụng React.lazy() kết hợp với router của bạn (ví dụ: react-router-dom).

Ví dụ với React.lazy()Suspense:

import React, { Suspense, lazy } from 'react';

const OtherComponent = lazy(() => import('./OtherComponent')); // Tải OtherComponent khi cần

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        {/* OtherComponent sẽ chỉ được tải khi phần này được render */}
        <OtherComponent />
      </Suspense>
    </div>
  );
}

Khi kết hợp với react-router-dom, bạn có thể cấu hình để mỗi route chỉ tải code cần thiết cho route đó, giúp ứng dụng khởi động nhanh hơn rất nhiều, đặc biệt là các ứng dụng có nhiều trang.

6. Tối Ưu Hình Ảnh

Hình ảnh thường chiếm phần lớn dung lượng của một trang web và ảnh hưởng trực tiếp đến tốc độ tải.

Các mẹo tối ưu hình ảnh:

  • Chọn định dạng phù hợp: Sử dụng WebP cho chất lượng tốt với kích thước tệp nhỏ hơn. PNG cho ảnh có nền trong suốt, JPG cho ảnh chụp.
  • Nén hình ảnh: Sử dụng các công cụ nén ảnh trực tuyến hoặc tích hợp vào quy trình build (ví dụ: imagemin).
  • Responsive Images: Sử dụng thẻ <img> với thuộc tính srcsetsizes hoặc thẻ <picture> để trình duyệt tự động chọn kích thước ảnh phù hợp với màn hình thiết bị.
  • Lazy Loading Images: Sử dụng thuộc tính loading="lazy" trên thẻ <img> (được hỗ trợ bởi hầu hết các trình duyệt hiện đại) hoặc implement manual lazy loading.

7. Sử Dụng Production Build

Đây là một lời khuyên “hiển nhiên” nhưng đôi khi bị bỏ qua. Khi deploy ứng dụng, hãy luôn sử dụng bản build dành cho production.

  • Development build: Được tối ưu cho việc debug, chứa nhiều cảnh báo, và không được tối ưu về hiệu suất.
  • Production build: Được tối ưu hóa cho hiệu suất, loại bỏ các console logs, nén code, và áp dụng các kỹ thuật tối ưu khác.

Cách đơn giản nhất để tạo production build với Create React App là chạy lệnh: npm run build hoặc yarn build.

8. Tránh Sử Dụng index Làm key Khi Có Thể

Khi bạn render một danh sách các phần tử, React sử dụng thuộc tính key để xác định xem phần tử nào đã thay đổi, thêm mới, hoặc bị xóa. Việc sử dụng index làm key có thể gây ra vấn đề hiệu suất, đặc biệt khi danh sách có thể thay đổi thứ tự hoặc có các phần tử được thêm/xóa ở giữa.

Tại sao lại có vấn đề? Nếu bạn thêm một mục vào đầu danh sách, tất cả các key phía sau nó sẽ bị “dịch chuyển” theo chỉ số. Điều này buộc React phải render lại nhiều DOM elements hơn mức cần thiết.

Giải pháp: Luôn cố gắng sử dụng một ID duy nhất, ổn định từ dữ liệu của bạn làm key. Ví dụ: key={item.id}.

Ví dụ:

// KHÔNG NÊN LÀM VỚI DANH SÁCH CÓ THỂ THAY ĐỔI
{items.map((item, index) => (
  <ListItem key={index} data={item} />
))}

// NÊN LÀM
{items.map(item => (
  <ListItem key={item.id} data={item} />
))}

9. Quản Lý State Hiệu Quả

Cách bạn quản lý state có thể ảnh hưởng lớn đến hiệu suất.

  • Chọn đúng công cụ: Với các ứng dụng nhỏ, useStateuseReducer là đủ. Với ứng dụng lớn hơn, hãy cân nhắc các thư viện quản lý state như Redux, Zustand, Jotai, Recoil.
  • Tránh cập nhật state không cần thiết: Chỉ cập nhật state khi thực sự có sự thay đổi dữ liệu cần thiết để render lại.
  • Lift State Up hợp lý: Đưa state lên component cha chung gần nhất có thể để tránh truyền props qua nhiều cấp. Tuy nhiên, đừng đưa state lên quá cao một cách không cần thiết, vì nó có thể khiến component cha render lại quá nhiều.
  • Sử dụng Context API cẩn thận: Context API rất tiện lợi để chia sẻ state toàn cục, nhưng nếu bạn không cẩn thận, việc cập nhật Context có thể kích hoạt render lại cho tất cả các component đang sử dụng nó. Hãy cân nhắc việc chia nhỏ Context hoặc sử dụng memoization cho các giá trị Context.

10. Sử Dụng React Developer Tools Để Profiling

Cuối cùng nhưng không kém phần quan trọng, hãy sử dụng công cụ mạnh mẽ nhất để hiểu rõ ứng dụng của bạn đang hoạt động như thế nào: React Developer Tools.

Công cụ này cho phép bạn:

  • Inspect Component Tree: Xem cấu trúc component của bạn.
  • Profile Render Performance: Xác định những component nào đang render lại quá nhiều, mất bao lâu để render, và lý do tại sao.
  • Highlight Updates: Xem trực quan những component nào đang được cập nhật.

Bằng cách sử dụng profiler, bạn có thể xác định chính xác các điểm “nghẽn cổ chai” trong ứng dụng của mình và áp dụng các mẹo trên một cách hiệu quả hơn.

Lời Kết

Tối ưu hóa hiệu suất là một hành trình liên tục, không phải là một đích đến. Bằng việc áp dụng những mẹo trên, bạn đã có trong tay một bộ công cụ mạnh mẽ để xây dựng các ứng dụng React không chỉ đẹp mắt mà còn cực kỳ nhanh nhạy và mang lại trải nghiệm tuyệt vời cho người dùng.

Hãy nhớ rằng, không có một giải pháp “one-size-fits-all”. Việc áp dụng mẹo nào sẽ phụ thuộc vào đặc thù của từng dự án. Điều quan trọng là phải hiểu rõ vấn đề, sử dụng công cụ phù hợp, và kiên trì tối ưu.

Chúc bạn thành công trong việc nâng cao hiệu suất ứng dụng React của mình! Nếu bạn có bất kỳ mẹo nào khác hoặc có câu hỏi, đừng ngần ngại để lại bình luận bên dưới nhé! Hẹn gặp lại trong những bài viết tiếp theo!

Gọi ngay Nhắn Messenger Nhắn Zalo