Bài 23.3: Profiling React-TypeScript apps

Chào mừng bạn trở lại với chuỗi bài viết về Lập trình Web Front-end! Hôm nay, chúng ta sẽ cùng nhau đi sâu vào một khía cạnh cực kỳ quan trọng đối với bất kỳ ứng dụng web nào: hiệu suất. Một ứng dụng chậm chạp có thể khiến người dùng nản lòng, ảnh hưởng tiêu cực đến trải nghiệm và thậm chí là tỷ lệ chuyển đổi. Với các ứng dụng React-TypeScript phức tạp, việc tìm ra gốc rễ của các vấn đề hiệu suất đôi khi giống như mò kim đáy bể. Đó là lý do tại sao chúng ta cần đến Profiling.

Profiling là quá trình phân tích cách ứng dụng của bạn hoạt động trong thời gian chạy để xác định các điểm nghẽn (bottlenecks) về hiệu suất, chẳng hạn như các phần mã chạy chậm, tiêu tốn nhiều tài nguyên CPU hoặc gây ra các lần render không cần thiết. Trong thế giới React, profiling chủ yếu tập trung vào việc hiểu các lần render của component: khi nào chúng render, tại sao chúng render, và mất bao lâu để render.

Tại sao profiling lại đặc biệt quan trọng với React-TypeScript?

  • Tính phức tạp: Các ứng dụng React hiện đại thường có cấu trúc component cây phức tạp, việc theo dõi luồng dữ liệu và render có thể trở nên khó khăn bằng mắt thường.
  • Re-renders: React hoạt động dựa trên cơ chế Virtual DOM và diffing để tối ưu hóa việc cập nhật giao diện. Tuy nhiên, việc re-render các component không cần thiết là nguyên nhân phổ biến nhất gây ra vấn đề hiệu suất. Profiling giúp bạn nhìn thấy những lần re-render "thừa" này.
  • TypeScript: Mặc dù TypeScript giúp tăng cường tính an toàn và khả năng bảo trì của mã nguồn, bản thân nó không giải quyết các vấn đề hiệu suất ở runtime. Việc sử dụng TypeScript kết hợp với React vẫn cần profiling để đảm bảo ứng dụng chạy mượt mà.

Hãy cùng biến chiếc "kính lúp hiệu suất" của chúng ta sắc bén hơn bằng cách tìm hiểu các công cụ và kỹ thuật profiling hiệu quả cho ứng dụng React-TypeScript.

Các Công Cụ Profiling Chính

Có hai công cụ chính mà chúng ta sẽ sử dụng:

  1. React Developer Tools (Tab Profiler): Đây là công cụ chuyên dụng do chính đội ngũ React phát triển. Nó tích hợp sâu vào cơ chế render của React và cung cấp thông tin chi tiết về các lần render của component.
  2. Browser Developer Tools (Tab Performance): Công cụ này có sẵn trong hầu hết các trình duyệt hiện đại (Chrome, Firefox, Edge). Nó cung cấp cái nhìn tổng quan về hoạt động của trình duyệt, bao gồm thực thi JavaScript, tính toán style, layout, painting, và hoạt động mạng. Mặc dù không chuyên sâu vào React như React DevTools, nó rất hữu ích để xác định xem vấn đề hiệu suất có phải do mã JavaScript (bao gồm cả React) hay do các giai đoạn khác của trình duyệt.

Trong bài viết này, chúng ta sẽ tập trung nhiều hơn vào React Developer Tools vì nó cung cấp thông tin cụ thể về React components.

Sử Dụng React Developer Tools Profiler

Đầu tiên, bạn cần cài đặt React Developer Tools extension cho trình duyệt của mình (có sẵn cho Chrome, Firefox, Edge). Sau khi cài đặt, mở Developer Tools (F12), bạn sẽ thấy tab "Components" và "Profiler".

Tab "Profiler" cho phép bạn ghi lại quá trình render của ứng dụng trong một khoảng thời gian nhất định. Đây là các bước cơ bản:

  1. Mở Developer Tools và chuyển sang tab "Profiler".
  2. Nhấp vào nút ghi (biểu tượng chấm tròn) ở góc trên bên trái của tab Profiler.
  3. Thực hiện các thao tác trong ứng dụng mà bạn muốn phân tích (ví dụ: cuộn trang, nhấp nút, nhập dữ liệu).
  4. Nhấp lại vào nút ghi để dừng quá trình.
  5. Profiler sẽ hiển thị dữ liệu đã ghi.

Giao diện của Profiler có nhiều chế độ xem khác nhau, mỗi chế độ cung cấp một góc nhìn riêng về hiệu suất render:

  • Flamegraph chart: Biểu đồ dạng ngọn lửa hiển thị cây component theo thời gian render. Các thanh ngang thể hiện từng lần render của một component. Độ rộng của thanh thể hiện thời gian render tương đối. Biểu đồ này giúp bạn thấy các component nào render và thứ tự render của chúng. Các component có màu vàng/đỏ nhạt thường là những nơi bạn nên xem xét kỹ hơn.
  • Ranked chart: Hiển thị danh sách các component đã render trong khoảng thời gian ghi, được sắp xếp theo thời gian render từ cao đến thấp. Đây là cách nhanh nhất để xác định các component "đắt đỏ" nhất.
  • Component chart: Khi chọn một component cụ thể, biểu đồ này hiển thị lịch sử render của component đó theo thời gian. Bạn có thể thấy component đó đã re-render bao nhiêu lần và mỗi lần mất bao lâu. Đây là công cụ tuyệt vời để phát hiện các lần re-render không cần thiết.
  • Interactions: Nếu bạn sử dụng React's unstable_trace API để theo dõi các "interaction" (ví dụ: nhấp chuột, nhập liệu), tab này sẽ hiển thị thời gian render liên quan đến từng interaction.

Tìm Điểm Nghẽn Hiệu Suất Phổ Biến Với Profiler

Bây giờ, hãy xem cách sử dụng Profiler để phát hiện các vấn đề phổ biến:

1. Phát hiện Re-renders Không Cần Thiết

Đây là nguyên nhân số 1 gây chậm ứng dụng React. Một component re-render khi state hoặc props của nó thay đổi. Tuy nhiên, đôi khi chúng re-render ngay cả khi những thay đổi đó không ảnh hưởng đến giao diện hiển thị hoặc logic của component con.

Cách phát hiện với Profiler:

  • Ghi lại một thao tác (ví dụ: cập nhật state ở component cha) mà bạn mong đợi chỉ làm re-render một số component nhất định.
  • Kiểm tra Flamegraph chart hoặc Ranked chart. Tìm các component con xuất hiện trong biểu đồ render mặc dù props hoặc state của chúng không thực sự thay đổi theo cách có ý nghĩa.
  • Chọn các component đáng ngờ và xem Component chart của chúng. Nếu biểu đồ hiển thị nhiều lần render không kèm theo thay đổi dữ liệu đáng kể, đó là dấu hiệu của re-render không cần thiết. Profiler thậm chí còn có một tùy chọn để highlight các component re-render tại sao.

Ví dụ (mã minh họa):

Giả sử bạn có một component Parent quản lý state và truyền một function handler xuống component con Child.

// ParentComponent.tsx
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';

const ParentComponent: React.FC = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // Hàm handler được tạo mới mỗi lần ParentComponent re-render
  const handleClick = () => {
    console.log('Button clicked');
    // Logic nào đó không làm thay đổi props của ChildComponent
  };

  // Text input thay đổi state, khiến ParentComponent re-render
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setText(event.target.value);
  };

  console.log('ParentComponent rendering');

  return (
    <div>
      <input type="text" value={text} onChange={handleInputChange} />
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
};

export default ParentComponent;

// ChildComponent.tsx
import React from 'react';

interface ChildProps {
  onClick: () => void;
}

const ChildComponent: React.FC<ChildProps> = ({ onClick }) => {
  console.log('ChildComponent rendering'); // Log để dễ theo dõi

  return (
    <div>
      <p>This is a child component.</p>
      <button onClick={onClick}>Click me</button>
    </div>
  );
};

export default ChildComponent;

Trong ví dụ trên, mỗi khi bạn gõ vào input (handleInputChange làm thay đổi state text), ParentComponent sẽ re-render. Vì handleClick được tạo mới mỗi lần ParentComponent re-render, prop onClick truyền xuống ChildComponent là một function mới. Mặc dù nội dung của function không đổi, React vẫn xem đó là một prop mới và mặc định sẽ làm ChildComponent re-render theo.

Sử dụng Profiler: Khi bạn gõ vào input, ghi lại quá trình. Bạn sẽ thấy cả ParentComponentChildComponent đều xuất hiện trong bản ghi render, ngay cả khi việc gõ chỉ liên quan đến ParentComponent và không làm thay đổi logic của ChildComponent. ChildComponent re-render là không cần thiết trong trường hợp này.

Cách tối ưu (sử dụng React.memouseCallback):

Để khắc phục, chúng ta có thể dùng React.memo cho component con và useCallback cho function prop.

// ChildComponent.tsx (với React.memo)
import React from 'react';

interface ChildProps {
  onClick: () => void;
}

const ChildComponent: React.FC<ChildProps> = ({ onClick }) => {
  console.log('ChildComponent rendering'); // Log để dễ theo dõi

  return (
    <div>
      <p>This is a child component.</p>
      <button onClick={onClick}>Click me</button>
    </div>
  );
};

// Sử dụng React.memo để memoize ChildComponent
// Nó sẽ chỉ re-render nếu props của nó thay đổi
export default React.memo(ChildComponent);

// ParentComponent.tsx (với useCallback)
import React, { useState, useCallback } from 'react';
import ChildComponent from './ChildComponent';

const ParentComponent: React.FC = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // Sử dụng useCallback để memoize hàm handleClick
  // Hàm này sẽ chỉ được tạo lại khi dependency array ([]) thay đổi
  // Trong trường hợp này, nó chỉ tạo 1 lần khi component mount
  const handleClick = useCallback(() => {
    console.log('Button clicked (memoized)');
    // Logic nào đó không làm thay đổi props của ChildComponent
  }, []); // Dependency array rỗng nghĩa là hàm này sẽ không bao giờ thay đổi

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setText(event.target.value);
  };

  console.log('ParentComponent rendering');

  return (
    <div>
      <input type="text" value={text} onChange={handleInputChange} />
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
};

export default ParentComponent;

Sử dụng Profiler sau tối ưu: Ghi lại quá trình khi gõ vào input lần nữa. Lần này, bạn sẽ thấy ParentComponent vẫn re-render (vì state text thay đổi), nhưng ChildComponent sẽ không xuất hiện trong bản ghi render (hoặc xuất hiện với thời gian rất ngắn và được đánh dấu là "skipped") vì prop onClick truyền xuống nó (được tạo bằng useCallback) không thay đổi, và ChildComponent đã được memoize bằng React.memo. Profiler xác nhận việc tối ưu đã thành công.

2. Xác định Các Tính Toán "Đắt Đỏ" Trong Render

Đôi khi, vấn đề không phải là số lượng lần render, mà là thời gian mỗi lần render mất. Các tính toán phức tạp, vòng lặp lớn, hoặc xử lý dữ liệu nặng nề ngay trong hàm render component có thể làm chậm ứng dụng.

Cách phát hiện với Profiler:

  • Ghi lại các thao tác mà bạn cảm thấy chậm.
  • Xem Ranked chart. Tìm các component ở top đầu danh sách với thời gian render cao bất thường.
  • Xem Flamegraph chart. Tìm các thanh ngang rộng, màu đậm (đỏ, vàng đậm) thể hiện các component mất nhiều thời gian render.

Ví dụ (mã minh họa):

Một component thực hiện tính toán phức tạp mỗi lần render.

// ExpensiveCalculationComponent.tsx
import React, { useState, useMemo } from 'react';

interface Props {
  data: number[];
}

const ExpensiveCalculationComponent: React.FC<Props> = ({ data }) => {
  console.log('ExpensiveCalculationComponent rendering');

  // Giả sử có một tính toán tốn kém dựa trên props.data
  // Thực hiện trực tiếp trong thân hàm component
  const calculateSumOfSquares = (arr: number[]) => {
    console.log('Calculating sum of squares...'); // Log để theo dõi
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
      // Giả lập tính toán phức tạp
      for (let j = 0; j < 1000; j++) {
        sum += arr[i] * arr[i] / (j + 1); // Một phép tính giả định tốn thời gian
      }
    }
    return sum;
  };

  const result = calculateSumOfSquares(data); // Tính toán mỗi lần render

  return (
    <div>
      <p>Result of expensive calculation: {result.toFixed(2)}</p>
    </div>
  );
};

export default ExpensiveCalculationComponent;

// Parent component sử dụng ExpensiveCalculationComponent
// ParentComponentWithExpensiveCalc.tsx
import React, { useState } from 'react';
import ExpensiveCalculationComponent from './ExpensiveCalculationComponent';

const ParentComponentWithExpensiveCalc: React.FC = () => {
  const [counter, setCounter] = useState(0);
  const data = [1, 2, 3, 4, 5]; // Dữ liệu không thay đổi

  console.log('ParentComponentWithExpensiveCalc rendering');

  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>Increment Counter ({counter})</button>
      {/* Child component render mỗi khi counter thay đổi,
          gây ra tính toán lại, ngay cả khi data không đổi */}
      <ExpensiveCalculationComponent data={data} />
    </div>
  );
};

export default ParentComponentWithExpensiveCalc;

Trong ví dụ này, mỗi khi bạn nhấp vào nút "Increment Counter", ParentComponentWithExpensiveCalc re-render, kéo theo ExpensiveCalculationComponent re-render. Hàm calculateSumOfSquares chạy lại, dù cho data prop không hề thay đổi. Với mảng data nhỏ và vòng lặp đơn giản, điều này có thể không quá tệ, nhưng với dữ liệu lớn hoặc tính toán phức tạp hơn, nó sẽ gây ra độ trễ đáng kể.

Sử dụng Profiler: Ghi lại quá trình nhấp nút "Increment Counter". Trong Ranked chart, bạn sẽ thấy ExpensiveCalculationComponent xuất hiện ở trên cùng với thời gian render đáng kể. Trong Flamegraph, thanh của ExpensiveCalculationComponent sẽ rộng và có màu thể hiện thời gian render lâu. Console log "Calculating sum of squares..." cũng xuất hiện mỗi lần.

Cách tối ưu (sử dụng useMemo):

Sử dụng useMemo để memoize kết quả của tính toán, chỉ tính toán lại khi các dependencies (ở đây là data) thay đổi.

// ExpensiveCalculationComponent.tsx (với useMemo)
import React, { useMemo } from 'react';

interface Props {
  data: number[];
}

const ExpensiveCalculationComponent: React.FC<Props> = ({ data }) => {
  console.log('ExpensiveCalculationComponent rendering');

  // Sử dụng useMemo để memoize kết quả tính toán
  // Hàm tính toán chỉ chạy lại khi dependencies ([data]) thay đổi
  const result = useMemo(() => {
    console.log('Calculating sum of squares (memoized)...'); // Log để theo dõi
    let sum = 0;
    for (let i = 0; i < data.length; i++) {
      // Giả lập tính toán phức tạp
      for (let j = 0; j < 1000; j++) {
        sum += data[i] * data[i] / (j + 1); // Một phép tính giả định tốn thời gian
      }
    }
    return sum;
  }, [data]); // Dependencies array chứa data

  return (
    <div>
      <p>Result of expensive calculation: {result.toFixed(2)}</p>
    </div>
  );
};

export default React.memo(ExpensiveCalculationComponent); // Có thể kết hợp với React.memo nếu props khác cũng được truyền vào

Sử dụng Profiler sau tối ưu: Ghi lại quá trình nhấp nút "Increment Counter". ParentComponentWithExpensiveCalc vẫn re-render, nhưng ExpensiveCalculationComponent sẽ re-render rất nhanh (hoặc bị bỏ qua nếu bạn cũng thêm React.memo và không có prop nào khác thay đổi). Quan trọng hơn, console log "Calculating sum of squares (memoized)..." sẽ chỉ xuất hiện lần đầu tiên component render, chứ không xuất hiện mỗi khi counter tăng. Ranked chart sẽ cho thấy ExpensiveCalculationComponent tốn ít thời gian render hơn đáng kể.

3. Hiểu Tác Động Của Context

React Context là một công cụ mạnh mẽ để chia sẻ dữ liệu giữa các component mà không cần truyền prop qua nhiều tầng. Tuy nhiên, việc cập nhật giá trị context có thể gây ra re-render cho tất cả các component con (và con của con, v.v.) sử dụng context đó, ngay cả khi chúng chỉ cần một phần nhỏ của giá trị context và phần đó không thay đổi.

Cách phát hiện với Profiler:

  • Ghi lại quá trình cập nhật giá trị context.
  • Xem Flamegraph chart. Bạn sẽ thấy một lượng lớn các component con của Provider (hoặc các component dùng useContext) re-render cùng lúc, ngay cả khi bạn chỉ thay đổi một phần nhỏ của context value.

Ví dụ (mã minh họa):

Context chứa nhiều giá trị, và một bản cập nhật nhỏ làm re-render nhiều component.

// MyContext.tsx
import React, { createContext, useState, useContext, ReactNode } from 'react';

interface MyContextValue {
  theme: string;
  user: { name: string; id: number } | null;
  counter: number;
  setTheme: (theme: string) => void;
  setCounter: (count: number) => void;
}

const MyContext = createContext<MyContextValue | undefined>(undefined);

interface MyContextProviderProps {
  children: ReactNode;
}

export const MyContextProvider: React.FC<MyContextProviderProps> = ({ children }) => {
  const [theme, setTheme] = useState('light');
  const [counter, setCounter] = useState(0);
  const user = { name: 'John Doe', id: 123 }; // Giả lập dữ liệu user

  // Giá trị context được tạo mới mỗi khi Provider re-render (theme hoặc counter thay đổi)
  const contextValue: MyContextValue = {
    theme,
    user,
    counter,
    setTheme,
    setCounter,
  };

  console.log('MyContextProvider rendering');

  return (
    <MyContext.Provider value={contextValue}>
      {children}
    </MyContext.Provider>
  );
};

// Custom hook để sử dụng context
export const useMyContext = () => {
  const context = useContext(MyContext);
  if (context === undefined) {
    throw new Error('useMyContext must be used within a MyContextProvider');
  }
  return context;
};

// Component con chỉ dùng theme
// ThemeDisplay.tsx
import React from 'react';
import { useMyContext } from './MyContext';

const ThemeDisplay: React.FC = () => {
  const { theme } = useMyContext();
  console.log('ThemeDisplay rendering');
  return <p>Current Theme: {theme}</p>;
};

export default ThemeDisplay;

// Component con chỉ dùng counter
// CounterDisplay.tsx
import React from 'react';
import { useMyContext } from './MyContext';

const CounterDisplay: React.FC = () => {
  const { counter } = useMyContext();
  console.log('CounterDisplay rendering');
  return <p>Current Counter: {counter}</p>;
};

export default CounterDisplay;

// App component sử dụng Provider và các component con
// App.tsx
import React from 'react';
import { MyContextProvider } from './MyContext';
import ThemeDisplay from './ThemeDisplay';
import CounterDisplay from './CounterDisplay';

const App: React.FC = () => {
  const { setTheme, setCounter } = useMyContext(); // Lưu ý: Hook này cần được gọi trong component nằm trong Provider

  return (
    <MyContextProvider>
      {/* Các component sử dụng context */}
      <ThemeDisplay />
      <CounterDisplay />
      {/* Các nút để thay đổi context */}
      <button onClick={() => setTheme('dark')}>Set Dark Theme</button>
      <button onClick={() => setCounter(c => c + 1)}>Increment Counter</button>
    </MyContextProvider>
  );
};

// AppWithButtons.tsx (Component cha chứa App và gọi hook)
import React from 'react';
import { MyContextProvider, useMyContext } from './MyContext';
import ThemeDisplay from './ThemeDisplay';
import CounterDisplay from './CounterDisplay';

const AppWithButtons: React.FC = () => {
  // Hook useMyContext được gọi ở đây, *trong* Provider
  return (
    <MyContextProvider>
      <ThemeDisplay />
      <CounterDisplay />
      <ContextControls /> {/* Component riêng chứa các nút */}
    </MyContextProvider>
  );
}

const ContextControls: React.FC = () => {
  const { setTheme, setCounter } = useMyContext();

  return (
    <div>
      <button onClick={() => setTheme('dark')}>Set Dark Theme</button>
      <button onClick={() => setCounter(c => c + 1)}>Increment Counter</button>
    </div>
  );
}

export default AppWithButtons;

(Lưu ý: Ví dụ ContextControls/AppWithButtons.tsx là cách gọi useMyContext đúng vị trí)

Trong ví dụ trên, khi bạn nhấp nút "Increment Counter" (làm thay đổi state counter trong MyContextProvider), cả ThemeDisplay (chỉ cần theme) và CounterDisplay (chỉ cần counter) đều re-render. Điều này xảy ra vì object contextValue được tạo mới mỗi lần MyContextProvider re-render, khiến React nghĩ rằng toàn bộ giá trị context đã thay đổi, do đó tất cả consumers đều phải cập nhật.

Sử dụng Profiler: Ghi lại quá trình nhấp nút "Increment Counter". Bạn sẽ thấy cả ThemeDisplayCounterDisplay xuất hiện trong bản ghi render của cùng một "commit" (lần cập nhật DOM của React), dù chỉ có CounterDisplay thực sự cần re-render.

Cách tối ưu:

  • Tách Context: Thay vì một Context lớn, tạo nhiều Context nhỏ cho các phần dữ liệu ít liên quan. Ví dụ: ThemeContext, UserContext, CounterContext.
  • Sử dụng Selector Hook: Viết custom hook để chỉ trích xuất (và subscribe) vào phần dữ liệu cần thiết từ context lớn, kết hợp với useMemo hoặc các thư viện như use-context-selector.

Ví dụ tách Context:

// ThemeContext.tsx
import React, { createContext, useState, useContext, ReactNode } from 'react';

interface ThemeContextValue {
  theme: string;
  setTheme: (theme: string) => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

export const ThemeContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState('light');
  const contextValue = { theme, setTheme };
  console.log('ThemeContextProvider rendering');
  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useThemeContext = () => {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useThemeContext must be used within a ThemeContextProvider');
  }
  return context;
};

// CounterContext.tsx
import React, { createContext, useState, useContext, ReactNode } from 'react';

interface CounterContextValue {
  counter: number;
  setCounter: (count: number) => void;
}

const CounterContext = createContext<CounterContextValue | undefined>(undefined);

export const CounterContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [counter, setCounter] = useState(0);
  const contextValue = { counter, setCounter };
  console.log('CounterContextProvider rendering');
  return (
    <CounterContext.Provider value={contextValue}>
      {children}
    </CounterContext.Provider>
  );
};

export const useCounterContext = () => {
  const context = useContext(CounterContext);
  if (context === undefined) {
    throw new Error('useCounterContext must be used within a CounterContextProvider');
  }
  return context;
};

// App component sử dụng các Providers đã tách
// AppSeparatedContext.tsx
import React from 'react';
import { ThemeContextProvider, useThemeContext } from './ThemeContext'; // Import riêng
import { CounterContextProvider, useCounterContext } from './CounterContext'; // Import riêng
import ThemeDisplay from './ThemeDisplaySeparated'; // Component con dùng Context mới
import CounterDisplay from './CounterDisplaySeparated'; // Component con dùng Context mới

// ThemeDisplaySeparated.tsx
import React from 'react';
import { useThemeContext } from './ThemeContext';

const ThemeDisplaySeparated: React.FC = React.memo(() => { // Có thể memoize để đảm bảo
  const { theme } = useThemeContext();
  console.log('ThemeDisplaySeparated rendering');
  return <p>Current Theme: {theme}</p>;
});

export default ThemeDisplaySeparated;

// CounterDisplaySeparated.tsx
import React from 'react';
import { useCounterContext } from './CounterContext';

const CounterDisplaySeparated: React.FC = React.memo(() => { // Có thể memoize để đảm bảo
  const { counter } = useCounterContext();
  console.log('CounterDisplaySeparated rendering');
  return <p>Current Counter: {counter}</p>;
});

export default CounterDisplaySeparated;


const AppSeparatedContext: React.FC = () => {
  // Các nút điều khiển state
  return (
    <ThemeContextProvider>
      <CounterContextProvider>
        <ThemeDisplaySeparated />
        <CounterDisplaySeparated />
        <ContextControlsSeparated /> {/* Component riêng chứa các nút */}
      </CounterContextProvider>
    </ThemeContextProvider>
  );
};

const ContextControlsSeparated: React.FC = () => {
  const { setTheme } = useThemeContext();
  const { setCounter } = useCounterContext();

  return (
    <div>
      <button onClick={() => setTheme('dark')}>Set Dark Theme</button>
      <button onClick={() => setCounter(c => c + 1)}>Increment Counter</button>
    </div>
  );
}


export default AppSeparatedContext;

Sử dụng Profiler sau tối ưu: Ghi lại quá trình nhấp nút "Increment Counter". Lần này, bạn sẽ thấy chỉ có CounterContextProviderCounterDisplaySeparated re-render. ThemeContextProviderThemeDisplaySeparated sẽ không re-render vì state theme trong ThemeContext không thay đổi. Profiler xác nhận rằng việc tách context đã giới hạn phạm vi re-render.

Workflow Profiling Hiệu Quả

Để profiling hiệu quả, hãy tuân theo một workflow đơn giản:

  1. Xác định Tình huống: Bạn muốn profile cái gì? Thao tác nào của người dùng bạn cảm thấy chậm? (Ví dụ: tải trang ban đầu, cuộn danh sách dài, nhấp một nút phức tạp).
  2. Ghi Lại: Mở React DevTools Profiler (hoặc Browser Performance tab), nhấp ghi, thực hiện thao tác, và dừng ghi.
  3. Phân Tích Dữ Liệu:
    • Nhìn vào Ranked chart để xem component nào mất nhiều thời gian render nhất.
    • Nhìn vào Flamegraph để hiểu luồng render và xem component nào re-render.
    • Sử dụng tính năng "Highlight updates" trong React DevTools (tab Components settings) để xem trực quan component nào nhấp nháy khi re-render.
    • Sử dụng Component chart cho các component đáng ngờ để xem lịch sử re-render của chúng.
  4. Xác Định Gốc Rễ: Tại sao component đó lại chậm hoặc re-render không cần thiết? Có phải do prop thay đổi không đáng kể? Do tính toán nặng? Do cập nhật context?
  5. Áp Dụng Tối Ưu: Dựa trên nguyên nhân gốc rễ, áp dụng các kỹ thuật tối ưu phù hợp (ví dụ: React.memo, useCallback, useMemo, tách context, lazy loading, virtualization).
  6. Profile Lại: Quan trọng! Sau khi áp dụng tối ưu, hãy profile lại tình huống tương tự để xác nhận rằng vấn đề đã được giải quyết và việc tối ưu của bạn có hiệu quả. Đôi khi, việc tối ưu sai cách có thể không mang lại lợi ích hoặc thậm chí gây hại.

Một vài lưu ý nhỏ

  • Chỉ Profile trong Production Build (gần giống): React DevTools hoạt động tốt nhất với bản build phát triển (development build) vì nó chứa nhiều thông tin debug. Tuy nhiên, hiệu suất thực tế của ứng dụng sẽ gần với bản build sản phẩm (production build). Bạn nên profile cả hai môi trường nếu có thể. React DevTools có chế độ "profiling production builds" giúp bạn có cái nhìn chính xác hơn về hiệu suất runtime mà không cần code debug overhead của dev build.
  • Cẩn thận với console.log: Việc dùng quá nhiều console.log trong render function có thể ảnh hưởng đến hiệu suất và làm sai lệch kết quả profiling. Sử dụng chúng có chừng mực khi cần thiết để theo dõi render.
  • Không Tối Ưu Hóa Quá Sớm: Đừng cố gắng tối ưu hóa mọi thứ ngay từ đầu. Tập trung vào các khu vực mà profiling chỉ ra là có vấn đề thực sự ảnh hưởng đến trải nghiệm người dùng. Việc tối ưu hóa không cần thiết có thể làm mã phức tạp hơn.

Profiling là một kỹ năng cần thiết đối với bất kỳ nhà phát triển React chuyên nghiệp nào. Nó cung cấp cái nhìn sâu sắc vào hoạt động bên trong của ứng dụng, giúp bạn không chỉ tìm ra vấn đề hiệu suất mà còn hiểu rõ hơn về cách React render và quản lý component. Hãy dành thời gian làm quen và sử dụng React DevTools Profiler thường xuyên trong quá trình phát triển của bạn.

Chúc bạn thành công trong việc xây dựng các ứng dụng React-TypeScript nhanh và mượt mà!

Comments

There are no comments at the moment.