Sử dụng requestIdleCallback

Nhiều trang web và ứng dụng có rất nhiều tập lệnh để thực thi. JavaScript của bạn thường cần được chạy càng sớm càng tốt, nhưng đồng thời bạn cũng không muốn nó cản trở người dùng. Nếu bạn gửi dữ liệu phân tích khi người dùng đang cuộn trang hoặc bạn nối các phần tử vào DOM trong khi họ nhấn vào nút, ứng dụng web của bạn có thể không phản hồi, dẫn đến trải nghiệm người dùng kém.

Sử dụng requestIdleCallback để lên lịch cho các công việc không cần thiết.

Tin vui là hiện đã có một API có thể giúp: requestIdleCallback. Tương tự như việc sử dụng requestAnimationFrame cho phép chúng tôi lên lịch ảnh động đúng cách và tăng tối đa khả năng đạt được 60 khung hình/giây, requestIdleCallback sẽ lên lịch công việc khi có thời gian rảnh ở cuối khung hình hoặc khi người dùng không hoạt động. Điều này có nghĩa là bạn có cơ hội làm việc mà không làm phiền người dùng. Tính năng này có trong Chrome 47, vì vậy, bạn có thể dùng thử ngay hôm nay bằng cách sử dụng Chrome Canary! Đây là một tính năng thử nghiệm và thông số kỹ thuật vẫn đang thay đổi, vì vậy, mọi thứ có thể thay đổi trong tương lai.

Tại sao tôi nên sử dụng requestIdleCallback?

Tự lên lịch cho công việc không cần thiết là việc rất khó. Không thể tìm ra chính xác thời gian kết xuất khung hình còn lại, vì sau khi lệnh gọi lại requestAnimationFrame thực thi, sẽ có các phép tính về kiểu, bố cục, vẽ và các thành phần nội bộ khác của trình duyệt cần chạy. Giải pháp tự phát triển không thể tính đến bất kỳ yếu tố nào trong số đó. Để đảm bảo rằng người dùng không tương tác theo một cách nào đó, bạn cũng cần đính kèm trình nghe vào mọi loại sự kiện tương tác (scroll, touch, click), ngay cả khi bạn không cần các trình nghe đó cho chức năng, chỉ để bạn có thể hoàn toàn chắc chắn rằng người dùng không tương tác. Mặt khác, trình duyệt biết chính xác lượng thời gian còn lại ở cuối khung và liệu người dùng có đang tương tác hay không. Vì vậy, thông qua requestIdleCallback, chúng ta có được một API cho phép chúng ta tận dụng mọi thời gian rảnh theo cách hiệu quả nhất có thể.

Hãy cùng tìm hiểu kỹ hơn về lớp này và xem cách sử dụng lớp này.

Đang kiểm tra requestIdleCallback

requestIdleCallback vẫn còn ở giai đoạn đầu, vì vậy, trước khi sử dụng, bạn nên kiểm tra để đảm bảo rằng bạn có thể sử dụng:

if ('requestIdleCallback' in window) {
    // Use requestIdleCallback to schedule work.
} else {
    // Do what you’d do today.
}

Bạn cũng có thể điều chỉnh hành vi của lớp này, yêu cầu quay lại setTimeout:

window.requestIdleCallback =
    window.requestIdleCallback ||
    function (cb) {
    var start = Date.now();
    return setTimeout(function () {
        cb({
        didTimeout: false,
        timeRemaining: function () {
            return Math.max(0, 50 - (Date.now() - start));
        }
        });
    }, 1);
    }

window.cancelIdleCallback =
    window.cancelIdleCallback ||
    function (id) {
    clearTimeout(id);
    }

Việc sử dụng setTimeout không phải là lựa chọn tốt vì hàm này không biết về thời gian rảnh như requestIdleCallback, nhưng vì bạn sẽ gọi hàm trực tiếp nếu không có requestIdleCallback, nên bạn không gặp bất kỳ vấn đề nào khi sử dụng phương thức này. Nếu có shim, nếu có requestIdleCallback, các cuộc gọi của bạn sẽ được chuyển hướng tự động. Điều này thật tuyệt.

Tuy nhiên, hiện tại, hãy giả định rằng lớp này tồn tại.

Sử dụng requestIdleCallback

Việc gọi requestIdleCallback rất giống với requestAnimationFrame ở chỗ hàm này lấy một hàm gọi lại làm tham số đầu tiên:

requestIdleCallback(myNonEssentialWork);

Khi myNonEssentialWork được gọi, đối tượng này sẽ được cung cấp một đối tượng deadline chứa hàm trả về một số cho biết thời gian còn lại cho công việc của bạn:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

Bạn có thể gọi hàm timeRemaining để lấy giá trị mới nhất. Khi timeRemaining() trả về giá trị 0, bạn có thể lên lịch một requestIdleCallback khác nếu vẫn còn công việc cần làm:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

Việc đảm bảo hàm của bạn sẽ được gọi

Bạn làm gì nếu mọi thứ thật sự bận rộn? Bạn có thể lo ngại rằng lệnh gọi lại của mình có thể không bao giờ được gọi. Mặc dù requestIdleCallback giống với requestAnimationFrame, nhưng cũng khác ở chỗ nó nhận một tham số thứ hai không bắt buộc: một đối tượng tuỳ chọn có thuộc tính thời gian chờ. Nếu được đặt, thời gian chờ này sẽ cho trình duyệt một khoảng thời gian tính bằng mili giây để thực thi lệnh gọi lại:

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

Nếu lệnh gọi lại của bạn được thực thi do hết thời gian chờ kích hoạt, bạn sẽ nhận thấy hai điều:

  • timeRemaining() sẽ trả về 0.
  • Thuộc tính didTimeout của đối tượng deadline sẽ là true.

Nếu thấy didTimeout là true, rất có thể bạn sẽ chỉ muốn chạy và hoàn tất tác vụ đó:

function myNonEssentialWork (deadline) {

    // Use any remaining time, or, if timed out, just run through the tasks.
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
            tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

Do thời gian chờ này có thể gây gián đoạn cho người dùng (công việc có thể khiến ứng dụng của bạn không phản hồi hoặc bị giật), hãy thận trọng khi đặt tham số này. Khi có thể, hãy để trình duyệt quyết định thời điểm gọi lệnh gọi lại.

Sử dụng requestIdleCallback để gửi dữ liệu phân tích

Hãy xem cách sử dụng requestIdleCallback để gửi dữ liệu phân tích. Trong trường hợp này, chúng ta có thể muốn theo dõi một sự kiện như – giả sử – nhấn vào trình đơn điều hướng. Tuy nhiên, vì chúng thường chạy động trên màn hình, nên chúng ta không muốn gửi ngay sự kiện này đến Google Analytics. Chúng ta sẽ tạo một mảng các sự kiện để gửi và yêu cầu gửi các sự kiện đó vào một thời điểm nào đó trong tương lai:

var eventsToSend = [];

function onNavOpenClick () {

    // Animate the menu.
    menu.classList.add('open');

    // Store the event for later.
    eventsToSend.push(
    {
        category: 'button',
        action: 'click',
        label: 'nav',
        value: 'open'
    });

    schedulePendingEvents();
}

Bây giờ, chúng ta sẽ cần sử dụng requestIdleCallback để xử lý mọi sự kiện đang chờ xử lý:

function schedulePendingEvents() {

    // Only schedule the rIC if one has not already been set.
    if (isRequestIdleCallbackScheduled)
    return;

    isRequestIdleCallbackScheduled = true;

    if ('requestIdleCallback' in window) {
    // Wait at most two seconds before processing events.
    requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
    } else {
    processPendingAnalyticsEvents();
    }
}

Tại đây, bạn có thể thấy tôi đã đặt thời gian chờ là 2 giây, nhưng giá trị này sẽ phụ thuộc vào ứng dụng của bạn. Đối với dữ liệu phân tích, bạn nên sử dụng thời gian chờ để đảm bảo dữ liệu được báo cáo trong một khung thời gian hợp lý thay vì chỉ tại một thời điểm nào đó trong tương lai.

Cuối cùng, chúng ta cần viết hàm mà requestIdleCallback sẽ thực thi.

function processPendingAnalyticsEvents (deadline) {

    // Reset the boolean so future rICs can be set.
    isRequestIdleCallbackScheduled = false;

    // If there is no deadline, just run as long as necessary.
    // This will be the case if requestIdleCallback doesn’t exist.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
    var evt = eventsToSend.pop();

    ga('send', 'event',
        evt.category,
        evt.action,
        evt.label,
        evt.value);
    }

    // Check if there are more events still to send.
    if (eventsToSend.length > 0)
    schedulePendingEvents();
}

Trong ví dụ này, tôi giả định rằng nếu không có requestIdleCallback thì dữ liệu phân tích sẽ được gửi ngay lập tức. Tuy nhiên, trong ứng dụng chính thức, bạn nên trì hoãn việc gửi bằng một thời gian chờ để đảm bảo không xung đột với bất kỳ hoạt động tương tác nào và gây ra hiện tượng giật.

Sử dụng requestIdleCallback để thực hiện các thay đổi DOM

Một trường hợp khác mà requestIdleCallback thực sự có thể giúp cải thiện hiệu suất là khi bạn cần thực hiện các thay đổi không cần thiết đối với DOM, chẳng hạn như thêm các mục vào cuối danh sách tải lười đang ngày càng phát triển. Hãy xem cách requestIdleCallback thực sự phù hợp với một khung hình thông thường.

Một khung hình thông thường.

Có thể trình duyệt sẽ quá bận nên không chạy được bất kỳ lệnh gọi lại nào trong một khung nhất định, vì vậy, bạn không nên dự kiến rằng sẽ có bất kỳ thời gian rảnh nào ở cuối khung để thực hiện thêm bất kỳ công việc nào khác. Điều đó khiến nó khác với những hàm như setImmediate, chạy trên mỗi khung hình.

Nếu lệnh gọi lại được kích hoạt ở cuối khung, thì lệnh gọi lại sẽ được lên lịch thực hiện sau khi khung hiện tại đã được xác nhận, tức là các thay đổi về kiểu sẽ được áp dụng và quan trọng là bố cục được tính toán. Nếu chúng ta thực hiện các thay đổi đối với DOM bên trong lệnh gọi lại khi rảnh, thì các phép tính bố cục đó sẽ không hợp lệ. Nếu có bất kỳ kiểu đọc bố cục nào trong khung tiếp theo, ví dụ: getBoundingClientRect, clientWidth, v.v., trình duyệt sẽ phải thực hiện Bố cục đồng bộ bắt buộc, đây là điểm tắc nghẽn tiềm ẩn của hiệu suất.

Một lý do khác không thể kích hoạt các thay đổi của DOM trong lệnh gọi lại ở trạng thái rảnh là tác động đến thời gian của việc thay đổi DOM là không thể dự đoán, và do đó chúng ta có thể dễ dàng vượt quá thời hạn mà trình duyệt đưa ra.

Phương pháp hay nhất là chỉ thực hiện các thay đổi đối với DOM bên trong lệnh gọi lại requestAnimationFrame, vì trình duyệt đã lên lịch cho loại công việc đó. Điều đó có nghĩa là mã của chúng ta sẽ cần sử dụng một mảnh tài liệu, sau đó có thể được nối thêm vào lệnh gọi lại requestAnimationFrame tiếp theo. Nếu đang sử dụng thư viện VDOM, bạn sẽ sử dụng requestIdleCallback để thực hiện các thay đổi, nhưng bạn sẽ áp dụng các bản vá DOM trong lệnh gọi lại requestAnimationFrame tiếp theo, chứ không phải lệnh gọi lại khi rảnh.

Do đó, hãy cùng xem mã:

function processPendingElements (deadline) {

    // If there is no deadline, just run as long as necessary.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    if (!documentFragment)
    documentFragment = document.createDocumentFragment();

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {

    // Create the element.
    var elToAdd = elementsToAdd.pop();
    var el = document.createElement(elToAdd.tag);
    el.textContent = elToAdd.content;

    // Add it to the fragment.
    documentFragment.appendChild(el);

    // Don't append to the document immediately, wait for the next
    // requestAnimationFrame callback.
    scheduleVisualUpdateIfNeeded();
    }

    // Check if there are more events still to send.
    if (elementsToAdd.length > 0)
    scheduleElementCreation();
}

Ở đây, tôi tạo phần tử và sử dụng thuộc tính textContent để điền vào phần tử đó, nhưng có thể mã tạo phần tử của bạn sẽ phức tạp hơn! Sau khi tạo phần tử, scheduleVisualUpdateIfNeeded sẽ được gọi. Thao tác này sẽ thiết lập một lệnh gọi lại requestAnimationFrame duy nhất, lần lượt sẽ thêm mảnh tài liệu vào phần nội dung:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
    // Append the fragment and reset.
    document.body.appendChild(documentFragment);
    documentFragment = null;
}

Mọi thứ đều tốt, giờ đây chúng ta sẽ thấy ít giật hơn khi thêm các mục vào DOM. Tuyệt vời!

Câu hỏi thường gặp

  • Có polyfill không? Đáng tiếc là không, nhưng có một shim nếu bạn muốn chuyển hướng minh bạch đến setTimeout. Lý do API này tồn tại là vì API này lấp đầy một khoảng trống rất thực tế trong nền tảng web. Việc suy luận thiếu hoạt động là rất khó, nhưng không có API JavaScript nào tồn tại để xác định lượng thời gian rảnh ở cuối khung hình, vì vậy tốt nhất bạn phải đưa ra các dự đoán. Bạn có thể dùng các API như setTimeout, setInterval hoặc setImmediate để lên lịch công việc, nhưng các API này không được định thời gian để tránh hoạt động tương tác của người dùng theo đúng cách của requestIdleCallback.
  • Điều gì sẽ xảy ra nếu tôi vượt quá thời hạn? Nếu timeRemaining() trả về giá trị 0 nhưng bạn chọn chạy lâu hơn, bạn có thể làm như vậy mà không sợ trình duyệt tạm dừng công việc của bạn. Tuy nhiên, trình duyệt đưa ra thời hạn để bạn cố gắng đảm bảo trải nghiệm mượt mà cho người dùng. Vì vậy, trừ phi có lý do chính đáng, bạn phải luôn tuân thủ thời hạn.
  • timeRemaining() có trả về giá trị tối đa không? Có, hiện tại là 50 mili giây. Khi cố gắng duy trì một ứng dụng thích ứng, tất cả phản hồi đối với hoạt động tương tác của người dùng phải được giữ dưới 100 mili giây. Trong hầu hết các trường hợp, nếu người dùng tương tác với cửa sổ 50 mili giây, nên cho phép hoàn tất lệnh gọi lại ở trạng thái rảnh và để trình duyệt phản hồi hoạt động tương tác của người dùng. Bạn có thể nhận được nhiều lệnh gọi lại khi rảnh được lên lịch liên tiếp (nếu trình duyệt xác định rằng có đủ thời gian để chạy các lệnh gọi đó).
  • Tôi có nên làm việc gì trong requestIdleCallback không? Lý tưởng nhất là công việc bạn làm phải được chia thành các phần nhỏ (microtask) có đặc điểm tương đối dễ dự đoán. Ví dụ: việc thay đổi DOM cụ thể sẽ có thời gian thực thi không thể dự đoán được, vì việc này sẽ kích hoạt các phép tính kiểu, bố cục, vẽ và kết hợp. Do đó, bạn chỉ nên thực hiện các thay đổi đối với DOM trong lệnh gọi lại requestAnimationFrame như đề xuất ở trên. Một điều khác cần lưu ý là việc phân giải (hoặc từ chối) Lời hứa, vì lệnh gọi lại sẽ thực thi ngay sau khi lệnh gọi lại ở trạng thái rảnh kết thúc, ngay cả khi không còn thời gian nào nữa.
  • Tôi có phải luôn nhận được requestIdleCallback ở cuối khung không? Không, không phải lúc nào cũng vậy. Trình duyệt sẽ lên lịch gọi lại bất cứ khi nào có thời gian rảnh ở cuối khung hoặc trong khoảng thời gian người dùng không hoạt động. Bạn không nên mong đợi lệnh gọi lại được gọi cho mỗi khung hình và nếu yêu cầu lệnh gọi lại chạy trong một khung thời gian nhất định, bạn nên sử dụng thời gian chờ.
  • Tôi có thể có nhiều lệnh gọi lại requestIdleCallback không? Có, bạn có thể, giống như bạn có thể có nhiều lệnh gọi lại requestAnimationFrame. Tuy nhiên, bạn cần nhớ rằng nếu lệnh gọi lại đầu tiên của bạn sử dụng hết thời gian còn lại trong lệnh gọi lại, thì sẽ không còn thời gian nào cho bất kỳ lệnh gọi lại nào khác. Sau đó, các lệnh gọi lại khác sẽ phải đợi cho đến khi trình duyệt ở trạng thái rảnh tiếp theo trước khi có thể chạy chúng. Tuỳ thuộc vào công việc bạn đang cố gắng hoàn thành, bạn nên có một lệnh gọi lại khi rảnh và chia công việc trong đó. Ngoài ra, bạn có thể sử dụng thời gian chờ để đảm bảo không có lệnh gọi lại nào bị thiếu thời gian.
  • Điều gì sẽ xảy ra nếu tôi đặt một lệnh gọi lại rảnh mới bên trong một lệnh gọi lại khác? Lệnh gọi lại khi rảnh mới sẽ được lên lịch chạy sớm nhất có thể, bắt đầu từ khung tiếp theo (thay vì khung hiện tại).

Bật chế độ rảnh!

requestIdleCallback là một cách tuyệt vời để đảm bảo bạn có thể chạy mã của mình mà không gây trở ngại cho người dùng. Nó dễ sử dụng và rất linh hoạt. Tuy nhiên, đây vẫn là giai đoạn đầu và thông số kỹ thuật chưa được hoàn thiện, vì vậy, chúng tôi rất mong nhận được ý kiến phản hồi của bạn.

Hãy dùng thử tính năng này trong Chrome Canary, thử nghiệm trên các dự án của bạn và cho chúng tôi biết cảm nhận của bạn!