Why requestIdleCallback?

On the web, there are many tasks that are time-consuming but not as urgent. They share event queues with critical tasks, such as those that respond promptly to user input. If the two conflict, the user experience will be terrible. We can use setTimout to defer processing of these tasks. However, we do not know if setTimeout performs the callback when the browser is idle.

RequestIdleCallback solves this pain point, requestIdleCallback is at the end of the frame and has free time. Or if the user does not interact with the web page, the callback is executed.

Introduction of requestIdleCallback API

  • The first argument to requestIdleCallback is callback
    • When callback is called, the callback takes an argument, Deadline, which is an object with two attributes
      • TimeRemaining, the timeRemaining attribute is a function whose return value represents how much time is left in the current idle time
      • DidTimeout. The didTimeout attribute is a Boolean value. If didTimeout is true, this callback is executed because of a timeout
  • The second parameter to requestIdleCallback is Options
    • Options is an object that can be used to configure a timeout

requestIdleCallback((deadline) = > {
    // deadline.timeremaining () Returns the remaining time of the current idle time
    if (deadline.timeRemaining() > 0) {
        task()
    }
}, {
    timeout: 500
})
Copy the code

Free time

The callback of requestIdleCallback runs at idle time in the browser, so what is idle time?

As shown above. When we perform a continuous animation, the first frame is rendered on the screen, and the time between the second frame and the start of rendering is idle. This idle time can be very short if our screen is 60 Hz (the screen refreshes 60 times in 1s). Then the idle time will be less than 16ms (1000ms / 16).

The other kind of idle time is when the user is idle (not interacting with the web page) and there is no on-screen animation. The idle time is infinite. However, to avoid unpredictable events (sudden user interaction with the web page), idle time should be limited to a maximum of 50ms.

Why is the maximum 50ms? Humans perceive responses within 100ms as instantaneous. The purpose of limiting the idle time to 50ms is to avoid the execution of tasks during the idle time, which leads to the blocking of user operation response and the significant response lag of the user.

During idle periods, callbacks are executed in FIFO (first-in, first-out) order. However, if a callback is executed consecutively in idle time, one callback will be executed at the next idle time.

const task1 = (a)= > console.log('Execute Task 1')
const task2 = (a)= > console.log('Task 2')
const task3 = (a)= > console.log('Task 3')

// console
// Perform task 1
// Perform task 2
// Perform task 3
requestIdleCallback(task1)
requestIdleCallback(task2)
requestIdleCallback(task3)
Copy the code

If the execution time of the current task exceeds the remaining time in the current idle time period, we can also carry the task to the next idle time period. After the next idle cycle begins, the newly added callback is added to the end of the callback list.


const startTask = (deadline) {
    // 如果 `task` 花费的时间是20ms
    // If the number of milliseconds remaining in the current idle time is exceeded, we wait until the next idle time to execute the task
    if (deadline.timeRemaining() <= 20) {
        // Take the task to the next idle period
        // Add to the end of the callback list for the next idle period
        requestIdleCallback(startTask)
    } else {
        // Execute the task
        task()
    }
}
Copy the code

When our page is in an invisible state (such as switching to another tag), our idle time will trigger an idle period every 10 seconds.

timeout

If timeout is specified, but the browser does not execute the callback within the specified time. Callback is enforced the next time it is idle. And the argument to callback, deadline.didTimeout equals true, and deadline.timeremaining () returns 0.


requestIdleCallback((deadline) = > {
    // true
    console.log(deadline.didTimeout)
}, {
    timeout: 1000
})

// This operation takes about 5000ms
for (let i = 0; i < 3000; i++) {
    document.body.innerHTML = document.body.innerHTML + `<p>${i}</p>`
}
Copy the code

RequestIdleCallback Practice: Make dots in requestIdleCallback

Using requestIdleCallback to delay the reporting of data can avoid some render blocking.


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <input type="text" id="text" />
</body>
<script>
    const datas = []
    const text = document.getElementById('text')
    let isReporting = false

    function sleep (ms = 100) {
        let sleepSwitch = true
        let s = Date.now()
        while (sleepSwitch) {
            if (Date.now() - s > ms) {
                sleepSwitch = false}}}function handleClick () {
        datas.push({
            date: Date.now()
        })
        // a function that listens for user responses, which takes 150ms
        sleep(150)
        handleDataReport()
    }

    / / = = = = = = = = = = = = = = = = = = = = = = = = = use requestIdleCallback = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    function handleDataReport () {
        if (isReporting) {
            return
        }
        isReporting = true
        requestIdleCallback(report)
    }

    function report (deadline) {
        isReporting = false
        while (deadline.timeRemaining() > 0 && datas.length > 0) {
            get(datas.pop())
        }
        if (datas.length) {
            handleDataReport()
        }
    }

    / / = = = = = = = = = = = = = = = = = = = = = = = = = use requestIdleCallback end = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    function get(data) {
        // Data reporting function, which costs 20ms
        sleep(20)
        console.log('~~~ data report ~~~:${data.date}`)
    }

    text.oninput = handleClick
</script>
</html>
Copy the code

If requestIdleCallback is not used and data is reported directly, the main thread will be blocked and browser rendering will be affected.



      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <input type="text" id="text" />
</body>
<script>
    const datas = []
    const text = document.getElementById('text')
    let isReporting = false

    function sleep (ms = 100) {
        let sleepSwitch = true
        let s = Date.now()
        while (sleepSwitch) {
            if (Date.now() - s > ms) {
                sleepSwitch = false}}}function handleClick () {
        datas.push({
            date: Date.now()
        })
        // a function that listens for user responses, which takes 150ms
        sleep(150)
        handleDataReport()
    }

    / / = = = = = = = = = = = = = = = = = = = = = = = = = don't use requestIdleCallback = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    function handleDataReport () {
        if (isReporting) {
            return
        }
        isReporting = true
        report()
    }

    function report (deadline) {
        isReporting = false
        while (datas.length > 0) {
            get(datas.pop())
        }
        if (datas.length) {
            handleDataReport()
        }
    }

    / / = = = = = = = = = = = = = = = = = = = = = = = = = don't use requestIdleCallback end = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    function get(data) {
        // Data reporting function, which costs 20ms
        sleep(20)
        console.log('~~~ data report ~~~:${data.date}`)
    }

    text.oninput = handleClick
</script>
</html>
Copy the code

Cause analysis:

If requestIdleCallback is used:

Monitor Event Processing –> Page Rendering –> Data reporting (idle) –> Monitor Event Processing –> Page rendering –> Data reporting (idle)

If not using requestIdleCallback:

Listen event Processing –> Data reporting (added to main thread) –> Listen event Processing –> Data reporting (added to main thread) –> Page rendering

Common Q&A

Q1: Will requestIdleCallback be executed at the end of each frame?

A1: requestIdleCallback is only executed when there is free time at the end of the frame and should not be expected to be executed at the end of each frame.

😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂

Q2: What operations do not fit into the callback of a requestIdleCallback?

A2: Update the DOM, and the Promise callback (which timed out the frame). Look at the code below. The code in requestIdleCallback should be small pieces of code that can predict the execution time.


// console
// Free time 1
// Wait for 1000ms
// Free time 2
// A Promise will be executed immediately after it is accepted at idle time 1, even if there is no idle time left. The delay in moving to the next frame

requestIdleCallback((a)= > {
    console.log('Idle time 1')
    Promise.resolve().then((a)= > {
        sleep(1000)
        console.log('Waiting for 1000ms')
    })
})

requestIdleCallback((a)= > {
    console.log('Free time 2')})Copy the code

reference

  • Using requestIdleCallback
  • Cooperative Scheduling of Background Tasks