大家好,我是林三心,用最通俗易懂的話講最難的知識(shí)點(diǎn)是我的座右銘,基礎(chǔ)是進(jìn)階的前提是我的初心~
就在 Chrome 115 版本,瀏覽器開始了對(duì) scheduler.yield 的灰度測(cè)試。scheduler.yield 是 scheduler API 中新增的一個(gè)功能,它能以更簡單、更好的方式將控制權(quán)交還給主線程。在開始講解這個(gè) API 之前,我們先來看一個(gè)新的性能指標(biāo)。

下次繪制交互 (INP) 是一項(xiàng)新的指標(biāo),瀏覽器計(jì)劃于 2024 年 3 月將其取代取代首次輸入延遲 (FID) ,成為最新的 Web Core Vitals(Web 核心性能指標(biāo),可以看我這篇文章:解讀新一代 Web 性能體驗(yàn)和質(zhì)量指標(biāo))。
Chrome 使用數(shù)據(jù)顯示,用戶在頁面上花費(fèi)的時(shí)間有 90% 是在網(wǎng)頁加載完成后花費(fèi)的,因此,仔細(xì)測(cè)量整個(gè)頁面生命周期的響應(yīng)能力是非常重要的,這就是 INP 指標(biāo)評(píng)估的內(nèi)容。
良好的響應(yīng)能力意味著頁面可以快速響應(yīng)并且與用戶進(jìn)行的交互。當(dāng)頁面響應(yīng)交互時(shí),最直接的結(jié)果就是視覺反饋,由瀏覽器在瀏覽器渲染的下一幀中體現(xiàn)。例如,視覺反饋會(huì)告訴我們是否確實(shí)添加了購物車的商品、是否快讀打開了導(dǎo)航菜單、服務(wù)器是否正在對(duì)登錄表單的內(nèi)容進(jìn)行身份驗(yàn)證等等。INP 的目標(biāo)就是確保對(duì)于用戶進(jìn)行的所有或大多數(shù)交互,從用戶發(fā)起交互到繪制下一幀的時(shí)間盡可能短。
INP 是一種指標(biāo),通過觀察用戶訪問頁面的整個(gè)生命周期中發(fā)生的所有單擊、敲擊和鍵盤交互的延遲來評(píng)估頁面對(duì)用戶交互的整體響應(yīng)能力。

交互是在同一邏輯用戶手勢(shì)期間觸發(fā)的一組事件處理程序。例如,觸摸屏設(shè)備上的 “點(diǎn)擊” 交互包括多個(gè)事件,例如 pointerup、pointerdown 和 click。交互可以由 JavaScript、CSS、內(nèi)置瀏覽器控件或其組合驅(qū)動(dòng)。

交互的延遲就是由驅(qū)動(dòng)交互的這一組事件處理程序的單個(gè)最長持續(xù)時(shí)間組成的,從用戶開始交互到渲染下一幀視覺反饋的時(shí)間。
INP 考慮的是所有頁面的交互,而首次輸入延遲 (FID) 只會(huì)考慮第一次交互。而且它只測(cè)量了第一次交互的輸入延遲,而不是運(yùn)行事件處理程序所需的時(shí)間或下一幀渲染的延遲。
瀏覽器希望使用 INP 替代 FID 就意味著用戶的交互體驗(yàn)越來越重要了,我們常常聽到的時(shí)間切片的概念,實(shí)際上就是為了提升網(wǎng)頁的交互響應(yīng)能力。
JavaScript 使用 run-to-completion 模型來處理任務(wù)。這意味著,當(dāng)任務(wù)在主線程上運(yùn)行時(shí),該任務(wù)將運(yùn)行必要的時(shí)間才能完成。任務(wù)完成后,控制權(quán)交會(huì)還給主線程,這樣主線程就可以處理隊(duì)列中的下一個(gè)任務(wù)。
除了任務(wù)永遠(yuǎn)不會(huì)完成的極端情況(例如無限循環(huán))之外,屈服是 JavaScript 任務(wù)調(diào)度邏輯不可避免的一個(gè)方面。屈服遲早會(huì)發(fā)生,只是時(shí)間問題,而且越早越好。當(dāng)任務(wù)運(yùn)行時(shí)間過長(準(zhǔn)確地說超過 50 毫秒)時(shí),它們會(huì)被視為長任務(wù)。
長任務(wù)是頁面響應(yīng)能力差的一個(gè)根源,因?yàn)樗鼈冄舆t了瀏覽器響應(yīng)用戶輸入的能力。長任務(wù)發(fā)生的次數(shù)越多,而且運(yùn)行的時(shí)間越長,用戶就越有可能感覺到頁面運(yùn)行緩慢,甚至感覺頁面完全掛掉了。
不過,代碼在瀏覽器中啟動(dòng)任務(wù)并不意味著必須等到任務(wù)完成后才能將控制權(quán)交還給主線程。你可以通過在任務(wù)中明確交出控制權(quán)來提高對(duì)頁面上用戶輸入的響應(yīng)速度,這樣就能在下一個(gè)合適的時(shí)間來完成任務(wù)。這樣,其他任務(wù)就能更快地在主線程上獲得時(shí)間,而不必等待長任務(wù)的完成。

這張圖可以很直觀的顯示:在上面的執(zhí)行中,只有在任務(wù)運(yùn)行完成后才會(huì)交還控制權(quán),這意味著任務(wù)可能需要更長時(shí)間才能完成,然后才會(huì)將控制權(quán)交還給主線程。在下面,控制權(quán)交還是主動(dòng)進(jìn)行的,將一個(gè)較長的任務(wù)分解成多個(gè)較小的任務(wù)。這樣,用戶交互可以更快地運(yùn)行,從而提高輸入響應(yīng)速度和 INP。
當(dāng)我們想要明確屈服時(shí),就是在告訴瀏覽器 “嘿,我知道我要做的工作可能需要一段時(shí)間,并且我不希望你在響應(yīng)用戶輸入之前必須完成所有這些工作或其他可能也很重要的任務(wù)”。
聽起來這個(gè)是不是很熟悉?這其實(shí)就是我們常說的 “時(shí)間切片” 的概念,之前你聽到可能還是在 React 的理念里,因?yàn)樗亲钤缣岢鲞@個(gè)能力的前端框架。我們?cè)賮砘仡櫹旅孢@個(gè)典型的例子:
舊版 React 架構(gòu)是遞歸同步更新的,如果節(jié)點(diǎn)非常多,即使只有一次 state 變更,React 也需要進(jìn)行復(fù)雜的遞歸更新,更新一旦開始,中途就無法中斷,直到遍歷完整顆樹,才能釋放主線程。

當(dāng)渲染的層級(jí)很深時(shí),遞歸更新時(shí)間超過了16ms,如果這時(shí)有用戶操作或動(dòng)畫渲染等,就會(huì)表現(xiàn)為卡頓:

后來,React 實(shí)現(xiàn)了自己的 Scheduler ,它可以將一次耗時(shí)很長的更新任務(wù)被拆分成一小段一小段的。這樣瀏覽器就有剩余時(shí)間執(zhí)行樣式布局和樣式繪制,減少掉幀的可能性。

每個(gè)小的任務(wù)完成后,控制權(quán)就會(huì)交還給主線程,瀏覽器就有了時(shí)間去及時(shí)的完成用戶的交互或頁面的繪制,所以頁面會(huì)很絲滑:

這個(gè)思路太棒了,在原生的 JavaScript 代碼,或者其他框架中我們也想要這樣的能力怎么辦?
一種常見的過渡方法是使用時(shí)間為 0 的 setTimeout。這種方法之所以有效,是因?yàn)閭鬟f給 setTimeout 的回調(diào)會(huì)將剩余工作轉(zhuǎn)移到一個(gè)單獨(dú)的任務(wù)中,這個(gè)任務(wù)將排隊(duì)等待后續(xù)執(zhí)行,這樣也可以實(shí)現(xiàn)把一大塊工作分成更小的部分。
但是,使用 setTimeout 進(jìn)行屈服可能會(huì)帶來不良的副作用:屈服之后的工作將進(jìn)入任務(wù)隊(duì)列的最尾部。通過用戶交互安排的任務(wù)仍會(huì)排在任務(wù)隊(duì)列的前面,但你想做的剩余工作可能會(huì)被排在它前面的其他任務(wù)進(jìn)一步延遲。
我們可以看一個(gè)下面的示例:
function blockingTask (ms = 200) { let arr = []; const blockingStart = performance.now(); console.log(`Synthetic task running for ${ms} ms`); while (performance.now() < (blockingStart + ms)) { arr.push(Math.random() * performance.now / blockingStart / ms); }}function yieldToMain () { return new Promise(resolve => { setTimeout(resolve, 0); });}async function runTaskQueueSetTimeout () { if (typeof intervalId === "undefined") { alert("Click the button to run blocking tasks periodically first."); return; } clearTaskLog(); for (const item of [1, 2, 3, 4, 5]) { blockingTask(); logTask(`Processing loop item ${item}`); await yieldToMain(); }}document.getElementById("setinterval").addEventListener("click", ({ target }) => { clearTaskLog(); intervalId = setInterval(() => { if (taskOutputLines < MAX_TASK_OUTPUT_LINES) { blockingTask(); logTask("Ran blocking task via setInterval"); } }); target.setAttribute("disabled", true);}, { once: true});document.getElementById("settimeout").addEventListener("click", () => { runTaskQueueSetTimeout();});我們先通過 setinterval 來定期執(zhí)行一些任務(wù),下面我們來使用 setTimeout 來模擬時(shí)間切片,將長任務(wù)進(jìn)行拆解,我們會(huì)得到下面這樣的打印結(jié)果:
Processing loop item 1Processing loop item 2Ran blocking task via setIntervalProcessing loop item 3Ran blocking task via setIntervalProcessing loop item 4Ran blocking task via setIntervalProcessing loop item 5Ran blocking task via setIntervalRan blocking task via setInterval很多腳本(尤其是第三方腳本)經(jīng)常會(huì)注冊(cè)一個(gè)定時(shí)器函數(shù),在某個(gè)時(shí)間間隔內(nèi)運(yùn)行工作。使用 setTimeout 來拆解長任務(wù)意味著,來自其他任務(wù)源的工作可能會(huì)排在退出事件循環(huán)后必須完成的剩余工作之前。
這也許能夠起到一定的作用,但在許多情況下,這種行為是開發(fā)者不愿輕易放棄主線程控制權(quán)的原因。能主動(dòng)交出控制權(quán)是好事,因?yàn)橛脩艚换ビ袡C(jī)會(huì)更快地運(yùn)行,但它也會(huì)讓其他非用戶交互的工作在主線程上獲得時(shí)間。這確實(shí)是個(gè)問題,scheduler.yield 可以幫助解決這個(gè)問題!
我們需要注意一下,交出主線程控制權(quán)并不是 setTimeout 的設(shè)計(jì)目標(biāo),它的核心目標(biāo)是能在未來某個(gè)時(shí)間完成某個(gè)任務(wù),所以它會(huì)把任務(wù)中的工作排在隊(duì)列的最后面。
但是,與之相反,默認(rèn)情況下,scheduler.yield 會(huì)將剩余的工作發(fā)送到隊(duì)列的前面。這意味著你想要在 yield 后立即恢復(fù)的工作不會(huì)讓位于其他來源的任務(wù)(用戶交互除外)。
scheduler.yield 是一個(gè)向主線程主動(dòng)屈服并在調(diào)用時(shí)返回 Promise 的函數(shù)。這意味著你可以在異步函數(shù)中等待它:
async function yieldy () { // Do some work... // ... // Yield! await scheduler.yield(); // Do some more work... // ...}還是使用前面的例子,這次我們使用 scheduler.yield 進(jìn)行等待:
async function runTaskQueueSchedulerDotYield () { if (typeof intervalId === "undefined") { alert("Click the button to run blocking tasks periodically first."); return; } if ("scheduler" in window && "yield" in scheduler) { clearTaskLog(); for (const item of [1, 2, 3, 4, 5]) { blockingTask(); logTask(`Processing loop item ${item}`); await scheduler.yield(); } } else { alert("scheduler.yield isn't available in this browser :("); }}我們會(huì)發(fā)現(xiàn)打印的結(jié)果是這樣的:
Processing loop item 1Processing loop item 2Processing loop item 3Processing loop item 4Processing loop item 5Ran blocking task via setIntervalRan blocking task via setIntervalRan blocking task via setIntervalRan blocking task via setIntervalRan blocking task via setInterval這樣就可以達(dá)到兩全其美的效果:既能將長任務(wù)進(jìn)行分割,主動(dòng)給主線程讓出控制權(quán)來提高網(wǎng)站的交互響應(yīng)速度,又能確保讓出主線程后要完成的工作不會(huì)被延遲。
如果大家對(duì) Scheduler.yield 感興趣并且想嘗試一下,從 Chrome 115版本開始可以:
打開 chrome://flags ,然后選擇啟用 Experimental Web Platform Features ,這樣就可以使用 Scheduler.yield 了。也可以嘗試使用官方提供的 Polifill :https://github.com/GoogleChromeLabs/scheduler-polyfill
如果在業(yè)務(wù)代碼里使用,為了兼容不支持的低版本瀏覽器,可以在不支持時(shí)回退到 setTimeout 寫法:
// A function for shimming scheduler.yield and setTimeout:function yieldToMain () { // Use scheduler.yield if it exists: if ('scheduler' in window && 'yield' in scheduler) { return scheduler.yield(); } // Fall back to setTimeout: return new Promise(resolve => { setTimeout(resolve, 0); });}// Example usage:async function doWork () { // Do some work: // ... await yieldToMain(); // Do some other work: // ...}當(dāng)然,如果你不想讓你的任務(wù)被其他任務(wù)延遲掉,也可以在不支持這個(gè) API 時(shí)選擇不屈服:
// A function for shimming scheduler.yield with no fallback:function yieldToMain () { // Use scheduler.yield if it exists: if ('scheduler' in window && 'yield' in scheduler) { return scheduler.yield(); } // Fall back to nothing: return;}// Example usage:async function doWork () { // Do some work: // ... await yieldToMain(); // Do some other work: // ...}
本文鏈接:http://m.www897cc.com/showinfo-26-71932-0.html瀏覽器也擁有了原生的 “時(shí)間切片” 能力!
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com