# Async Waterfall: Chạy song song tác vụ độc lập thay vì tuần tự (/vi/docs/programming/async/avoiding-sequential-async-waterfalls)



## Tóm tắt nhanh (TL;DR) [#tóm-tắt-nhanh-tldr]

Hãy tưởng tượng một trang bảng điều khiển (dashboard) cần tải 4 khối dữ liệu độc lập: hồ sơ người dùng, cờ tính năng (feature flags), phân quyền và thông báo hệ thống. Mỗi API phản hồi trong 100ms. Nếu viết bằng một chuỗi `await` tuần tự nối đuôi nhau, 4 lời gọi 100ms sẽ biến thành một "thác nước" kéo dài 400ms—làm chậm ứng dụng gấp 4 lần và hủy hoại trải nghiệm người dùng cùng các chỉ số Core Web Vitals (LCP và TTFB). Bốn tác vụ này hoàn toàn không phụ thuộc dữ liệu của nhau, nhưng luồng điều khiển lại bắt chúng xếp hàng như những quân cờ domino. Khởi chạy song song bằng `Promise.all()` hoặc tách biệt thời điểm khởi tạo Promise khỏi thời điểm await sẽ co tổng thời gian chờ về đúng bằng request chậm nhất: \~100ms.

> 💡 &#x2A;*Quy tắc bỏ túi:** Tách biệt thời điểm khởi tạo Promise khỏi thời điểm await kết quả. Hãy kích hoạt mọi tác vụ bất đồng bộ độc lập càng sớm càng tốt, và chỉ `await` tại đúng vị trí mà công việc phía sau thực sự cần đến dữ liệu đầu ra của tác vụ đó.

* **`await` chỉ tạm dừng hàm hiện tại, không chặn Main Thread:** Đặt `await` trước một Promise độc lập sẽ khiến hàm hiện tại đứng chờ vô ích, bỏ lỡ cơ hội kích hoạt các tác vụ khác vốn có thể chạy ngầm đồng thời.
* **Tách biệt thời điểm gọi hàm và thời điểm await:** Gọi `const p = fetch(...)` là lệnh mạng đã lập tức được bắn đi; `await p` sau đó chỉ để thu nhận kết quả. Hãy kích hoạt sớm toàn bộ tác vụ độc lập rồi mới thu hoạch kết quả.
* **Chạy gối đầu co độ trễ về đường găng (Critical Path):** Các tác vụ độc lập chạy đồng thời sẽ rút ngắn thời gian xử lý từ tổng số `sum(thời_lượng)` xuống xấp xỉ bằng `max(thời_lượng)`, xóa bỏ thời gian chờ nhân tạo.
* **`Promise.all` không tự động hủy tác vụ nền:** Khi một Promise con thất bại, `Promise.all` lập tức reject (fail-fast), nhưng các tác vụ anh em đã khởi chạy vẫn âm thầm tiếp tục trừ khi được hủy chủ động bằng `AbortSignal`.
* **Cạm bẫy chết người (Cơn bão Fan-out không giới hạn):** Vội vã thay thế vòng lặp bằng `await Promise.all(items.map(...))` khi xử lý hàng nghìn phần tử sẽ tạo ra cơn bão kết nối đồng thời, làm cạn kiệt connection pool của cơ sở dữ liệu, tràn socket mạng và đánh sập hạ tầng nội bộ. Luôn phải giới hạn mức độ đồng thời (bounded concurrency) khi độ bung tỏa lớn.

<TermBox term="Async waterfall">
  **Async waterfall (Thác nước bất đồng bộ)** là hiện tượng các thao tác bất đồng bộ có thể chạy gối đầu thời gian nhưng lại bị bắt buộc chạy nối đuôi nhau do mã nguồn đặt `await` quá sớm.

  **Ý nghĩa thực tiễn:** Chờ đợi không cần thiết sẽ kéo dài tổng thời gian phản hồi mà không mang lại bất kỳ giá trị đúng đắn nào. Thực thi tuần tự chỉ có giá trị khi thao tác sau cần dữ liệu đầu ra từ thao tác trước hoặc thứ tự ghi dữ liệu bắt buộc phải tuần tự.
</TermBox>

## Mô hình tư duy (Mental Model) [#mô-hình-tư-duy-mental-model]

Hãy bắt đầu với một kịch bản thời gian biểu cụ thể. Giả sử hệ thống cần thực hiện ba tác vụ độc lập lần lượt mất 800ms, 400ms, và 300ms.

Nếu mỗi tác vụ chỉ khởi động sau khi tác vụ trước kết thúc hoàn toàn:

```text
Tác vụ A: 0 ───────────── 800
Tác vụ B:                 800 ───── 1200
Tác vụ C:                            1200 ─── 1500
```

Tổng thời gian thực thi là `800 + 400 + 300 = 1500ms`.

Nếu cả ba tác vụ có thể khởi động ngay lập tức tại thời điểm ban đầu:

```text
Tác vụ A: 0 ───────────── 800
Tác vụ B: 0 ───── 400
Tác vụ C: 0 ─── 300
```

Tổng thời gian thực thi chỉ bằng tác vụ lâu nhất: `max(800, 400, 300) = 800ms`. Khoảng thời gian chờ đợi đã gối đầu lên nhau, tiết kiệm được tới `700ms`.

<AtlasIllustration id="network-waterfall-vs-overlap" />

<TermBox term="Dependency graph">
  **Đồ thị phụ thuộc (Dependency graph)** mô tả những thao tác nào bắt buộc phải có kết quả từ các thao tác khác trước khi được phép bắt đầu. Một mũi tên `A → B` thể hiện B phụ thuộc vào dữ liệu do A sinh ra.

  **Ý nghĩa thực tiễn:** Thứ tự dòng code đọc từ trên xuống dưới rất dễ vô tình tạo ra sự chờ đợi giả mạo. Hãy tối ưu đồ thị phụ thuộc dữ liệu thay vì đếm số lượng từ khóa `await`.
</TermBox>

Câu hỏi cốt lõi khi thiết kế:

> Cần thông tin gì tồn tại trước khi thao tác này có thể bắt đầu?

Nếu câu trả lời là "không cần dữ liệu nào từ thao tác trước", thao tác đó hoàn toàn có thể được kích hoạt sớm hơn.

<TermBox term="Concurrency vs parallelism">
  **Concurrency (Tính đồng thời)** nghĩa là nhiều tác vụ cùng ở trạng thái đang diễn ra trong các khoảng thời gian gối đầu nhau. &#x2A;*Parallelism (Xử lý song song)** nghĩa là các lệnh CPU được thực thi vật lý tại cùng một tích tắc thời gian trên nhiều lõi vi xử lý.

  **Ý nghĩa thực tiễn:** Thời gian chờ mạng (I/O), cơ sở dữ liệu hay bộ đếm giờ có thể diễn ra đồng thời dù mã JavaScript chỉ chạy đơn luồng trên Main Thread.
</TermBox>

## Thử nghiệm thực tế: Async Waterfall Lab [#thử-nghiệm-thực-tế-async-waterfall-lab]

Tự điều chỉnh thời lượng của 3 tác vụ để quan sát trực quan sự chênh lệch thời gian giữa hai mô hình:

<AsyncWaterfallLab />

## Nhận diện nguyên nhân gây ra Thác nước [#nhận-diện-nguyên-nhân-gây-ra-thác-nước]

Mã nguồn tuần tự rất dễ đọc và trực quan, do đó thác nước vô tình xuất hiện trông rất bình thường:

```ts
const user = await getUser(id);
const organization = await getOrganization(user.organizationId);
const flags = await getFeatureFlags(id);
const recommendations = await getRecommendations(id);
```

Trong ví dụ trên:

* `getOrganization` bắt buộc phải chờ `getUser` vì nó cần `user.organizationId` (phụ thuộc thực tế).
* Nhưng `getFeatureFlags(id)` và `getRecommendations(id)` chỉ cần `id` ban đầu; chúng hoàn toàn không cần `user` hay `organization`! Việc bắt chúng đứng chờ là sự lãng phí thời gian không đáng có.

Giải pháp tối ưu là chạy gối đầu các tác vụ độc lập:

```ts
const flagsPromise = getFeatureFlags(id);
const recommendationsPromise = getRecommendations(id);

const user = await getUser(id);
const organization = await getOrganization(user.organizationId);

const [flags, recommendations] = await Promise.all([
  flagsPromise,
  recommendationsPromise,
]);
```

Ở đây, chuỗi `user → organization` vẫn diễn ra tuần tự đúng nghiệp vụ, trong khi hai tác vụ độc lập kia đã hoàn thành xong từ trước.

<TermBox term="Critical path">
  **Đường găng (Critical path)** là chuỗi các thao tác phụ thuộc nhau có tổng thời gian dài nhất, quyết định thời điểm hoàn thành sớm nhất của toàn bộ hệ thống.

  **Ý nghĩa thực tiễn:** Độ trễ đầu cuối được quyết định bởi đường găng, không phải tổng thời gian cộng dồn của từng thao tác riêng lẻ. Kích hoạt sớm các tác vụ ngoài đường găng sẽ rút ngắn thời gian xử lý mà không vi phạm tính toàn vẹn dữ liệu.
</TermBox>

<AtlasIllustration id="dag-critical-path" />

## `Promise.all()` là công cụ, không phải quy tắc vạn năng [#promiseall-là-công-cụ-không-phải-quy-tắc-vạn-năng]

`Promise.all(iterable)` trả về một Promise hoàn thành khi tất cả các Promise con đều hoàn thành. Kết quả trả về giữ nguyên thứ tự truyền vào ban đầu.

Tuy nhiên, cần đặc biệt lưu ý: &#x2A;*khi một Promise con bị reject, `Promise.all()` lập tức bị reject mà không hề tự động hủy bỏ (cancel) các tác vụ con khác đã được khởi chạy!** Các thao tác đó vẫn tiếp tục chạy ngầm trong nền trừ khi API bên dưới hỗ trợ cơ chế hủy (như `AbortSignal`) và bạn chủ động kích hoạt nó.

## Cân nhắc trong môi trường Production [#cân-nhắc-trong-môi-trường-production]

### Tình huống thực tế: Cơn bão Fan-out đánh sập dịch vụ nội bộ [#tình-huống-thực-tế-cơn-bão-fan-out-đánh-sập-dịch-vụ-nội-bộ]

Một dịch vụ báo cáo phân tích cần truy xuất lịch sử giao dịch cho 2,000 thành viên tích cực. Một lập trình viên nhận thấy vòng lặp `for...await` chạy quá chậm nên đã thay thế bằng:

```ts
await Promise.all(members.map(fetchHistory));
```

Ở môi trường kiểm thử với 10 người dùng, đoạn code chạy cực nhanh trong 45ms. Tuy nhiên khi triển khai lên production với 2,000 thành viên, nó lập tức kích hoạt 2,000 kết nối TCP đồng thời, làm tràn bộ đệm kết nối và kéo sập dịch vụ xác thực nội bộ với lỗi `ECONNRESET`, gây tê liệt hàng loạt dịch vụ phụ thuộc trong toàn công ty.

* **Hậu quả:** Hệ thống sập cục bộ trên diện rộng, tỷ lệ lỗi 5xx tăng đột biến trên production.
* **Nguyên nhân cốt lõi:** Lập trình viên nhầm lẫn giữa việc tối ưu hóa độ trễ của một request cá nhân với việc bảo toàn sức chịu tải đồng thời (throughput) của toàn hệ thống.
* **Cách khắc phục chuẩn:** Áp dụng cơ chế &#x2A;*giới hạn mức độ đồng thời (Bounded Concurrency)** thông qua Semaphore hoặc Worker Pool với `limit = 10-20`:

```ts
import pLimit from 'p-limit';
const limit = pLimit(15);

const results = await Promise.all(
  members.map(member => limit(() => fetchHistory(member)))
);
```

<TermBox term="Fan-out">
  **Fan-out (Độ bung tỏa)** là số lượng thao tác hạ tầng được kích hoạt từ một công việc đầu vào duy nhất. Một request gọi 100 dịch vụ có độ bung tỏa lớn hơn rất nhiều so với một request chỉ gọi 3 dịch vụ.

  **Ý nghĩa thực tiễn:** Bung tỏa không kiểm soát sẽ nhanh chóng vét cạn connection pool của cơ sở dữ liệu, socket mạng, file descriptors, dung lượng RAM và CPU.
</TermBox>

<AtlasIllustration id="bounded-concurrency" />

## Khi nào thực thi tuần tự là chính xác? [#khi-nào-thực-thi-tuần-tự-là-chính-xác]

Thực thi tuần tự không phải là lỗi thiết kế. Hãy giữ nguyên tính tuần tự khi:

* Thao tác sau cần dữ liệu được sinh ra từ thao tác trước.
* Các thao tác ghi dữ liệu bắt buộc phải theo thứ tự thời gian định sẵn.
* Giao thức xử lý giao dịch (Transaction Protocol) yêu cầu các bước tuần tự.
* Hệ thống chủ động áp dụng cơ chế điều áp ngược (Backpressure).
* Thao tác phía sau chỉ nên chạy sau khi bước xác thực hoặc kiểm tra quyền phía trước thành công để tránh lãng phí chi phí tính toán.

## Bài tập củng cố tư duy [#bài-tập-củng-cố-tư-duy]

Một request cần thực thi 4 thao tác sau:

| Thao tác                               | Thời lượng | Phụ thuộc |
| -------------------------------------- | ---------: | --------- |
| `getUser(id)`                          |      500ms | Không     |
| `getFeatureFlags(id)`                  |      300ms | Không     |
| `getRecommendations(id)`               |      700ms | Không     |
| `getOrganization(user.organizationId)` |      400ms | `getUser` |

Hãy xác định:

1. Những thao tác nào có thể chạy ngay?
2. Thao tác nào bắt buộc phải chờ?
3. Thời gian hoàn thành tối ưu nhất trong trường hợp lý tưởng là bao nhiêu?

<details>
  <summary>
    Xem giải thích chi tiết
  </summary>

  1. `getUser`, `getFeatureFlags`, và `getRecommendations` có thể bắt đầu ngay lập tức vì chúng chỉ cần `id` ban đầu.
  2. `getOrganization` bắt buộc phải chờ `getUser` vì nó cần giá trị `user.organizationId`.
  3. Đường găng (Critical path) của bài toán:

     * Nhánh `user → organization`: `500 + 400 = 900ms`.
     * Nhánh `featureFlags`: `300ms`.
     * Nhánh `recommendations`: `700ms`.

     Đường găng dài nhất là **900ms**. Đây chính là thời gian hoàn tất tối ưu. Nếu thực thi tuần tự hoàn toàn, hệ thống sẽ tốn tới `500 + 300 + 700 + 400 = 1900ms`.
</details>

## Nguyên tắc cốt lõi dành cho Kỹ sư [#nguyên-tắc-cốt-lõi-dành-cho-kỹ-sư]

> Khi các thao tác bất đồng bộ độc lập với nhau, hãy khởi chạy chúng trước khi chờ đợi kết quả của thao tác khác. Giữ nguyên `await` tuần tự khi có phụ thuộc dữ liệu thực tế hoặc khi cần điều áp hệ thống. Không bao giờ đưa vào cơ chế đồng thời không giới hạn chỉ để giảm chút độ trễ, và không mặc định rằng `Promise.all()` bị lỗi sẽ tự động hủy các thao tác đang chạy ngầm.

Checklist kiểm tra khi tối ưu luồng bất đồng bộ:

* [ ] **Phụ thuộc thật vs Phụ thuộc giả:** Thao tác B có thực sự cần giá trị trả về của thao tác A không, hay chỉ ngẫu nhiên được viết nối tiếp nhau?
* [ ] **Tác dụng phụ & Tính toàn vẹn:** Có thao tác ghi dữ liệu hoặc sửa đổi trạng thái nào trở nên mất an toàn nếu chạy đồng thời không?
* [ ] **Giới hạn Fan-out:** Số lượng thao tác đồng thời có nguy cơ làm cạn kiệt socket mạng, pool kết nối cơ sở dữ liệu hay vượt ngưỡng rate limit không?
* [ ] **Tận dụng thời gian gối đầu:** Có công việc độc lập nào có thể kích hoạt sớm trong khi đường găng đang chạy không?
* [ ] **Cơ chế hủy bỏ (Cancellation):** Nếu một thao tác thất bại, các request còn lại có cần được hủy bỏ chủ động qua `AbortSignal` không?

## Nguồn tham khảo chuẩn mực [#nguồn-tham-khảo-chuẩn-mực]

* [ECMAScript 2026 — `Promise.all`](https://tc39.es/ecma262/2026/multipage/control-abstraction-objects.html#sec-promise.all)
* [MDN — `await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)
* [MDN — `Promise.all()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)
* [MDN — Using promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises)
