【性能优化】怎样去优化Long Task
相关文章
怎样去优化Long Task
一、前言
在web前端中,我们总是会听说不要阻塞主线程,我们要打断Long Task的运行。做这些事情的意义到底在哪里?
如果我们看过很多关于web 性能优化的文章,我们会发现保持我们的javascript 应用程序变快的建议中往往都会提到如下的建议:
- 不要阻塞主线程
- 把Long Task打断
我们在加载的页面的时候,基本上都会去考虑怎么减少javascript 代码的体积,这个能够提高页面打开的速度,但是更少的代码并不一定意味着用户的界面体验会更流畅。
要懂得优化js中任务的重要性。那么我们首先需要了解什么是任务,任务的角色,以及浏览器是怎么处理任务的。我们先来了解一下什么是任务?
二、什么是任务
浏览器执行的各个任务之间是相互独立的。例如页面渲染,解析html和css,运行我们编写的javascript代码,以及一些我们无法直接控制的事情都输入任务的范畴。但是浏览器中任务的主要来源是我们编写和部署的代码。
任务影响性能的方式很多,比如我们在打开网站时下载js代码,浏览器会把任务放在队列中,当解析编译javascript完成执行,再去排队执行任务。随后页面的任务会随着用户交互驱动的事件处理器,js动画以及分析收集的后台活动等js活动触发(web worker 等类似的api除外)。
三、什么是主线程
浏览器中大多数任务都发生在主线程中。主线程之所以被称为主线程的一个主要原因是:我们写的javascript代码几乎都在该线程上执行。
在同一时刻,主线程只能运行一个任务。当该任务的执行时间超过50ms的时候,那么这个任务就会被称之为Long Task(long task)。当一个Long Task正在运行的过程中,这个时候用户也在与页面进行交互,或者这个时候有个关键的渲染更新需要进行。这个时候,浏览器会延迟用户交互或者渲染。这个时候导致用户交互或渲染延迟,也是就页面发生卡顿。
我们需要把Long Task进行分解,也就是把一个Long Task分解长几个小的任务,让每个任务的执行时间变短。
分拆任务是个很重要的,原因是当Long Task被分拆成短任务之后,浏览器有更多的机会去执行优先级比较高的工作,如用户的交互。
从上图可以看出,因为Long Task的原因,处理用户交互的时间会被延迟到等待Long Task的完成,这个时候会让用户感觉页面卡顿。当把Long Task拆分成短任务时,
用户交互产生的事件处理器会在短任务之间执行,这样会让用户感觉体验变动流畅,页面并不卡顿。问题是,我们都知道要把Long Task拆分成短任务,减少主线程的阻塞,但是在实际的操作中,我们该怎么去做,怎么去拆分Long Task?
四、任务管理策略
在软件架构中,常常提及到将功能分拆成一个个小功能。这样在代码可读性,项目可维护程度会更高,此外测试用例也更容易.
1 |
|
在上面的例子中,名为saveSettings()的函数调用了五个函数完成保存设置的功能。保存设置的功能验证表单,显示加载条,保存数据等等。从设计概念上来讲,这是一个很好的架构,如果我们想要调试其中的一个函数,我们能够查到具体每个函数的功能实现。
需要注意的是,在javascript 并不是把每个子函数当一个独立的任务来执行,因为他们是在saveSettings()函数中执行的。这意味着上面的五个函数被当做个任务进行执行。
注意:javascript使用 run-to-completion model 来执行每一个任务。这意味着每个任务都会执行完之后,才会退出主线程,不会考虑阻塞主线程多长时间。
在最好的情况下,即使只是这些功能中的一个,也可以为任务的总时长贡献50毫秒或更多的时间。在最坏的情况下,更多的这些任务可以运行相当长的时间–特别是在低端设备上。我们可以使用下面的策略来分解,优先处理优先级高的任务。
五、手动延迟代码执行
我们之前经常使用setTImeout这个函数来将Long Task分解成一个个更小的任务。我们可以在函数中使用setTimout来将saveSettings 进行分解。利用它将回调函数分割成一个独立的任务,即使我们将延迟时间设置为0。
1 |
|
如果我们有一系列需要连续执行的函数,使用上述的方式,能够达到预期的效果,但是我们的代码并不总是以上述的方式进行组织。例如,我们有个巨大的数据需要在一个队列中处理,当数据量为百万级别时,需要花费很长的时间。
1 |
|
在上述代码中使用setTimeout() 是有很大问题的,因为它的人机工程学使得它很难实现,而且整个数据数组可能需要很长的时间来处理,即使每个item都可以很快处理。在这里使用setTimeout并不合时宜。
除了使用 setTimeout(),我们还可以使用其他的api将代码延迟到后续的任务中执行。我们可以使用postMessage()让延迟时间更短。我们也可以使用requestIdleCallback()来拆分任务,但是我们必须要注意这个函数。requestIdleCallback()这个函数调度的task的优先级相当低,它只有当浏览器有空闲的时间时候,才会去执行。当主线线程一直处于拥挤状态,由requestIdleCallback()生成的任务可能会一直不执行。
六、使用 aysnc/await 来创建让步点
在本文的剩下部分我们会到看到一个短语就是,在主线程上创建让步点。这个是什么意思?我们为什么要这么做?哪种情况下我们需要这么做?
重要提示:当让步于主线程之后,给了主线程一个机会去处理比当前正在排队的任务更重要的任务。理想情况下我们有一些关键的面向的用户的工作需要更快的执行时。我们应该创建让步点,为主线程更快的执行关键的工作创造机会。
当Long Task被拆分之后,根据浏览器自带的执行策略划分,浏览器能够更好的执行其他优先级级别高的任务。让步给主线程的一种方式是使用setTimeout的Promise。
1 |
|
注意:尽管这个例子在返回promise中通过setimeout来调用resolve,但是并没有新开一个任务让promise执行后续代码,而是通过setTimeout调用。因为promise的回调属于微任务,因此不会让步于主线程。
在saveSettings() 函数中,如果在每个函数之后添加 await yieldToMain() 代码,就会在每个task处理完之后,让步给主线程去执行更重要的工作。代码如下图所示
1 |
|
重要提示:我们并不一定需要在每个函数执行之后创建让步点。举个例子,如果现在我们执行了两个函数,而这两个函数会导致页面进行关键的更新,这个时候我们并一定需要在这两个函数之间创建让步点。我们在哪些并不需要运行关键任务的函数之间创建让步点。
通过上述代码,我们将一个Long Task分拆成一个个独立的任务。
我们使用基于promise的方法而不是使用直接使用setTimeOut来创建让步点。让代码的可读性变强。
六、只在有必要的情况让步
如果我们有一堆任务,,但是我们只想在用户试图与页面交互时让出主线程,这个时候该怎么办?(其实需要知道用户是否在与页面进行交互,需要有这样的API)这个时候我们可以使用isInputPending()这个API来确定用户是否与页面进行交互。
我们可以在任何时候调用isInputPending()来判断用户是否企图与页面元素进行交互。当正在交互的时候,isInputPending() 返回true,否则的话返回false。
假如我们有一系列需要运行的任务看,但是我们不想妨碍任何输入。下面的这段代码,它同时使用了isInputPending()和我们自定义的yieldToMain()函数,来确保用户在尝试交互时输入不会被延迟。
1 |
|
当 saveSettings()运行的时候,它将遍历队列的任务。如果在循环期间,有用户输入,这个时候isInputPending()将返回true。saveSettings()将调用yieldToMain()处理用户输入,否则将下一个任务从队列中移出,并执行该任务,直到所有任务都执行完成。
注意:isInputPending() 可能并不总是在用户输入之后立即返回true,这是因为操作系统需要时间来告诉浏览器发生了交互。这意味着其他的代码已经开始执行了(IsInputPending的返回延迟,如上图所示,在执行完saveToDatabase()之后,浏览器才知道有交互发生)。因此,即使我们使用了isInputPending(),但是我们仍然需要限制每个函数执行的工作量。
将isInputPending()与让步机制结合使用,这是个让浏览器停止正在处理的任何任务,来响应关键的用户交互的好方法。当有繁重的任务在进行的时候,这可以提高页面的响应能力。
当浏览器不支持isInputPending()时候,我们可以利用performance来进行降级处理,如下面代码所示。
1 |
|
利用这种方式,我们可以在浏览器不支持isInputPending()的情况下来进行降级处理。
六、上述API的缺点
到目前为止提到的API可以帮助我们分解Long Task,但是都有个明显的缺点:这些API只是让任务延迟执行。如果页面能够控制页面上的所有代码,那么我们可以创建自己的调度程序,并确定任务的优先级,但是当引入第三方脚本时,我们是无法控制工作的优先级的。我们只能够将其分块,或者显性的地让步给用户交互。
很高兴,目前浏览器提供了一个专用的调度api来解决这些难题。
六、专门用于编排优先级的 API
scheduler API 提供了 postTask() 函数,目前的支持情况如下
postTask() 函数支持颗粒度更新的调度任务,可以给任务排优先级,让低优先级的任务让步给主线程。postTask() 返回promise,接受的参数用来设置优先权。
postTask()支持三个层级的优先权设置,分别如下
- background 代表最低优先级的任务
- user-visible 代表中等优先级的任务,这个是默认值设置
- user-blocking 代码优先级最高的任务
我们拿下面的代码作为例子,有三个任务设置了最高的优先级,有两个任务设置了最低的优先级。
1 |
|
这样任务被浏览器按照自己给的优先级来进行编排,这样用户交互能够如期的产生。
上面是如何使用postTask()的一个简单示例。可以实例化不同的TaskController对象,这些对象可以在任务之间共享优先级,包括根据需要更改不同TaskController实例的优先级的能力。
重要提示:postTask()目前的兼容性并不好,如果使用它,可以考虑使用polyfill。
六、内置不中断的让步方法
scheduler API 的一个目标是内置一个让步机制,这个机制目前还没有在浏览器中实现,目前还是个提案。我们可以参考WICG的说明yield-and-continuation。
1 |
|
使用scheduler.yield的好处是不中断,也就意味着如果是在一连串任务中yield,那么从yield的时间点开始,其他编排好的任务的执行会继续执行,不会被第三方js代码阻塞代码的执行。
六、总结
虽然管理任务富有挑战性,但管理任务却能让我们受益颇多,会让网站能有更快的用户交互体验。管理和调优没有万灵药,但确有一系列不同的技巧。最后总结一下,管理任务时主要需要考虑以下几点:
- 遇到关键任务和用户侧的任务需要让步于主线程
- 使用isInputPending来让步主线程让用户可以与页面交互
- 适应postTask来调整任务的优先级
- 最后,每个函数尽可能地减少活动
使用以上一个或多个方法,就能够将应用中的任务进行管理,根据用户需要来调整优先级,同时能保证相对不那么重要的工作得以继续执行,来创造更好的用户体验,使网站响应更快,使用更令人心情愉悦。