Peeps Avatar

Hello, codestus.

Go back

Nguyên nhân và cách phòng tránh Memory Leaks trong React

Published at: 24/01/2025

7 mins read

Memory leak (rò rỉ bộ nhớ) trong React xảy ra khi component không dọn dẹp tài nguyên sau khi unmount, dẫn đến việc bộ nhớ bị chiếm dụng vĩnh viễn. Dù React có cơ chế quản lý vòng đời component, việc không tuân thủ nguyên tắc cleanup sẽ gây ra rò rỉ. Bài viết này tập trung vào các nguyên nhân phổ biến trong React và cách khắc phục chi tiết qua ví dụ thực tế.


1. Nguyên nhân

a. Subscriptions/Event Listeners không được cleanup trong useEffect

Ví dụ: Component đăng ký một WebSocket hoặc event từ thư viện bên ngoài nhưng quên hủy đăng ký khi unmount.

function RealTimeFeed() {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    // Đăng ký WebSocket
    const socket = new WebSocket("wss://api.chat.com")
    socket.onmessage = (event) => {
      setMessages((prev) => [...prev, event.data])
    }

    // ❌ Quên đóng kết nối khi component unmount
  }, [])

  return <div>{/* Render messages */}</div>
}

Phân tích:

  • Khi component unmount, WebSocket vẫn mở và tiếp tục nhận dữ liệu → setMessages được gọi trên component đã unmount → Memory leak.
  • Hậu quả: Ứng dụng ngốn RAM, có thể crash sau thời gian dài.

Cách Sửa: Thêm cleanup function để đóng kết nối:

useEffect(() => {
  const socket = new WebSocket("wss://api.chat.com")
  socket.onmessage = (event) => {
    /* ... */
  }

  return () => {
    socket.close() // ✅ Đóng kết nối khi unmount
  }
}, [])

b. Async Operations không được hủy đúng cách

Ví dụ: Fetch dữ liệu từ API nhưng không hủy request nếu component unmount trước khi hoàn thành.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((response) => response.json())
      .then((data) => setUser(data)) // ❌ Cập nhật state sau khi unmount
  }, [userId])

  return <div>{/* Render user */}</div>
}

Phân Tích:

  • Nếu người dùng rời khỏi trang trước khi fetch hoàn thành → setUser cập nhật state của component đã unmount → Memory leak.
  • Hậu quả: Cảnh báo "Can't perform a React state update on an unmounted component" và rò rỉ bộ nhớ.

Cách Sửa: Sử dụng AbortController để hủy request:

useEffect(() => {
  const abortController = new AbortController()

  fetch(`/api/users/${userId}`, {
    signal: abortController.signal,
  })
    .then(/* ... */)
    .catch((error) => {
      if (error.name !== "AbortError") console.error(error)
    })

  return () => {
    abortController.abort() // ✅ Hủy request khi unmount
  }
}, [userId])

c. Giữ tham chiếu đến dữ liệu lớn trong state/ref

Ví dụ: Lưu trữ dữ liệu lớn (như hình ảnh, file) trong state mà không giải phóng khi không cần.

function ImageViewer() {
  const [imageData, setImageData] = useState(null)

  const loadImage = async () => {
    const data = await fetchLargeImage() // Dữ liệu ảnh lớn (10MB+)
    setImageData(data)
  }

  // ❌ imageData vẫn tồn tại ngay cả khi component unmount
  return <div>{/* Render ảnh */}</div>
}

Phân Tích:

  • Khi component unmount, imageData vẫn được giữ trong bộ nhớ → Không được garbage collected → Memory leak.

Cách Sửa: Reset state khi unmount:

useEffect(() => {
  return () => {
    setImageData(null) // ✅ Giải phóng dữ liệu khi unmount
  }
}, [])

2. Các công cụ phát hiện Memory Leak trong React

a. React Developer Tools

  • Component Tab: Kiểm tra xem component có bị mount nhiều lần không mong muốn không.
  • Profiler: Ghi lại quá trình render để phát hiện component re-render liên tục do state leak.

b. Chrome DevTools

  • Memory Tab: Chụp heap snapshot trước và sau khi thao tác với component để tìm đối tượng bị rò rỉ.
  • Performance Tab: Theo dõi bộ nhớ theo thời gian thực, phát hiện tăng đột biến khi component unmount.

3. Case Study: Memory Leak trong quản lý form

Bài Toán: Form đăng ký sử dụng thư viện bên ngoài để validate, nhưng không hủy instance validate khi unmount.

Code Ban Đầu:

function RegistrationForm() {
  const [email, setEmail] = useState("")
  const emailRef = useRef(null)

  useEffect(() => {
    // Khởi tạo validator từ thư viện bên ngoài
    const validator = new ExternalValidator(emailRef.current, {
      rules: { email: "required|email" },
    })

    // ❌ Quên hủy validator
  }, [])

  return <input ref={emailRef} value={email} onChange={(e) => setEmail(e.target.value)} />
}

Nguyên Nhân:

  • Khi component unmount, validator vẫn giữ tham chiếu đến DOM element (emailRef.current) → DOM element không được giải phóng.

Cách Sửa:

useEffect(() => {
  const validator = new ExternalValidator(emailRef.current, {
    /* ... */
  })

  return () => {
    validator.destroy() // ✅ Hủy validator và dọn dẹp DOM
  }
}, [])

4. Best Practices để tránh Memory Leak trong React

a. Luôn dọn dẹp trong useEffect

  • Event listeners, subscriptions, timers: Luôn hủy trong cleanup function.
  • Async operations: Sử dụng AbortController hoặc biến flag (isMounted).

b. Tránh lưu trữ dữ liệu không cần thiết

  • State: Reset state khi component unmount nếu dữ liệu quá lớn.
  • Ref: Đặt ref.current = null trong cleanup nếu ref giữ tài nguyên (DOM element, third-party instances).

c. Sử dụng useMemo/useCallback đúng cách

  • Tránh tạo function/object mới không cần thiết → Giảm re-render và rò rỉ tiềm ẩn.
const fetchData = useCallback(async () => {
  // Logic fetch
}, [dependencies])

useEffect(() => {
  fetchData()
}, [fetchData])

d. Kiểm tra thư viện bên thứ ba

  • Đảm bảo thư viện cung cấp cơ chế hủy (ví dụ: unsubscribe(), destroy()).

Kết luận

Memory leak trong React thường xuất phát từ việc không tuân thủ nguyên tắc cleanup resources. Bằng cách:

  1. Luôn thêm cleanup function trong useEffect.
  2. Sử dụng AbortController cho async operations.
  3. Kiểm soát dữ liệu lớn trong state/ref, bạn có thể xây dựng ứng dụng ổn định và tiết kiệm tài nguyên.

Hãy coi cleanup là một phần không thể thiếu trong mỗi useEffect – nó không chỉ ngăn memory leak mà còn giúp code của bạn dễ bảo trì hơn.