Giới thiệu Các ứng dụng web hiện đại có thể cảm thấy nặng nề.Đôi khi, chỉ cần một chức năng JavaScript chạy trong một thời gian dài là đủ để đóng băng giao diện, để lại người dùng thất vọng và không chắc chắn liệu ứng dụng vẫn hoạt động hay đã đóng băng. Giới thiệu một phương pháp nhỏ nhưng mạnh mẽ: Nó cho phép người dùng tạm dừng việc thực hiện, cho trình duyệt một cơ hội để xử lý các nhiệm vụ quan trọng hơn (chẳng hạn như nhấp chuột hoặc gõ), và sau đó tiếp tục ngay nơi họ dừng lại. , khám phá các workarounds cũ, và xem làm thế nào Làm cho cuộc sống dễ dàng hơn Task Scheduling API ưu tiên Lời bài hát: Yield( Main Thread scheduler.yield() Lời bài hát: What Is Planer.Yield() Vì vậy, những gì là Nó là một phương pháp của Interface từ New Phương pháp này cho phép bạn, với tư cách là một nhà phát triển, tạm dừng việc thực hiện JavaScript của bạn và mang lại quyền kiểm soát một cách rõ ràng. – để nó có thể xử lý các nhiệm vụ quan trọng khác đang chờ đợi, chẳng hạn như tương tác người dùng, nhấp chuột, gõ, v.v., và sau đó tiếp tục thực hiện từ nơi bạn ngừng. Bạn đang nói với browser: scheduler.yield() Lịch Task Scheduling API ưu tiên. Main Thread scheduler.yield() "Chờ đợi, hít thở, chúng ta hãy tạm dừng nhiệm vụ hiện tại và tập trung vào các nhiệm vụ khác không kém hoặc quan trọng hơn. "Chờ đợi, hít thở, chúng ta hãy tạm dừng nhiệm vụ hiện tại và tập trung vào các nhiệm vụ khác không kém hoặc quan trọng hơn. Điều này làm cho trang của bạn đáp ứng hơn, đặc biệt là khi chạy các nhiệm vụ JavaScript dài hoặc nặng. – đó là tất cả về tốc độ trình duyệt phản ứng với đầu vào của người dùng. Tương tác với Next Paint (INP) của Terminology. Trước khi bạn đi sâu hơn, hãy nhanh chóng đi qua một vài thuật ngữ cơ bản sẽ được sử dụng trong suốt bài viết. Main Thread – Đây là nơi trung tâm nơi trình duyệt thực hiện hầu hết công việc của nó. Nó xử lý rendering, bố trí, và chạy hầu hết mã JavaScript của bạn. Nhiệm vụ dài – Đây là bất kỳ nhiệm vụ JavaScript nào giữ cho Main Thread bận rộn quá lâu, thường là hơn 50 milisecond. Tác vụ chặn – Là một hoạt động đồng bộ trên Main Thread ngăn chặn trình duyệt xử lý các điều quan trọng khác, chẳng hạn như trả lời các nhấp chuột hoặc cập nhật UI. Vấn đề Để hiểu được vẻ đẹp của , trước tiên bạn cần phải hiểu vấn đề mà nó đang cố gắng giải quyết. JavaScript chạy trên một chủ đề duy nhất. Điều đó có nghĩa là nó chỉ có thể làm một điều tại một thời điểm. Nếu mã của bạn giữ cho chủ đề bận rộn, mọi thứ khác - rendering, nhấp chuột, nhập phải chờ đợi. Trong một thế giới lý tưởng, bạn sẽ luôn luôn chia các nhiệm vụ nặng thành các mảnh nhỏ. Nhưng thực tế là hỗn loạn. Bạn đang đối phó với mã truyền thống, kịch bản của bên thứ ba, hoặc tính toán nặng không thể tránh khỏi. Và khi điều đó xảy ra, người dùng bị mắc kẹt với các trang bị đóng băng. scheduler.yield() Mô hình thực hiện JavaScript. Như một bản cập nhật nhanh, đây là một biểu đồ ví dụ về cách JavaScript xử lý các nhiệm vụ - nói cách khác, làm thế nào Tôi chắc chắn nhiều người trong số các bạn đã nhìn thấy các biểu đồ như thế này trước đây – hàng đợi nhiệm vụ, vòng tròn sự kiện, số lượng cuộc gọi. JavaScript Execution Model Chúng ta hãy đi qua những ý tưởng chính từng bước: Tất cả mã đồng bộ đi thẳng vào Call Stack và chạy theo dòng, chức năng theo chức năng. Nó tuân theo nguyên tắc LIFO - cuối cùng vào, đầu tiên ra. JavaScript chạy trong một chủ đề duy nhất, có nghĩa là nó chỉ có thể làm một điều tại một thời điểm. Các hoạt động không đồng bộ (chẳng hạn như setTimeout, fetch) được xử lý bên ngoài Main Thread – bởi các API Web (được cung cấp bởi trình duyệt hoặc môi trường). Một khi chúng được thực hiện, chúng không quay trở lại trực tiếp vào Call Stack. Thay vào đó, các cuộc gọi của họ được xếp hàng – hoặc trong chuỗi microtasks (ví dụ: Promise.then, chuỗiMicrotask) hoặc chuỗi macrotasks (ví dụ: setTimeout, setInterval). Khi Call Stack trống rỗng, Event Loop kiểm tra hàng đợi microtasks và chạy tất cả microtasks một cho một theo thứ tự. Chỉ sau đó, nó lấy một macrotask từ hàng đợi và chạy nó. Nếu, trong quá trình, microtasks mới được thêm vào, chúng được chạy trước macrotasks tiếp theo. Vòng tròn này tiếp tục: tất cả các microtask → một macrotask → lặp lại. Mã đồng bộ mới được nhập vào Call Stack khi các tác vụ mới đến, chẳng hạn như một người dùng nhấp vào một nút, một kịch bản mới đang chạy, hoặc khi một microtask hoặc macrotask chạy cuộc gọi của nó. Đây là một lời giải thích rất ngắn gọn và hời hợt, chỉ để nhắc nhở bạn làm thế nào nó hoạt động. Mô tả vấn đề Bây giờ bạn đã làm mới sự hiểu biết của bạn về cách JavaScript thực hiện các nhiệm vụ, chúng ta hãy xem xét kỹ hơn vấn đề thực sự đi kèm với mô hình này. Vấn đề đơn giản: khi một nhiệm vụ mất quá nhiều thời gian trên chủ đề chính, nó chặn mọi thứ khác – tương tác người dùng, hiển thị cập nhật và hoạt hình. Điều này dẫn đến UI đóng băng và phản hồi kém. Ý nghĩ đầu tiên rõ ràng có thể là: "Vâng, chỉ cần không viết các chức năng dài hoặc nặng, và đó là nó. Vấn đề được giải quyết!". Và vâng, đó là sự thật - trong một thế giới lý tưởng, bạn sẽ luôn luôn chia mã nặng thành các phần nhỏ hơn, tối ưu hóa mọi thứ, và tránh chặn chủ đề chính. Nhưng hãy trung thực - nhiều người trong chúng ta đã chạy vào các vấn đề này, ngay cả khi chúng ta không Để làm điều này, chúng tôi sẽ tạo ra một chức năng gọi là hoạt động như một nhiệm vụ chặn cho chủ đề chính trong khoảng thời gian được chỉ định. hàm mô phỏng loại tính toán "khó" này trên mỗi yếu tố của mảng. blockingTask() Thật không may, các khối mã không hiển thị số dòng. Trong lời giải thích của tôi, đôi khi tôi đề cập đến các dòng cụ thể (ví dụ, "line 5 does X"). Thật không may, các khối mã không hiển thị số dòng. Trong lời giải thích của tôi, đôi khi tôi đề cập đến các dòng cụ thể (ví dụ, "line 5 does X"). function blockingTask(ms = 10) { const arr = []; const start = performance.now(); while (performance.now() - start < ms) { // Perform pointless computation to block the CPU. arr.unshift(Math.sqrt(Math.random())); } return arr; } Không có gì tuyệt vời về chức năng này, đây là tất cả những gì nó làm: Nó chấp nhận một đối số – số milliseconds.Đây là thời gian tối thiểu mà hàm sẽ chạy, do đó chiếm chủ đề chính. Nó tạo ra một array trống. Nó tạo một thời gian bắt đầu (như thời gian hiện tại). Sau đó chạy vòng một thời gian cho đến khi thời gian quy định đã trôi qua. Bên trong vòng tròn, nó chỉ thực hiện các tính toán ngẫu nhiên, vô nghĩa để mô phỏng tải. Cuối cùng, nó trả về kết quả của các tính toán. Chức năng này không làm bất cứ điều gì hữu ích, nhưng nó mô phỏng một kịch bản thế giới thực của tải trọng nặng. Hãy tưởng tượng một tình huống phổ biến mà bạn cần phải cuộn qua một loạt dữ liệu và áp dụng công việc nặng nề đó cho mỗi mục. Để làm điều này, chúng tôi sẽ tạo ra một Chức năng : heavyWork() function heavyWork () { const data = Array.from({ length: 200 }, (_, i) => i) const result = [] for (let i = 0; i < data.length; i++) { result.push(blockingTask(10)) } return result; } Trong đó xảy ra những điều sau đây: Trên dòng 2, nó tạo ra một loạt các mặt hàng 200, chỉ số từ 0 đến 199. Tôi muốn lưu ý rằng 200 mặt hàng không phải là rất nhiều, nhưng nó sẽ là đủ để xem bản chất của vấn đề. Sau đó, một mảng "kết quả" trống mới được tạo để lưu trữ các giá trị được xử lý. Dòng 5 tuyên bố một vòng tròn đi qua toàn bộ chiều dài của mảng dữ liệu. Bên trong vòng tròn, chúng tôi chạy chức năng BlockingTask() , mô phỏng 10 milliseconds của công việc cho mỗi yếu tố, và kết quả được thêm vào mảng "kết quả". Một lần nữa, tôi muốn nhắc nhở bạn rằng, cho bản demo, chức năng BlockingTask() không mang bất kỳ tải trọng ngữ nghĩa nào. Nó chỉ đơn giản là thực hiện một số công việc tưởng tượng, chuyên sâu về tài nguyên. Trong thế giới thực, nó có thể là một số quá trình xử lý công việc của một yếu tố mảng. Cuối cùng, nó trả về mảng kết quả. Và đó là nơi phần đáng kinh ngạc đi vào. chỉ 10 milliseconds mỗi yếu tố, và chỉ 200 yếu tố - nhưng cùng nhau, họ chặn chủ đề chính trong 2 giây đầy đủ. Vấn đề biểu tình Now it’s time to look at the problem not just in theory, but in action. This is not a full-fledged demo just yet – think of it as a simplified visual to help you clearly see the issue. Đây là những gì bạn thấy: Cửa sổ bên trái, có tựa đề "Configuration", cho phép bạn bật và tắt khóa chủ đề chính - có nghĩa là nếu chức năng blockingTask() thực sự đang chạy. Cửa sổ có tựa đề "Heavy Task" chạy hàm heavyWork(). Đây là hàm xử lý một mảng bằng cách sử dụng blockingTask() trên mỗi yếu tố nếu khóa chủ đề được bật. Và cửa sổ có tựa đề "Logger" chỉ ghi lại thời gian hiện tại cho console, bao gồm cả milliseconds. Chúng ta hãy xem điều gì sẽ xảy ra khi Chặn được tắt, vì vậy các nhiệm vụ rất nhẹ. nó chỉ là một vòng tròn trên một mảng của 200 yếu tố, mà không có bất kỳ tính toán phức tạp. Main Thread Những gì bạn quan sát: Người dùng nhấp vào nút "OK" - hàm heavyWork() chạy, và ngay lập tức trở lại. Điều này được chỉ ra bởi thông điệp HEAVY_TASK_DONE trong console, sau đó là kết quả - một loạt các số. Sau đó người dùng nhấp vào nút "Log" ba lần, để đăng nhập thời gian hiện tại vào console - dấu thời gian xuất hiện ngay lập tức, với một sự khác biệt nhỏ về thời gian. Người dùng chạy hàm heavyWork() một lần nữa, và một lần nữa, trả lời ngay lập tức. Cuối cùng, người dùng đóng hai cửa sổ, thực sự chỉ loại bỏ các yếu tố đó khỏi DOM. Trong trường hợp này, mọi thứ cảm thấy nhanh chóng và đáp ứng. Trình duyệt không có vấn đề xử lý các tương tác, bởi vì chủ đề chính vẫn miễn phí. Bây giờ, chúng ta hãy cho phép chặn, để cho mỗi yếu tố của mảng, các chức năng sẽ được gọi với sự chậm trễ chỉ 10 millisecond. Main Thread blockingTask() Và bây giờ bạn có thể quan sát rằng sự tương tác của người dùng với các yếu tố UI đã trở nên ít mịn màng hơn, UI đóng băng đã xuất hiện. Người dùng nhấn nút "OK", do đó khởi động chức năng heavyWork() và sự chậm trễ đầu tiên xảy ra là nút "OK" được nhấn trực quan.Tại sao? Bởi vì trình duyệt không thể sơn lại trong khi heavyWork() vẫn chặn chủ đề chính. Trong thời gian này, người dùng nhấp vào nút "Log" bốn lần - không có gì xảy ra. Các nhấp chuột được đăng ký và người xử lý của họ được thêm vào hàng đợi, nhưng trình duyệt không thể phản ứng. Chỉ sau khi heavyWork() kết thúc bạn thấy đầu ra của console: đầu tiên là kết quả heavyWork(), sau đó là bốn timestamps - tất cả được in trong một loạt. Tiếp theo, người dùng nhấp vào nút "OK" một lần nữa. Hành vi tương tự - nút dính. Sau đó, trong khi nhiệm vụ heavyWork() đang chạy, anh ta cố gắng đóng một cửa sổ bằng cách nhấp vào biểu tượng "X" ba lần. Một lần nữa, không có phản ứng trực quan. Chỉ khi nhiệm vụ kết thúc, chúng ta thấy cửa sổ biến mất. Và cuối cùng, một nỗ lực khác để chạy heavyWork() và đóng cửa sổ cuối cùng. Demo đơn giản này cho thấy các nhiệm vụ chặn khả năng của trình duyệt để đáp ứng các hành động của người dùng trong bao lâu. Mặc dù mỗi cuộc gọi chặn chỉ mất 10 milliseconds, chuỗi 200 trong số họ kết quả trong một đóng băng 2 giây. Người dùng không thể tương tác với các nút, giao diện không sơn lại. Các sự kiện được xếp hàng, nhưng không được xử lý cho đến khi khối cuộc gọi được làm rõ. Đây không chỉ là một vấn đề về hiệu suất - đó là một vấn đề kinh nghiệm người dùng. Và đó chính xác là loại vấn đề mà chúng tôi muốn giải quyết - lý tưởng, mà không cần phải thủ công chia logic của chúng tôi thành hàng chục phản hồi. Giải pháp của vấn đề. Bây giờ bạn hiểu vấn đề, chúng ta hãy nói về các giải pháp có thể. Tất nhiên, chiến lược tốt nhất là tránh các nhiệm vụ dài ở nơi đầu tiên bằng cách giữ cho mã hiệu quả và phá vỡ mọi thứ sớm. Nhưng, như bạn đã thấy, mọi thứ xảy ra. Cho dù đó là mã cũ, tính toán không thể tránh khỏi, hoặc chỉ không có đủ thời gian để tối ưu hóa, đôi khi, bạn phải đối phó với nó. xuất hiện, các giải pháp và thủ thuật khác nhau đã được đưa ra để cải thiện khả năng đáp ứng. nhưng ý tưởng cốt lõi đằng sau tất cả chúng - và đằng sau Ngoài ra - là khá đơn giản: Prioritized Task Scheduling API scheduler.yield() Chia một nhiệm vụ thành các mảnh nhỏ hơn hoặc cái gọi là mảnh. Và thỉnh thoảng, dừng lại để cho trình duyệt thở. Nói cách khác, bạn cho chủ đề một cơ hội để chạy các nhiệm vụ cấp bách hơn, chẳng hạn như tương tác người dùng hoặc hiển thị cập nhật, và sau đó bạn quay trở lại để hoàn thành công việc của riêng bạn. Đây là khái niệm của Chức năng này trông giống như trong pseudocode: heavyWork() function heavyWork() { // Do heavy work... /** * Take a breather! * Yield the execution to the Main Thread... * */ // Continue to do heavy work... } Điều gì đang xảy ra ở đây: Bạn đang thực hiện một phần nhiệm vụ của bạn. Sau đó, bạn tạm dừng, cho phép trình duyệt xử lý các nhiệm vụ ưu tiên cao khác (chẳng hạn như cập nhật UI). Tiếp tục thực hiện chức năng từ nơi nó dừng lại. Phương pháp giải quyết vấn đề cũ trước đến, thủ thuật phổ biến nhất để đối phó với các nhiệm vụ chặn dài là sử dụng Bằng cách gọi nó với 0 (zero) chậm trễ, bạn thêm nhiệm vụ gọi lại của nó vào cuối hàng đợi, cho phép các nhiệm vụ khác chạy trước.Nói cách khác, bạn nói với trình duyệt: scheduler.yield() setTimeout() macrotasks "Hãy chạy phần code này sau đó, sau khi bạn đã xử lý mọi thứ khác". "Hãy chạy phần code này sau đó, sau khi bạn đã xử lý mọi thứ khác". Đó là cách bạn có thể cung cấp cho chủ đề một hơi thở ngắn giữa các mảnh công việc nặng nề. chức năng có thể trông giống như sử dụng phương pháp này: heavyWork() async function heavyWork() { // Yield to Main Thread to avoid UI blocking before heavy work await new Promise(resolve => setTimeout(resolve, 0)) const data = Array.from({ length: 200 }, (_, i) => i) const result = [] // Interval at which execution will be yielded to the main thread (approx. ~ 25%). const yieldInterval = Math.ceil(data.length / 4) for (let i = 0; i < data.length; i++) { // Yield control to Main Thread to update UI and handle other tasks. if (i % yieldInterval === 0) { await new Promise(resolve => setTimeout(resolve, 0)) } result.push(threadBlockingEnabled ? blockingTask(10) : data[i]) } return result } Hãy chia nhỏ những gì đang xảy ra ở đây: Dòng 3: Một lời hứa được tạo ra và người thực hiện của nó chạy ngay lập tức, lên lịch một setTimeout() với sự chậm trễ không. Việc gọi lại của timeout (giải quyết lời hứa) được thêm vào cuối chuỗi macrotask. Do chờ đợi, phần còn lại của hàm async bị tạm dừng. Về mặt kỹ thuật, phần tiếp tục này được thêm vào chuỗi microtask, chờ đợi để lời hứa được giải quyết. Công cụ JavaScript kiểm tra Stack Call – một khi nó trống rỗng, Event Loop bắt đầu chạy. Đầu tiên, nó nhìn vào chuỗi microtask – nhưng kể từ khi lời hứa chưa được giải quyết, không có gì để chạy. Sau đó, Event Loop chọn macrotask từ chuỗi (trong ví dụ của chúng tôi, nó là Time setout() callback), chạy nó, và điều này Dòng 9: Chúng tôi tính toán tần suất chúng tôi muốn cung cấp cho Chủ đề, khoảng 25% công việc. con số này có thể thay đổi tùy thuộc vào mức độ nặng của nhiệm vụ. Dòng 13-15: Trong vòng tròn, nếu điều kiện cho interval yielding được đáp ứng, việc thực hiện được chuyển sang chủ đề, tức là, kỹ thuật setTimeout() được lặp lại, cho phép trình duyệt tương tác với người dùng hoặc vẽ lại giao diện. Về cơ bản, cách tiếp cận này hoạt động – nó tương đối đơn giản và cải thiện khả năng đáp ứng. Nhưng có những thỏa hiệp. Nó đặt nhiệm vụ ở cuối hàng đợi macrotask, và bất cứ điều gì đã có trong hàng đợi đó có thể trì hoãn việc tiếp tục của bạn. setTimeout() Ví dụ, giả sử một phần khác của trang sử dụng Thực hiện các nhiệm vụ thường xuyên: setInterval() setInterval(() => { /* Another heavy work... */ }) async function heavyWork() { // Yield to Main Thread to avoid UI blocking before heavy work await new Promise(resolve => setTimeout(resolve, 0)) const data = Array.from({ length: 200 }, (_, i) => i) const result = [] // Interval at which execution will be yielded to the main thread (approx. ~ 25%). const yieldInterval = Math.ceil(data.length / 4) for (let i = 0; i < data.length; i++) { // Yield control to Main Thread to update UI and handle other tasks. if (i % yieldInterval === 0) { await new Promise((resolve, reject) => setTimeout(resolve, 0)) } result.push(threadBlockingEnabled ? blockingTask(10) : data[i]) } return result } Bây giờ nhiệm vụ của riêng bạn - phần tiếp theo của chức năng – có thể bị trì hoãn bởi một hoặc nhiều cuộc gọi ngược thời gian này. trình duyệt chỉ chạy bất cứ điều gì tiếp theo trong hàng, và bạn không kiểm soát trật tự. Loại này cho phép bạn đầu tư, bạn không biết chính xác khi nào bạn sẽ lấy lại quyền kiểm soát. heavyWork() setTimeout() Có những cách khác để đối phó với tình huống này. chức năng, cho phép bạn lên lịch công việc ngay trước khi vẽ lại tiếp theo.Thường được sử dụng kết hợp với , và có những nhược điểm tương tự. hoặc mà chạy mã của bạn trong một thời gian trống rỗng trình duyệt. Nó không phải là một lựa chọn thay thế, nhưng tốt cho nền tảng, công việc ít quan trọng hơn, giúp các chủ đề chính để được miễn phí cho các nhiệm vụ quan trọng hơn. Nói chung, chúng ta có thể thảo luận về các chiến lược khác để giải quyết và ngăn chặn các vấn đề như vậy. Tuy nhiên, để ở lại trên chủ đề, hãy tiếp tục và xem những gì đưa lên bàn. requestAnimationFrame() setTimeout() requestIdleCallback() scheduler.yield() Trang chủ » Lợi nhuận – là một cách hiện đại để tạm dừng việc thực hiện, và mang lại quyền kiểm soát cho chủ đề chính, cho phép trình duyệt thực hiện bất kỳ công việc ưu tiên cao nào đang chờ, và sau đó tiếp tục thực hiện từ nơi nó dừng lại. biểu thức đạt được, việc thực hiện hàm hiện tại trong đó nó đã được gọi bị đình chỉ, và mang lại quyền kiểm soát cho chủ đề chính, do đó phá vỡ, hoặc tạm dừng, nhiệm vụ hiện tại. scheduler.yield() await scheduler.yield() Vẻ đẹp của Đó là sự tiếp tục sau khi vẫn ở phía trước hàng đợi, và được lên kế hoạch để chạy bất kỳ nhiệm vụ không thiết yếu nào khác đã được xếp hàng. sự khác biệt chính từ Cách tiếp cận đó là với , những phần tiếp theo này thường chạy sau khi bất kỳ nhiệm vụ mới nào đã được xếp hàng, có khả năng gây ra sự chậm trễ dài giữa việc chuyển sang chủ đề và hoàn thành chúng. scheduler.yield() scheduler.yield() BEFORE setTimeout() setTimeout() Biểu đồ sau đây minh họa cách ba cách tiếp cận so sánh trong thực tế: In the first example, without yielding to the main thread: At first, the long " " runs uninterrupted, blocking the main thread and UI accordingly. Then, a user event is processed – a button click triggered during the execution of " ". And finally, " " is executed – callback scheduled earlier or during the execution of the long task. Task 1 Task 1 Task 2 setTimeout() In the second example, using as a yielding to the main thread: The execution queue is different. At first, the long " " runs. Then, when the yield to the main thread happens, " " pauses to let the browser breathe, and the button click is processed. But after the button click is processed, the callback will be executed first, which could have been scheduled in advance or during the execution of " ". And finally, only after that, the continuation of " " will be executed. setTimeout() Task 1 Task 1 setTimeout() Task 1 Task 1 In the last example, using : After the long " " has been paused and the user click event has been processed, then the continuation of " " is prioritized and runs before any queued tasks. scheduler.yield() Task 1 Task 1 setTimeout() In summary, is a more intelligent and predictable way to give the main thread breathing room. It avoids the risk of your code being pushed too far back in the queue and helps maintain performance and responsiveness, especially in complex applications. scheduler.yield() Ưu tiên Vì vậy, những gì gây ra sự khác biệt như vậy trong hành vi? Đó là tất cả về các ưu tiên! Là các nhà phát triển, chúng tôi thường không nghĩ về thứ tự thực hiện các nhiệm vụ trong vòng tròn sự kiện về các ưu tiên. và Nhưng nếu bạn nhìn sâu hơn, bạn sẽ nhận thấy rằng cũng có các ưu tiên ngụ ý trong trò chơi. Ví dụ, một trình xử lý nhấp chuột, được kích hoạt bởi hành động của người dùng, thường sẽ thực hiện trước khi một Callback, mặc dù cả hai đều Như đã đề cập trước đó, Là một phần của – một giao diện rộng rãi và giàu tính năng xứng đáng với một cuộc thảo luận toàn diện riêng biệt và rõ ràng vượt ra ngoài phạm vi của cuộc trò chuyện này. Tuy nhiên, điều quan trọng là phải đề cập đến một trong những tính năng chính của nó: việc giới thiệu một mô hình ưu tiên nhiệm vụ rõ ràng. API Lập lịch nhiệm vụ ưu tiên chỉ đơn giản là làm cho các ưu tiên này rõ ràng, làm cho nó dễ dàng hơn để xác định nhiệm vụ nào sẽ chạy trước, và cho phép điều chỉnh ưu tiên để thay đổi thứ tự thực hiện, nếu cần thiết. microtasks macrotasks setTimeout() macrotasks scheduler.yield() Prioritized Task Scheduling API "Ngăn chặn người dùng" - Các nhiệm vụ ưu tiên cao nhất ảnh hưởng trực tiếp đến sự tương tác của người dùng, chẳng hạn như xử lý nhấp chuột, nhấn và các hoạt động UI quan trọng. "Nhìn thấy người dùng" - Các nhiệm vụ ảnh hưởng đến khả năng hiển thị UI hoặc nội dung, nhưng không quan trọng đối với đầu vào ngay lập tức. "Phía sau" - Các nhiệm vụ không khẩn cấp, có thể được hoãn lại một cách an toàn mà không ảnh hưởng đến trải nghiệm người dùng hiện tại, và không hiển thị cho người dùng. Theo mặc định, có a” Ưu tiên: Ngoài ra, Triển lãm The phương pháp, được chỉ định để lên lịch các nhiệm vụ với một ưu tiên được chỉ định từ trên. Trong khi nó sẽ không đi vào chi tiết về phương pháp này ở đây, nó là giá trị đề cập đến rằng nếu được thiết kế từ bên trong a , it inherits its priority. scheduler.yield() user-visible Prioritized Task Scheduling API postTask() scheduler.yield() postTask() Cách sử dụng scheduler.yield(). Một khi bạn hiểu làm thế nào tất cả mọi thứ hoạt động – các loại nhiệm vụ, vấn đề gây ra bởi các hoạt động chặn dài, và các ưu tiên, việc sử dụng trở nên đơn giản. Nhưng nó nên được sử dụng một cách khôn ngoan và thận trọng. Đây là một phiên bản cập nhật của chức năng sử dụng Bây giờ, thay vì Bạn chỉ cần gọi Và phần còn lại vẫn không thay đổi. scheduler.yield() heavyWork() scheduler.yield() setTimeout() await scheduler.yield() async function heavyWork() { // Yield to Main Thread to avoid UI blocking before heavy work await scheduler.yield() const data = Array.from({ length: 200 }, (_, i) => i) const result = [] // Interval at which execution will be yielded to the main thread (approx. ~ 25%). const yieldInterval = Math.ceil(data.length / 4) for (let i = 0; i < data.length; i++) { // Yield control to Main Thread to update UI and handle other tasks. if (i % yieldInterval === 0) { await scheduler.yield() } result.push(threadBlockingEnabled ? blockingTask(10) : data[i]) } return result } Khi một người dùng bắt đầu một chức năng sử dụng , sự khác biệt là ngay lập tức đáng chú ý. đầu tiên, các " " nút không dính, và thứ hai, người dùng nhấp vào sự kiện trên " nút được xử lý thành công, không chặn sự tương tác của người dùng với trang. heavyWork() scheduler.yield() OK Log Đó là, ban đầu, các chức năng đã được khởi động, và nút đã được tái tạo mà không dính.Trong khi nhiệm vụ nặng nề này đang được thực hiện, người dùng nhấn " " nút. sự kiện đã được xử lý thành công, và dữ liệu được in lên console. chức năng tiếp tục, và kết quả cuối cùng của nó đã được in trên console. Sau khi hoàn thành, người dùng nhấn " Nói tóm lại, bạn có thể cho trình duyệt của mình một break chỉ với một dòng. heavyWork() Log heavyWork() Log Demo ạ Mô tả chức năng Bây giờ bạn đã khám phá lý thuyết, hãy tiếp tục thực hành và xem một bản demo thực tế làm việc. Đây là một ứng dụng ngân hàng mô phỏng. Tất nhiên, nó là hư cấu và đơn giản hóa, nhưng nó chỉ nắm bắt đủ sự phức tạp của thế giới thực để giúp bạn hiểu cách chặn chủ đề ảnh hưởng đến tương tác, và làm thế nào Có thể giúp scheduler.yield() Đây là những gì người dùng nhìn thấy trong giao diện: – By default, the account balance is hidden behind a placeholder of asterisks. This is a familiar pattern in real banking apps, where sensitive information is hidden unless explicitly revealed by the user. A button labeled " " toggles visibility. Balance section Show balance – A visual representation of a bank card, shown front side by default, where some details are displayed: card type in the top left corner, last 4 digits of the card, the cardholder's name, and payment system, at the bottom right corner of the card. There are two buttons to the right of the card: Bank card – which flips the card when clicked. The back side of the card reveals sensitive card data like its full number, expiration date, and CVV code. Although the card number is generally not considered private information, some applications still prefer not to show the full number by default, but only if the user initiates it. However, I know and even use banks that generally do not allow you to see the bank card number in the application. Show card details – by clicking this button, this feature supposedly generates a list of transactions on the card and displays them in the table below. It imitates the real functionality where users can generate reports on bank card transactions. In reality, these reports can be complex tables with many customizable filters and the ability to download the report as a file. Such operations might involve heavy computations, process a huge amount of data, making them resource-intensive and time-consuming. For the sake of the demo, it's simplified. Under the hood, the " " button triggers the previously discussed function, which simply blocks the main thread using the function, which was also discussed above. After that, static mock transaction data is simply rendered into the table. Generate report Generate report heavyWork() blockingTask() Hành vi của ứng dụng có thể được tùy chỉnh bằng cách sử dụng các thiết lập khác nhau trên bảng điều khiển cấu hình ở bên trái. Bây giờ là thời gian để giải thích những gì nó làm: Main Thread blocking – xác định xem Main Thread sẽ bị chặn hay không. Trên thực tế, khi tùy chọn này được bật, chức năng blockingTask() được thực hiện. Scheduler.yield() – Xác định xem scheduler.yield() có được sử dụng hay không. Chiều dài mảng dữ liệu – Điều khiển số lượng các yếu tố được lặp lại bởi hàm heavyWork(). – Specifies how many milliseconds each element of the array takes to process. Blocking time duration Khoảng thời gian lợi nhuận – Xác định tần suất scheduler.yield() được gọi như thế nào, như một tỷ lệ phần trăm tiến bộ thông qua mảng. Đó là, con số này càng thấp, nó sẽ được gọi thường xuyên hơn. Trong các ví dụ trước đó, chúng tôi đã sử dụng một mảng 200-element với một sự chậm trễ 10ms và một khoảng thời gian 25% – một sự cân bằng tốt cho tác động có thể nhìn thấy mà không có sự chậm trễ quá mức. Với các tập dữ liệu lớn hơn, một khoảng thời gian nhỏ hơn thường tốt hơn. Biểu tình Sau khi sắp xếp tất cả các chức năng và cấu hình, hãy đi qua một kịch bản sử dụng thực tế và xem cách chặn chủ đề ảnh hưởng đến trải nghiệm người dùng. Block và Disable Chúng tôi cũng sẽ tăng chiều dài mảng một chút, vì vậy hoạt động nặng mất nhiều thời gian hơn, cho chúng tôi thời gian để quan sát các hiệu ứng. " button. Behind the scenes, this triggers the chức năng, trong đó xử lý 1000 nguyên tố, trong đó mỗi nguyên tố mất 10 millisecond. Main Thread scheduler.yield() Generate report heavyWork() Xem những gì xảy ra: The " " button stays stuck, it doesn't unpress, and the UI doesn't re-render.While the report is being generated, the user tries to click on the " “Sau đó” " nút, nhưng không có phản hồi. giao diện hoàn toàn đóng băng, không có hoạt hình, không có phản hồi, không có cảm giác tiến bộ. Đây là một ví dụ điển hình của một trải nghiệm người dùng xấu. Ứng dụng xuất hiện đóng băng, mặc dù về mặt kỹ thuật nó vẫn hoạt động. Người dùng không biết liệu phải chờ đợi hay tải lại trang. Generate report Show card details Show balance Hãy giải quyết những thiếu sót này bằng cách sử dụng bằng cách điều chỉnh một số cấu hình. Đây là cách cấu hình bây giờ trông như thế nào: vẫn bị chặn. Lần này, tùy chọn sử dụng kích hoạt. Chiều dài mảng được tăng nhẹ, chỉ để rõ ràng. Thời gian chặn vẫn giữ nguyên, 10 milliseconds. Khoảng thời gian đáp ứng được giảm xuống còn 5% để đáp ứng mượt mà hơn, vì chiều dài mảng đã được tăng lên. scheduler.yield() Main Thread scheduler.yield() scheduler.yield() Và bây giờ với cấu hình cập nhật, cùng một dòng người dùng trông hoàn toàn khác. điều đầu tiên bắt mắt là sau khi " " nút đã được nhấp, nó tái tạo một cách chính xác, và hoạt hình tải xuất hiện. Trong khi báo cáo đang được tạo ra, người dùng tương tác thành công với giao diện người dùng: họ có thể xoay thẻ và chuyển đổi số dư. Ứng dụng vẫn đáp ứng, ngay cả khi các hoạt hình ít mượt mà hơn một chút, đó là một bước tiến lớn so với đóng băng trước đó. Đây là một trải nghiệm tốt hơn nhiều. Người dùng được thông báo, trong kiểm soát, và không để lại đoán xem ứng dụng có hoạt động hay không. Tất nhiên, việc thực hiện thực tế có thể được tối ưu hóa hơn nữa, nhưng ngay cả ở dạng đơn giản này, sự khác biệt là đáng chú ý. Generate report scheduler.yield() Kết luận Vì vậy, hôm nay bạn đã học được về việc cho trình duyệt của bạn nghỉ ngơi, tầm quan trọng của việc đầu hàng để thực hiện các nhiệm vụ ưu tiên cao hơn, và những ưu và nhược điểm của các kỹ thuật này. có các khả năng khác không được đề cập trong bài viết này. nhưng mục tiêu của tôi là cung cấp cho bạn một nền tảng vững chắc, đủ để bắt đầu thử nghiệm, và đủ để bắt đầu suy nghĩ khác nhau về cách mã của bạn chơi với trình duyệt. Main Thread Prioritized Task Scheduling API Links hữu ích Trực tuyến demo – https://let-your-browser-take-a-breather.onrender.com/ Demo GitHub repository – https://github.com/WOLFRIEND/let_your_browser_take_a_breather Biểu đồ – https://drive.google.com/file/d/1FLKKPaseyypE3pVXXn7Cj0aWac3rCayn/view