它被称为
主线程
是有原因的:它是您编写的
几乎所有
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://web.dev/optimize-long-tasks/?utm_source=xgqfrms.xyz