Mẹo đơn giản để cải thiện hiệu suất với Async/Await 😊 (Series: Bí kíp Javascript - PHẦN 31)
Bạn có phải là người thích sử dụng cú pháp async
/await
không? Mình cũng vậy, theo quan điểm của mình thì async
/await
tốt hơn là Promise chains
. Nhưng mình tự hỏi liệu bạn đã sử dụng nó đúng cách chưa. Đôi khi nó có thể làm cho chương trình của bạn chạy chậm hơn mong đợi. Trong bài viết này, mình sẽ chia sẻ một điều rất quan trọng nếu bạn muốn cải thiện hiệu suất khi sử dụng async
/ await
và điều đó cực kỳ dễ áp dụng trong project của bạn.
Before Reading
Nhiều JavaScript Dev yêu thích cú pháp async
/ await
. Mình nghĩ lý do mà tại sao mọi người thích cú pháp này là vì Promise hell
, nó tiếp tục tạo ra một chain
khi nhiền lần chấm then
liên tiếp.
async
/ await
cho phép các bạn giảm bớt hoặc thoát khỏi Promise hell
bằng cách sử dụng keyword await
. Nhưng bạn đã bao giờ nghĩ rằng cú pháp này có thể làm chậm ứng dụng của bạn khi nó được sử dụng theo cách không tối ưu chưa?
Vậy chúng ta phải làm gì? Hãy nói về các lựa chọn của chúng ta nào.
Helper Functions
Giả sử có một hàm được gọi là sleep
. Nó sẽ chờ một khoảng thời gian bạn muốn và là một Promise
cơ bản.
const sleep = t => {
return new Promise (res => {
setTimeout (res, t);
});
};
Và chúng ta cũng có một hàm fetch
const fetch = url => {
const time = url.length * 1000;
sleep(time);
};
Hàm giả fetch
nhận đối số kiểu string
và nó được sử dụng để tính toán thời gian chờ. Nếu chúng ta gọi fetch('/notice')
, thời gian chờ sẽ là bảy giây vì /notice
có bảy ký tự.
Tình huống cơ bản và vấn đề gặp phải của ví dụ này là
Bây giờ, chúng ta truy cập ứng dụng; nó có lẽ là /
. Ngay sau khi chúng ta truy cập trang chính, code sẽ cố gắng load dữ liệu bằng cách nhập fetch
. (Ở ví dụ này mình dùng React
nhé)
// React base
async componentDidMount() {
const banners = await fetch('/banners');
const events = await fetch('/events');
const notices = await fetch('/notices');
/* Do other tasks here */
this.setState({
...this.state,
banners,
events,
notices
});
}
Chúng ta đã thực hiện một hàm fetch
trước đó, đó là một Promise
. Nó có đối số là 1 string.
/banners
→ tám ký tự/độ trễ tám giây/events
→ bảy ký tự/độ trễ bảy giây/notices
→ tám ký tự/độ trễ tám giây
Tổng thời gian fetch
sẽ là 23 giây
vì nó đợi quá trình fetch
hiện tại kết thúc trước khi gọi lần tiếp theo.
Cấu trúc này ổn nếu thứ tự fetch
là quan trọng. Ví dụ: chúng ta nên fetch
/login
trước /my-profile
vì đó là thông tin chỉ dành cho thành viên.
Điều gì sẽ xảy ra nếu có nhiều hơn năm API
mà bạn cần fetch trước khi hiển thị? Càng nhiều API, user của bạn sẽ đợi lâu hơn trước khi xem trang.
Giải phảp
Giải pháp rất rõ ràng và đơn giản.
// React base
async componentDidMount() {
const bannersFetch = fetch('/banners');
const eventsFetch = fetch('/events');
const noticesFetch = fetch('/notices');
/* Do other tasks here */
const banners = await bannersFetch;
const events = await eventsFetch;
const notices = await noticesFetch;
this.setState({
...this.state,
banners,
events,
notices
});
}
Điểm khác biệt duy nhất so với phiên bản trước đó là chúng ta không còn đặt keyword await
trước fetch
. Thay vào đó, chúng ta đặt nó sau. Điều gì xảy ra sau đó?
Mỗi fetch
request sẽ được gửi đến server vì không có request nào trong số chúng chờ response trước khi kích hoạt request tiếp theo fetch
.
Mặc dù không có thêm công việc nào trong componentDidMount
ở ví dụ trên, thời gian chờ tối đa lâu nhất sẽ chỉ là tám giây.
Promise mang đến điều kỳ diệu
Lý do mà chúng ta có thể nhận được lợi ích to lớn từ việc thay đổi một vài dòng code là vì Promise
. Về cơ bản, Promise
được biết đến như một API
hoạt động asynchronous
. Tuy nhiên, bí mật thực sự được giấu trong hàng đợi sự kiện.
JavaScript
là đơn luồng (single-threaded
), như nhiều người trong số các bạn đã biết. Điều đó có nghĩa là chỉ có một nhiệm vụ chạy trong chương trình trong cùng một thời điểm . Vùng chứa, là một loại hàng đợi giữ các tác vụ cho đến khi đến lượt chúng được thực thi, được gọi là event queue hoặc task queue điều này có thể khác nhau tùy thuộc vào tài liệu bạn đang đọc.
Ví dụ về các nhiệm vụ có thể được coi là một normal task là console.log(1)
, obj.foo()
hoặc bất cứ điều gì tương tự như vậy không phải là một job asynchronous
.
Promise
là một API asynchronous
. Trong thời gian chạy, JavaScript
gửi một tác vụ Promise
(hoặc một tác vụ dựa trên Promise
) đến hàng đợi sự kiện Promise
. Và các nhiệm vụ trong hàng đợi Promise
cũng phải đợi đến lượt được gọi. Nhưng lưu ý rằng bất kỳ tác vụ nào trong hàng đợi Promise
chỉ có thể được kéo ra và chạy nếu tác vụ bình thường normal task
hoàn toàn trống.
const banners = await fetch('/banners');
const events = await fetch('/events');
const notices = await fetch('/notices');
Do có keyword await
trên mỗi dòng, hàng đợi Promise
không thể nhận thêm nhiệm vụ. Thứ nhất, fetch('/banners')
được thực thi. Chương trình đợi response
của nó và đưa tác vụ fetch
tiếp theo vào hàng đợi.
const bannersFetch = fetch('/banner');
const eventsFetch = fetch('/events');
const noticesFetch = fetch('/notices');
const banners = await bannersFetch;
const events = await eventsFetch;
const notices = await noticesFetch;
Với code này, tất cả ba fetch
có thể được xem như là xếp chồng lên nhau trong hàng đợi Promise
theo thứ tự từ trên xuống dưới. Những Promise
đó sẽ được kéo ra và thực hiện gần như cùng một lúc (khi mà hàng đợi normal task
trống), nhưng chúng không phải đợi response của fetch
lần trước nữa.
Một ví dụ khác
const fetchA = () => {
return new Promise((callback) => {
setTimeout(() => callback("OK"), 3000);
});
};
const fetchB = () => {
return new Promise((callback) => {
setTimeout(() => callback("OK"), 2000);
});
};
const fetchC = () => {
return new Promise((callback) => {
setTimeout(() => callback("OK"), 1000);
});
};
const runFetch = async () => {
const startTime = new Date().getTime();
const A = await fetchA();
const B = await fetchB();
const C = await fetchC();
const result = {
A,
B,
C,
time: (new Date().getTime() - startTime) / 1000,
};
console.log("runFetch :>> ", result);
};
const runBetterFetch = async () => {
const startTime = new Date().getTime();
const fetch1 = fetchA();
const fetch2 = fetchB();
const fetch3 = fetchC();
const A = await fetch1;
const B = await fetch2;
const C = await fetch3;
const result = {
A,
B,
C,
time: (new Date().getTime() - startTime) / 1000,
};
console.log("runBetterFetch :>> ", result);
};
runFetch();
runBetterFetch();
Kết quả sẽ là:
runBetterFetch :>> { A: 'OK', B: 'OK', C: 'OK', time: 3.005 }
runFetch :>> { A: 'OK', B: 'OK', C: 'OK', time: 6.03 }
Promise.all
Bạn cũng có thể sử dụng Promise.all
để giải quyết vấn đề trên (hoặc Promise.allSettled
trong trường hợp bạn không quan tâm đến các request
không thành công).
const [banners, events, notices] = await Promise.all([
fetch("/banners"),
fetch("/events"),
fetch("/notices"),
]);
Kết luận
Sử dụng cú pháp async
/ await
làm cho cuộc sống của chúng ta dễ dàng hơn và hạnh phúc hơn, nhưng chúng ta nên sử dụng chúng tốt. Đơn giản chỉ cần đặt keyword await
ở đâu đó có thể gây ra những hậu quả rất khác nhau tùy thuộc vào nơi bạn đặt nó.
Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.
Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.
Momo: NGUYỄN ANH TUẤN - 0374226770
TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)
All rights reserved