性能优化指标分析

发布时间 2023-10-17 09:51:26作者: 长安城下翩翩少年

原则

  1. 不要阻塞主线程
  2. 将长任务进行拆分 (大图切片,因为image.decode 大图会形成长任务)

Js 单线程 与长任务
主线程是浏览器中大多数任务运行的地方。它被称为主线程是有原因的:几乎所有编写的JavaScript都在主线程中工作。
主线程一次只能处理一个任务。当任务持续时间超过某一点(确切地说是50毫秒)时,它们被归类为长任务。如果用户在运行较长时间的任务时试图与页面交互,或者需要进行重要的呈现更新,则浏览器将延迟处理该工作。这会导致交互或呈现延迟。

因为当任务被分解时,浏览器有更多的机会来响应高优先级的工作——包括用户交互。

任务管理策略
软件架构中一个常见的建议是将工作分解成更小的功能。这为您提供了更好的代码可读性和项目可维护性的好处。这也使得编写测试更容易。

function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}

在这个示例中,有一个名为saveSettings()的函数,它调用其中的五个函数来完成工作,例如验证表单、显示微调器、发送数据等等。从概念上讲,这是很好的架构。如果需要调试其中一个函数,可以遍历项目树以找出每个函数的功能。
然而,问题在于JavaScript并没有将这些函数作为单独的任务运行,因为它们是在saveSettings()函数中执行的。这意味着所有五个函数都作为一个任务运行。
JavaScript以这种方式工作是因为它使用了任务执行的运行到完成模型。这意味着每个任务将运行到完成为止,而不管它阻塞主线程多长时间。
在最好的情况下,即使这些函数中的一个也可以为任务的总长度贡献50毫秒或更多。在最坏的情况下,更多这样的任务可能会运行相当长的时间——尤其是在资源有限的设备上。下面是一组你可以用来分解任务并划分优先级的策略。

  • 手动延迟代码执行
    开发人员用来将任务分解为更小的任务的一种方法是setTimeout()。使用这种技术,可以将函数传递给setTimeout()。这将延迟回调到单独任务的执行,即使您指定的超时为0。在这里使用setTimeout()是有问题的,因为它的人体工程学使其难以实现,而且整个数据数组可能需要很长时间来处理,即使每个项都可以非常快速地处理。综上所述,setTimeout()并不是该作业的合适工具——至少在以这种方式使用时不是。

除了setTimeout()之外,还有其他一些api允许您将代码执行推迟到后续任务。其中一个涉及使用postMessage()来实现更快的超时。您还可以使用requestIdleCallback()来分解工作,但要注意! -requestIdleCallback()以尽可能低的优先级调度任务,并且仅在浏览器空闲时间调度任务。当主线程发生拥塞时,使用requestIdleCallback()调度的任务可能永远无法运行。

  • 使用async/await创建让步点
    当您让位于主线程时,您就给了主线程一个处理比当前正在排队的任务更重要的任务的机会。理想情况下,当您有一些重要的面向用户的工作需要更快地执行时,您应该让位于主线程。服从主线程可以让关键工作更快地运行。
    当任务被分解时,其他任务可以通过浏览器的内部优先级计划更好地进行优先级排序。向主线程屈服的一种方法是使用Promise的组合,它通过调用setTimeout()来解决:
    function yieldToMain () {
    return new Promise(resolve => {
    setTimeout(resolve, 0);
    });
    }
    虽然这个代码示例返回一个在调用setTimeout()后解决的Promise,但并不是Promise负责在新任务中运行其余代码,而是setTimeout()调用。Promise回调函数作为微任务而不是任务运行,因此不会屈服于主线程。
    async function saveSettings () {
    // Create an array of functions to run:
    const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
    ]

    // Loop over the tasks:
    while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread:
    await yieldToMain();
    }
    }
    您不必在每次函数调用后都让步。例如,如果运行两个函数导致用户界面的重要更新,您可能不希望在它们之间让步。如果可以的话,让这些工作先运行,然后考虑在那些不太重要的功能或用户看不到的背景工作之间做出让步。

使用基于承诺的方法而不是手动使用setTimeout()的好处是更好的人机工程学。让步点变得具有声明性,因此更容易编写、阅读和理解。

  • Yield only when necessary 必要时才让步
    如果您有一堆任务,但您只想在用户试图与页面交互时让步,该怎么办?这就是isInputPending()的用途。
    isInputPending()是一个可以在任何时候运行的函数,以确定用户是否试图与页面元素交互:调用isInputPending()将返回true。否则返回false。
    假设您有一个需要运行的任务队列,但您不想妨碍任何输入。这段代码使用了isInputPending()和自定义的yieldToMain()函数,确保了当用户试图与页面交互时,输入不会被延迟:
    async function saveSettings () {
    // A task queue of functions
    const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
    ];

    while (tasks.length > 0) {
    // Yield to a pending user input:
    if (navigator.scheduling.isInputPending()) {
    // There's a pending user input. Yield here:
    await yieldToMain();
    } else {
    // Shift the task out of the queue:
    const task = tasks.shift();

    // Run the task:
    task();
    

    }
    }
    }
    当saveSettings()运行时,它将遍历队列中的任务。如果isInputPending()在循环期间返回true, saveSettings()将调用yieldToMain()以便处理用户输入。否则,它将把下一个任务移出队列的前面,并持续运行它。它将这样做,直到没有更多的任务。

saveSettings()为五个任务运行一个任务队列,但是当第二个工作项正在运行时,用户已经单击打开了一个菜单。isInputPending()让位于主线程以处理交互,并继续运行其余的任务。

isInputPending()可能并不总是在用户输入后立即返回true。这是因为操作系统需要时间来告诉浏览器发生了交互。这意味着其他代码可能已经开始执行(正如您可以在上面的屏幕截图中通过saveToDatabase()函数看到的那样)。即使使用isInputPending(),限制在每个函数中执行的工作量仍然至关重要。
将isInputPending()与让步机制结合使用是让浏览器停止它正在处理的任何任务的好方法,这样它就可以响应重要的面向用户的交互。在许多情况下,当大量任务在运行时,这有助于提高页面对用户的响应能力。
使用isInputPending()的另一种方法——特别是当你担心为不支持它的浏览器提供一个回退时——是将基于时间的方法与可选的链接操作符结合使用:

async function saveSettings () {
// A task queue of functions
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
];

let deadline = performance.now() + 50;

while (tasks.length > 0) {
// Optional chaining operator used here helps to avoid
// errors in browsers that don't support isInputPending:
if (navigator.scheduling?.isInputPending() || performance.now() >= deadline) {
// There's a pending user input, or the
// deadline has been reached. Yield here:
await yieldToMain();

  // Extend the deadline:
  deadline = performance.now() + 50;

  // Stop the execution of the current loop and
  // move onto the next iteration:
  continue;
}

// Shift the task out of the queue:
const task = tasks.shift();

// Run the task:
task();

}
}

有了这种方法,对于不支持isInputPending()的浏览器,您可以使用基于时间的方法,使用(并调整)一个截止日期,以便在必要时分解工作,无论是根据用户输入,还是根据某个时间点。
Gaps in current APIs
到目前为止所提到的api可以帮助您分解任务,但它们有一个显著的缺点:当您通过延迟代码以在后续任务中运行而让位于主线程时,该代码将被添加到任务队列的最末端。

如果您控制页面上的所有代码,则可以创建自己的调度器,并能够对任务进行优先级排序,但第三方脚本不会使用您的调度器。实际上,在这样的环境中,你真的无法分清工作的轻重缓急。你只能把它分成若干块,或者明确地让位于用户交互。

幸运的是,目前正在开发一个专门的调度器API来解决这些问题。
A dedicated scheduler API
调度器API目前提供postTask()函数,在撰写本文时,该函数在Chromium浏览器和Firefox的一个标志后面可用。postTask()允许更细粒度的任务调度,并且是帮助浏览器对工作进行优先级排序的一种方法,以便将低优先级的任务让位给主线程。postTask()使用promise,并接受优先级设置。

postTask() API有三个优先级可以使用:
如果您控制页面上的所有代码,则可以创建自己的调度器,并能够对任务进行优先级排序,但第三方脚本不会使用您的调度器。实际上,在这样的环境中,你真的无法分清工作的轻重缓急。你只能把它分成若干块,或者明确地让位于用户交互。
'background'用于最低优先级的任务。
'user-visible'中等优先级任务的“用户可见”。如果没有设置优先级,这是默认值。
'user-blocking'用于需要高优先级运行的关键任务。
以下面的代码为例,其中postTask() API用于运行三个可能优先级最高的任务,其余两个任务以可能优先级最低的任务。
function saveSettings () {
// Validate the form at high priority
scheduler.postTask(validateForm, {priority: 'user-blocking'});

// Show the spinner at high priority:
scheduler.postTask(showSpinner, {priority: 'user-blocking'});

// Update the database in the background:
scheduler.postTask(saveToDatabase, {priority: 'background'});

// Update the user interface at high priority:
scheduler.postTask(updateUI, {priority: 'user-blocking'});

// Send analytics data in the background:
scheduler.postTask(sendAnalytics, {priority: 'background'});
};
在这里,任务的优先级是通过浏览器优先级任务(例如用户交互)来安排的。

当运行savessettings()时,该函数使用postTask()来调度各个函数。关键的面向用户的工作被安排在高优先级,而用户不知道的工作被安排在后台运行。这使得用户交互执行得更快,因为工作被适当地分解和优先级。

这是一个使用postTask()方法的简单例子。可以实例化不同的TaskController对象,这些对象可以在任务之间共享优先级,包括根据需要更改不同TaskController实例的优先级。

总结
管理任务是具有挑战性的,但这样做可以帮助页面更快地响应用户交互。对于管理和安排任务的优先级并没有一个单一的建议。相反,它是一些不同的技术。重申一下,这些是管理任务时需要考虑的主要事项。

让主线程处理关键的、面向用户的任务。
当用户试图与页面交互时,使用navigator.scheduling.isInputPending()将任务交给主线程处理。
使用postTask()确定任务的优先级。
最后,在函数中做尽可能少的工作。
使用一个或多个这样的工具,您应该能够组织应用程序中的工作,以便它能够优先满足用户的需求,同时确保不那么重要的工作仍然可以完成。这将创建一个更好的用户体验,响应更快,使用更愉快。