+2

5 Sai Lầm Phổ Biến Khiến Hệ Thống Của Bạn Chậm Chạp

Trong quá trình phát triển ứng dụng, hiệu suất API là yếu tố cốt lõi quyết định trải nghiệm người dùng. Một API chạy chậm không chỉ gây khó chịu cho người dùng cuối mà còn tiêu tốn tài nguyên hệ thống (CPU, RAM, băng thông) do phải giữ request trong thời gian xử lý dài.

Dưới đây là 5 sai lầm phổ biến liên quan đến coding khiến API của bạn "ì ạch" và những giải pháp đơn giản nhưng hiệu quả để khắc phục. 👉️Video Chi tiết

1. Lấy Toàn Bộ Dữ Liệu (Over-Fetching Data)

Đây là thói quen phổ biến của nhiều lập trình viên: gom tất cả data (danh sách sản phẩm, ảnh, tồn kho, đánh giá...) vào chung một API endpoint.

Hậu quả:

Payload (phản hồi) trả về có thể lên tới vài chục MB, gây áp lực lớn lên server (đóng gói, gửi đi) và client (render, filter).

Lãng phí tài nguyên và làm tăng thời gian phản hồi một cách đáng kể.

Giải pháp:

Tách nhỏ API: Chia API theo từng chức năng hoặc component độc lập trên màn hình. Ví dụ, một API cho thông tin sản phẩm chính, một API khác cho các đánh giá.

Hạn chế data thừa: Chỉ trả về những trường dữ liệu mà client thực sự cần.

Lazy Loading: Ở tầng Frontend, sử dụng cơ chế lazy loading để chỉ tải những thành phần mà người dùng tương tác.

Tận dụng cơ chế song song: Việc tách nhỏ sẽ giúp tận dụng được cơ chế gửi request song song của trình duyệt.

2. Thao Tác Dữ Liệu Ở Tầng Application

Đây là sai lầm ít được nhắc đến hơn, nhưng lại gây hại nghiêm trọng với lượng dữ liệu lớn. Đó là việc tải toàn bộ dữ liệu từ Database lên tầng Application (Backend) rồi mới thực hiện các thao tác như lọc (filter), sắp xếp (sort), hoặc nhóm (group).

Hậu quả:

Application server phải gánh toàn bộ công việc xử lý dữ liệu, làm tăng tải CPU và bộ nhớ (RAM).

Làm mất đi lợi thế về tốc độ của Database.

Giải pháp:

Cô đọng Data trong Query: Hãy để Database làm công việc của nó. Sử dụng WHERE, ORDER BY, GROUP BY ngay trong câu truy vấn.

Tận dụng Index: Việc thực hiện các thao tác này ở Database sẽ tận dụng được Index và bộ Optimizer của hệ thống, giúp việc xử lý nhanh hơn rất nhiều.

3. Vấn đề N+1 Query

N+1 Query xảy ra khi bạn truy vấn danh sách N bản ghi (ví dụ: 50 User) trong một query ban đầu, sau đó tiếp tục thực hiện thêm N query con (tổng cộng N+1 query) trong vòng lặp để lấy dữ liệu liên quan cho từng bản ghi đó (ví dụ: lấy chi tiết đơn hàng cho từng User).

Hậu quả:

Với hàng nghìn bản ghi, số lượng query có thể lên đến hàng nghìn, làm Database System phải làm việc quá tải.

Tăng cao độ trễ và tải CPU.

Giải pháp:

Sử dụng Eager Loading: Hầu hết các ORM đều hỗ trợ Eager Loading (như with trong Laravel, include trong các framework khác). Kỹ thuật này sẽ lấy tất cả dữ liệu liên quan chỉ trong một hoặc hai query, sau đó application sẽ tự động ánh xạ dữ liệu.

Sử dụng JOIN: Thiết kế API trả đủ thông tin cần thiết ngay từ đầu bằng cách sử dụng các câu lệnh JOIN trong SQL.

4. Xử Lý Tất Cả trong Một Request (Long-Running Tasks)

Trong một số nghiệp vụ phức tạp, một request có thể kích hoạt nhiều tác vụ độc lập như:

Lưu thông tin đơn hàng vào Database. (Task quan trọng)

Gửi email xác nhận cho khách hàng/admin. (Task phụ)

Gọi API bên thứ ba để tạo đơn vận chuyển. (Task phụ)

Nếu phải chờ hoàn thành tất cả các task này rồi mới trả Response về cho client, thời gian phản hồi sẽ rất lâu.

Giải pháp:

Đồng bộ và Bất đồng bộ: Chỉ xử lý những phần quan trọng (ví dụ: Lưu đơn hàng) trong request chính.

Background Service/Queue: Đưa các tác vụ phụ, độc lập (gửi email, đồng bộ với bên thứ ba) vào một Background Service hoặc Message Broker (như RabbitMQ, Kafka).

Lợi ích: Người dùng nhận được phản hồi nhanh hơn, server chịu tải nhẹ hơn, và hệ thống dễ dàng mở rộng (scale) hơn.

5. Sử Dụng Offset/Limit Phân Trang Không Hiệu Quả

Khi phân trang, việc sử dụng LIMIT kết hợp với OFFSET có thể gây ra hiện tượng giảm hiệu suất rõ rệt đối với những page cuối cùng trong các table có lượng data lớn.

Nguyên nhân: Database phải scan gần như toàn bộ số trang trước đó để tìm đến vị trí OFFSET mong muốn. Thậm chí, một số framework còn thực hiện thêm bước COUNT để lấy tổng số bản ghi, dẫn đến việc phải scan full table hai lần.

Giải pháp:

Sử dụng Keyset Pagination (hoặc Cursor/Seek Pagination): Thay vì dùng OFFSET, hãy dùng giá trị của một trường duy nhất (ví dụ: id hoặc timestamp) làm điểm neo.

Ví dụ: Đây là một ví dụ cụ thể để bạn thấy sự khác biệt giữa cách phân trang truyền thống (LIMIT/OFFSET) và Keyset Pagination (sử dụng WHERE trên khóa chính/chỉ mục).

1. Phân trang truyền thống (Dùng OFFSET) Cách này yêu cầu Database phải scan qua 1000 bản ghi đầu tiên, sau đó mới lấy ra 10 bản ghi tiếp theo. Khi OFFSET càng lớn, Database càng phải làm việc vất vả. Chúng ta giả sử có một bảng products với hàng triệu bản ghi, và bạn muốn lấy 10 sản phẩm mỗi trang.

Số trang SQL note
1 SELECT * FROM products ORDER BY id ASC LIMIT 10 OFFSET 0; Lấy 10 bản ghi đầu.
101 SELECT * FROM products ORDER BY id ASC LIMIT 10 OFFSET 1000; Database phải bỏ qua 1000 bản ghi, sau đó mới lấy 10 bản ghi.

2. Keyset Pagination (Dùng WHERE và id cuối cùng) Thay vì nói với Database "hãy bỏ qua 1000 bản ghi", bạn nói "hãy tìm các bản ghi có ID lớn hơn ID cuối cùng của trang trước đó". Database sẽ sử dụng index trên cột id để nhảy thẳng đến vị trí cần thiết, hiệu suất gần như không thay đổi dù bạn ở trang 10 hay trang 10000. Giả định: Trang 1 được trả về, bản ghi cuối cùng có id = 10.

Trang 100 được trả về, bản ghi cuối cùng có id = 1000.

trang cuối cùng SQL note
Trang 1 ((Không có) SELECT * FROM products ORDER BY id ASC LIMIT 10; Lấy 10 bản ghi đầu.
Trang 2 (10) SELECT * FROM products WHERE id > 10 ORDER BY id ASC LIMIT 10; Database dùng index tìm thẳng đến ID > 10.
Trang 101 (1000) SELECT * FROM products WHERE id > 1000 ORDER BY id ASC LIMIT 10; Database dùng index tìm thẳng đến ID > 1000. Tốc độ rất nhanh và không phụ thuộc vào số trang đã qua.
  • Tốc độ: Nhanh hơn đáng kể khi truy vấn các trang sâu (offset lớn) vì nó tận dụng được Index.
  • Đơn giản hóa: Tránh được lỗi sai khi Database phải tính toán OFFSET lớn. *Note: Yêu cầu: Bảng phải có một cột được index (thường là khóa chính id) và dữ liệu phải được sắp xếp theo cột đó (ORDER BY id ASC). Tách Query: Nếu bắt buộc phải dùng OFFSET và muốn lấy cả COUNT, hãy tách riêng việc tính COUNT và lấy dữ liệu thành hai API khác nhau để kiểm soát tốt hơn. Việc tối ưu hóa hiệu suất API đôi khi chỉ là những thay đổi nhỏ trong cách code và tư duy truy vấn dữ liệu. Hãy luôn nhớ: "Một API tốt là API trả đúng thứ và đúng lúc khi người dùng cần."

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí