Chrome 浏览器性能优化:主线程 & 长任务 All In One
Chrome 浏览器性能优化:主线程 & 长任务 All In One
Web 性能优化
- 不要阻塞
主线程
; - 分解你的
长任务
;
主线程
什么是主线程?
主线程是大多数任务
在浏览器中运行的地方。
它被称为主线程
是有原因的:它是您编写的几乎所有
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
指定宽度
和格式
webp
vs png
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
demo
https://codepen.io/xgqfrms/pen/oNPYdLQ?editors=1011
(🐞 反爬虫测试!打击盗版⚠️)如果你看到这个信息, 说明这是一篇剽窃的文章,请访问 https://www.cnblogs.com/xgqfrms/ 查看原创文章!
refs
https://web.dev/optimize-long-tasks/?utm_source=xgqfrms.xyz
©xgqfrms 2012-2021
www.cnblogs.com/xgqfrms 发布文章使用:只允许注册用户才可以访问!
原创文章,版权所有©️xgqfrms, 禁止转载 🈲️,侵权必究⚠️!
本文首发于博客园,作者:xgqfrms,原文链接:https://www.cnblogs.com/xgqfrms/p/17160145.html
未经授权禁止转载,违者必究!