Memory leak là gì?

https://vi.wikipedia.org/wiki/R%C3%B2_r%E1%BB%89_b%E1%BB%99_nh%E1%BB%9B

Theo Wikipedia, Memory leak (Rò rỉ bộ nhớ) là 1 loại rò rỉ tài nguyên xảy ra khi 1 chương trình máy tính quản lý không chính xác việc cấp phát bộ nhớ dẫn đến việc bộ nhớ đó không cần thiết nữa nhưng chưa được giải phóng (released). 1 memory leak có thể xảy ra khi 1 object lưu trữ trên bộ nhớ nhưng lại không thể truy cập đến được (cannot be accessed) khi chạy code.

Nói 1 cách đơn giản thì memory leak đề cập đến việc không thể truy cập hay tham chiếu đến dữ liệu tồn tại trên bộ nhớ. Hiện nay các ngôn ngữ lập trình đều có các kĩ thuật xử lý để giải phóng dữ liệu không cần thiết nữa gọi chung là garbage collection (mọi người có thể đọc thêm ở link sau: https://en.wikipedia.org/wiki/Garbage_collection_(computer_science) ), tuy nhiên lại có những lỗi không phổ biến khác có thể khiến ứng dụng React của bạn bị rò rỉ bộ nhớ (memory leak). Nếu ở mức độ lớn thì nó có thể gây ảnh hưởng đến hiệu suất làm việc ứng dụng của bạn.

Bài viết này sẽ đi vào nguyên nhân và cách khắc phục 1 số memory leaks mà bạn có thể gặp phải.

Nguyên nhân gây Memory Leaks ở ứng dụng React (React application)

Memory Leaks trong ứng dụng React thường là kết quả của việc không hủy đăng ký (subscriptions) khi component unmounted (ngắt kết nối). Những đăng ký ở đây có thể là việc lắng nghe các sự kiện DOM, đăng ký WebSocket hay 1 request đến API. 2 cái đầu thì bạn chỉ cần nhớ xóa đăng ký (remove event, unsubscribe) chúng trước khi component unmouted là được. Tuy nhiên đối với việc xử lý memory leak khi call API thì có chút phức tạp hơn.

Chúng ta có thể tham khảo đoạn code dưới đây:

import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';

const MyCompany = function() {
    const [ company, setCompany ] = useState(null);
    useEffect(() => {
        (async () {
             const { data } = await axios.get(
                 'https://random-data-api.com/api/company/random_company'
             );
             setCompany(data);
        })();
    }, []);

    return (
        <>
            <pre>{JSON.stringify(company, null, 3)}</pre>
            <Link to = '/anotherpage'>Another Interesting Page</Link>
        </>
    )
}

Đoạn code trên khá đơn giản, MyCompany component khi được kết nối (mounted) sẽ tạo 1 request lấy dữ liệu từ API và set vào biến company.

Vấn đề xảy ra là gì?

Giả sử nếu user của chúng ta khi vào đến đoạn code này, thực hiện việc call API nhưng với tốc độ mạng rùa bò, chưa có kết quả trả về thì họ đã quyết định chuyển sang 1 màn hình khác; lúc này request của chúng ta đã được tạo, browser (trình duyệt) của chúng ta đang chờ response trả về. Khi nhận được response thì chúng sẽ gọi phương thức setState (cụ thể là setCompany ở đoạn code trên) nhưng component thì đã bị unmounted.

Ngoài case set state này ra thì chúng ta cũng có nhiều dữ liệu không quan trọng trong app mà không cách nào truy cập được chúng nữa. Quá trình này lặp lại sẽ có thể khiến vấn đề tối ưu hiệu suất trong ứng dụng của bạn trở nên nghiêm trọng hơn.

Cách giải quyết ở đây: sử dụng AbortControllers

https://developer.mozilla.org/en-US/docs/Web/API/AbortController

Hiểu được vấn đề trên thì cách giải quyết ở đây là sẽ cancel (hủy) request đi ngay lúc mà component của chúng ta unmount, đảm bảo rằng chúng ta không cần dữ liệu từ API đó nữa. AbortControllers đại diện cho 1 đối tượng điều khiển (controller object) cho phép bạn hủy bỏ 1 hoặc nhiều Web request khi bạn cần. AbortControllers tạo ra với cú pháp new AbortController(), nó sẽ khởi tạo 1 instance của lớp AbortController. Mỗi đối tượng AbortController có 1 thuộc tính signal (read-only) để truyền vào trong request, kèm theo đó là 1 phương thức abort() dùng để gọi khi bạn muốn hủy request.

Viết thêm vào trong code của component MyCompany sẽ như sau:

import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';

const MyCompany = function() {
    const [ company, setCompany ] = useState(null);

    useEffect(() => {
         let abortController;
        (async () {
             abortController = new AbortController();
             let signal = abortController.signal;    

             // the signal is passed into the request(s) we want to abort using this controller
             const { data } = await axios.get(
                 'https://random-data-api.com/api/company/random_company',
                 { signal: signal }
             );
             setCompany(data);
        })();

        return () => abortController.abort();
    }, []);

    return (
        <>
            <pre>{JSON.stringify(company, null, 3)}</pre>
            <Link to = '/anotherpage'>Another Interesting Page</Link>
        </>
    )
}

Từ giờ, khi user có chuyển sang trang khác thì AbortController sẽ hủy request đó đi và bạn không cần bận tâm đến việc bị leak data ở đây nữa.

NOTE: Việc gọi abortController.abort() sau khi request hoàn thành sẽ không throw (bắn ra) bất cứ lỗi nào. abortController đơn giản là sẽ không làm bất cứ action gì khi request đã hoàn thành.

Link bài gốc:

https://dev.to/jeremiahjacinth13/memory-leaks-how-to-avoid-them-in-a-react-app-1g5e