这个性能优化场景,我敢打赌大多数性能优化文章没有提到!!

发布于:2024-05-10 ⋅ 阅读:(22) ⋅ 点赞:(0)

大家都知道浏览器是一个单线程的,大多数浏览器让一个单线程共用于执行JavaScript和更新用户界面。每个时刻只能执行其中一种操作,这意味着当JavaScript 代码正在执行时用户界面无法响应输入,反之亦然。当JavaScript 代码执行时,用户界面处于“锁定”状态。管理好JavaScript 的运行时间对 Web应用的性能非常重要。

浏览器UI线程执行过程

这是一个简单的交互,点击按钮,会在body尾部添加一个内容:

以上代码遵循以下流程:

  1. 添加按钮被点击时,它会触发UI线程来创建两个任务并添加到队列中。
  2. 第一个任务是更新按钮的UI,它需要改变外观以指示它被点击了。
  3. 第二个任务是执行JavaScript,包含handleClick()中的代码,唯一被运行的代码就是这个方法和所有被它调用的方法。
  4. 假设UI线程处于空闲状态,那么第一个任务被提取出来执行更新按钮的外观。
  5. 然后JavaScript任务被提取出来并执行。
  6. 在运行过程中,handleClick() 创建了一个新的<div>元素并把它附加在<body>元素末尾。
  7. 第六步导致引发了另一次UI变化。
  8. 一个新的UI更新任务被添加在队列中,即当JavaScript 运行完之后,UI还会再更新一次。

用流程图表示:

浏览器UI线程示例图.png

根据以上流程,当第5步执行JavaScript的时间过久,界面就得不到更新,就会有卡顿的现象,优化点就是这里。

优化点

Nielsen指出如果界面在100 毫秒内响应用户输入,用户会认为自己在“直接操纵界面中的对象”。超过100毫秒意味着用户会感到自己与界面失去联系。由于JavaScript 运行时无法更新UI,所以如果JavaScript 运行时间超过100 毫秒,用户就会感觉失去了对界面的控制。

那这种JavaScript执行过程中,在哪些业务场景下会有呢?这里我列举几个场景:

  • 大量数据计算和处理
  • 复杂的渲染逻辑
  • 第三方库和框架的渲染和初始化
  • 长任务队列
  • 图像和视频处理

如果这种业务场景不进行性能优化,那最终结果是一个失去响应的UI,表现为“挂起”或“假死”状态。

那怎么优化JavaScript的处理时长呢?

方案

如果JavaScript任务执行过长,那么我们可以限制JavaScript任务在100毫秒或更短的时间内完成,以避免类似情况出现。但是有时候,尽管你尽了最大努力,但难免会有一些复杂的JavaScript 任务不能在100 毫秒或更短时间内完成。这个时候,最理想的方法是让出UI线程的控制权,使得UI可以更新。让出控制权意味着停止执行JavaScript, 使UI线程有机会更新,然后再继续执行JavaScript。于是我们可以用JavaScript的定时器来让出时间片段。

setTimeout 是 JavaScript 中使用非常广泛的一个函数,它允许我们在指定的延迟之后执行一段代码。这是一个异步操作,因为它不会立即执行代码,而是将代码的执行推迟到等待的时间过去之后,然后将需要执行的代码加到任务队列的后面。

使用定时器处理数组

常见的一种造成长时间运行脚本的起因就是耗时过长的循环。我们可以通过选用定时器。它的基本方法是把循环的工作分解到一系列定时器中。

我们可以写一个工具函数来处理长任务数组

  function timedProcessArray(items, process, callback) {
    const todo = items.concat(); // 克隆原始数组
    setTimeout(function() {
      const start = +new Date();
      do {
        process(todo.shift());
      } while (todo.length > 0 && (+new Date() - start < 50));
      if (todo.length > 0) {
        setTimeout(arguments.callee, 25);
      } else {
        callback(items);
      }
    }, 25);
  }

这个模式的基本思路是创建一个原始数组的克隆,并将它作为数组项队列来处理。第一次调用setTimeout() 创建一个定时器处理数组中的第一个条目。调用todo.shift()返回它的第一个条目然后把它从数组中删除。这个值作为参数传给process()。处理完后,检查是否还有更多条目需要处理。如果todo 数组中还有条目,那么就再启动一个定时器。因为下一个定时器需要运行相同的代码,所以第一个参数为arguments.callee。该值指向当前正在运行的匿名函数。如果不再有条目需要处理,那么调用callback() 函数。

上面的工具函数接受三个参数:待处理的数组,对每一个数组项调用的函数,处理完成后运行的回调函数。该函数的用法如下:

const items = [ 123789323778232654219 543321, 160];
function outputValue(value){
    console.log(value);
}
processArray(items, outputValue, function(){
    console.1og("Done!");
});
  

上面的工具函数有以下限制:

  • 处理过程是否必须同步?
  • 数据是否必须按顺序处理?

如果这两个问题都是“否”,那么代码将使用于定时器分解任务。

我们可以模拟下长任务处理数组的场景:

结论

如果你在工作中有遇到类似于这种场景,那就赶紧将这个函数添加到你的utils里吧。

本文参考 高性能JavaScript 第六章节 快速响应的用户界面。


网站公告

今日签到

点亮在社区的每一天
去签到