添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
它被称为 主线程 是有原因的:它是您编写的 几乎所有 JavaScript 工作的 唯一线程

主线程一次只能处理一个任务。
当任务超过某个点时——准确地说是 50 毫秒 ——它们被归类为 长任务
如果用户在运行较长的任务时尝试与页面交互——或者如果需要进行重要的渲染更新——浏览器将 延迟 处理该工作。
这会导致交互或渲染 延迟

交互阻塞 / 渲染阻塞

Chrome 性能分析器中描述的一项长任务。
长任务 由任务角落的 红色三角形 表示,任务的 阻塞部分 对角线 红色 条纹图案 填充。

将一项长任务 分解 为更小的任务,这些任务单独运行所需的时间更少。

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

当任务太长并且浏览器不能足够快地响应交互时交互发生的情况的 可视化 ,以及当较长的任务被分解成较小的任务时的情况。

在上图的顶部,由用户交互排队的事件处理程序必须 等待 一个长任务才能运行,这会 延迟 交互的发生。
在底部,事件处理程序有机会 更快地 运行。
因为事件处理程序有机会在 较小的任务之间 运行,所以它比必须 等待 长任务完成 的情况 运行得更快
在上面的示例中,用户可能已经注意到 延迟
在底部,交互可能是 即时 的。

任务管理策略

软件架构 中的一个常见建议是将您的 工作分解 更小的功能
这为您带来了更好的 代码可读性 项目可维护性 的好处。
这也使得 测试 更容易编写。

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

在此示例中,有一个名为的函数 saveSettings()调用其中的五个函数来完成工作,例如验证表单、显示微调器、发送数据等。
从概念上讲,这是精心设计的。
如果您需要调试其中一个函数,您可以遍历项目树来找出每个函数的作用。

然而,问题是 JavaScript 不会将这些函数中的每一个都作为单独的任务运行,因为它们是在函数 saveSettings() 内执行的。
这意味着所有五个函数都作为一个任务运行。

在最好的情况下,即使只是其中一个函数也可以为任务的总长度贡献 50 毫秒或更多时间。
在最坏的情况下,更多的这些任务可以运行更长的时间——尤其是在资源受限的设备上。
接下来是一组策略,您可以使用这些策略来分解任务和确定任务的优先级

  • 手动延迟代码执行 setTimeout
  • function saveSettings () {
      // Do critical work that is user-visible:
      validateForm();
      showSpinner();
      updateUI();
      // Defer work that isn't user-visible to a separate task:
      setTimeout(() => {
        saveToDatabase();
        sendAnalytics();
      }, 0);
    

    如果您有一系列需要按顺序运行函数,这种方法很有效,但您的代码可能并不总是以这种方式组织。

    除了setTimeout(),还有一些其他 API 允许您将代码执行推迟到后续任务。
    一种涉及使用postMessage()更快的超时。您还可以使用requestIdleCallback()- 但要小心
    requestIdleCallback() 以尽可能低的优先级安排任务,并且仅在浏览器空闲期间安排任务。
    当主线程拥塞时,调度的任务 requestIdleCallback()可能永远无法运行

    https://dbaron.org/log/20100309-faster-timeouts

  • 使用 async/await 创建屈服点
  • 当您屈服于主线程时,您将有机会处理比当前排队的任务更重要的任务。
    理想情况下,只要有一些重要的面向用户的工作需要比不让步更快地执行,就应该让步到主线程。
    屈服于主线程为关键工作更快运行创造了机会。

    当任务被分解时,其他任务可以通过浏览器的内部优先级方案更好地排列优先级。
    屈服于主线程的一种方法涉及使用Promise通过调用解析的 a 的组合setTimeout():

    function yieldToMain () {
      return new Promise(resolve => {
        // 使用 setTimeout 生成一个新的宏任务
        setTimeout(resolve, 0);
    

    虽然此代码示例返回一个Promise在调用后解析的setTimeout(),但它不是Promise负责在新任务中运行其余代码的 ,而是调用setTimeout()。

    Promise 回调作为微任务而不是任务(宏任务)运行,因此不会屈服于主线程。

    在saveSettings()函数中,如果在每次函数调用后调用函数,则可以在每次工作后让位于主await线程yieldToMain():

    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();
    

    您不必在每次函数调用后都让步。
    例如,如果您运行两个导致对用户界面进行重要更新的函数,您可能不希望在它们之间进行让步。
    如果可以,让该工作先运行,然后考虑在执行不太重要的功能或用户看不到的后台工作之间让步。

    结果是曾经的整体任务现在被分解成单独的任务

    该 saveSettings()函数现在将其子函数作为单独的任务执行。

  • 仅在必要时产生
  • 如果您有一堆任务,但您只想在用户尝试与页面交互时让步怎么办?isInputPending()这就是为之而生的东西。

    https://web.dev/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()返回,将调用以便处理用户输入。否则,它将把下一个任务从队列的前面移开并连续运行。
    它将执行此操作,直到没有更多任务为止。truesaveSettings()yieldToMain()

    saveSettings()为五个任务运行任务队列,但用户在第二个工作项运行时单击打开菜单。
    isInputPending()让主线程处理交互,并恢复运行其余任务。

    isInputPending()可能不会总是true在用户输入后立即返回。
    这是因为操作系统需要时间来告诉浏览器交互发生了。
    这意味着其他代码可能已经开始执行(如您在上面屏幕截图中的函数所见saveToDatabase())。
    即使您使用isInputPending()它,限制您在每个功能中所做的工作量仍然很重要。

    与让步机制结合使用isInputPending()是让浏览器停止其正在处理的任何任务的好方法,以便它可以响应关键的面向用户的交互。
    这有助于提高您的页面在许多情况下响应用户的能力,当许多任务正在进行时。

    另一种使用方法isInputPending()——特别是如果您担心为不支持它的浏览器提供回退——是结合使用基于时间的方法和可选的链接运算符:

    https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Optional_chaining

    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()使用这种方法,您可以通过使用(和调整)截止日期的基于时间的方法来获得不支持的浏览器的回退,以便在必要时分解工作,无论是通过屈服于用户输入,还是通过某个时间点。

    当前 API 中的差距

    到目前为止提到的 API 可以帮助您分解任务,但它们有一个明显的缺点:当您通过延迟代码后续任务中运行而屈服于主线程时,该代码将被添加到任务队列最后

    如果您控制页面上的所有代码,则可以创建自己的调度程序并能够确定任务的优先级,但第三方脚本不会使用您的调度程序。
    实际上,您无法真正确定在此类环境中工作的优先级
    您只能将其分块,或明确屈服于用户交互

    幸运的是,目前正在开发的专用调度程序 API 可以解决这些问题。

  • 专用的调度程序 API
  • 调度程序 API 目前提供的postTask()功能,在撰写本文时,在 Chromium 浏览器和 Firefox 中可用。
    postTask()允许更细粒度任务调度,并且是帮助浏览器确定工作优先级以便低优先级任务让步给主线程的一种方法。
    postTask()使用承诺,并接受priority设置。

    postTask() API 具有三个您可以使用的优先级

    'background'对于最低优先级的任务。
    'user-visible'用于中优先级任务。priority如果没有设置,这是默认值
    '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'});
    

    在这里,任务优先级的安排方式使得浏览器优先级任务(例如用户交互)可以按自己的方式进行。

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

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

    postTask()并非所有浏览器都支持。您可以使用特征检测来查看它是否可用,或者考虑使用polyfill。

    https://www.npmjs.com/package/scheduler-polyfill

    https://wicg.github.io/scheduling-apis/

  • 带有延续的内置收益
  • 目前尚未在任何浏览器中实现的调度程序 API 的一个建议部分是内置的屈服机制。
    它的使用类似于yieldToMain()本文前面演示的函数:

    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 with the scheduler
        // API's own yielding mechanism:
        await scheduler.yield();
    

    您会注意到上面的代码很熟悉,但是yieldToMain()您没有使用 ,而是调用了 and await scheduler.yield()。

    不屈服、有屈服、有屈服和延续的任务执行的可视化。使用时scheduler.yield(),即使在屈服点之后,任务执行也会从中断的地方开始。

    的好处scheduler.yield()是continuation,就是说如果你在一组task中间yield了,其他定时任务在yield点之后会按照同样的顺序继续。这可以避免来自第三方脚本的代码篡夺代码的执行顺序。

    管理任务具有挑战性,但这样做有助于您的页面更快地响应用户交互。
    没有一条关于管理任务和确定任务优先级的建议。
    相反,它是许多不同的技术。
    重申一下,这些是您在管理任务时需要考虑的主要事项:

    主线程执行关键的、面向用户的任务。
    isInputPending()当用户试图与页面交互时,用于屈服于主线程。
    优先处理任务postTask()。
    最后,在你的函数中做尽可能少的工作。
    使用这些工具中的一个或多个,您应该能够构建应用程序中的工作,以便优先考虑用户的需求,同时确保仍然完成不太重要的工作。
    这将创造更好的用户体验响应速度更快,使用起来更愉快。

    图片性能优化

    https://web-dev.imgix.net/image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/NOVR7JgJ8sMM7Fhc0tzo.png

    指定宽度格式

    https://web-dev.imgix.net/image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/NOVR7JgJ8sMM7Fhc0tzo.png?auto=format&w=845

    webp vs png

    https://cdn.xgqfrms.xyz/video/image-rendering%402x.mp4

    Event loop / 事件循环

    Stack: (后进先出,入栈,出栈),值类型,函数调用栈
    Heap: ,对象,引用类型
    Queue: 队列(先进先出,入队,出队),宏任务队列,微任务任务,消息队列

    task queue 任务队列

    macrotask 宏任务:
    setTimeout, setInterval, setImmediate, requestAnimationFrameset, requestIdleCallback
    clearTimeout, clearInterval, clearImmediate, cancelAnimationFrame
    I/O, UI rendering

    microtask 微任务:
    Promise(.then/.catch/.finally), Async / Await, queueMicrotask,
    MutationObserver,IntersectionObserver, PerformanceObserver, ResizeObserver
    process.nextTick

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#run-to-completion

    Web APIs

    https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
    https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout

    https://developer.mozilla.org/en-US/docs/Web/API/setInterval
    https://developer.mozilla.org/en-US/docs/Web/API/clearInterval

    https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate
    https://developer.mozilla.org/en-US/docs/Web/API/Window/clearImmediate

    https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
    https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame

    https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback
    https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelIdleCallback

    https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
    https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth

    https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

    https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
    https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
    https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
    https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver

    https://codepen.io/xgqfrms/pen/oNPYdLQ?editors=1011

    (🐞 反爬虫测试!打击盗版⚠️)如果你看到这个信息, 说明这是一篇剽窃的文章,请访问 https://www.cnblogs.com/xgqfrms/ 查看原创文章!

    https://web.dev/optimize-long-tasks/?utm_source=xgqfrms.xyz