摘要:我將這個(gè)策略稱之為閑置直到緊急。請注意,在腳本執(zhí)行時(shí),它作為單個(gè)任務(wù)需要毫秒才能運(yùn)行完成。很明顯,解決方案是將這些代碼分解為多個(gè)任務(wù)。原因如下推遲組件初始化僅在組件尚未渲染時(shí)才有用。這稱為輸入優(yōu)先級。
Idle Until Urgent(閑置直到緊急)
譯者注:大家耳熟能詳?shù)膬?yōu)化策略已經(jīng)談?wù)摿撕枚嗄炅?,?Chrome 性能分析工具發(fā)現(xiàn)瓶頸并針對性優(yōu)化的文章網(wǎng)絡(luò)上也有不少,但是從運(yùn)行時(shí)調(diào)度策略來思考優(yōu)化方式的卻鳳毛麟角,正如我們之前只知道使用 setTimeout 來進(jìn)行 throttling 和 debounce。因此在偶然看到這篇文章時(shí),我有一種__豁然開朗__的感覺:原來我們還可以在這么細(xì)致的粒度上進(jìn)行調(diào)度。
原文:https://philipwalton.com/articles/idle-until-urgent/
幾周前,我正著手查看我網(wǎng)站的一些性能指標(biāo)。具體來說,我想看看我在我們最新的性能標(biāo)準(zhǔn),即首次輸入延遲?(FID)上的表現(xiàn)。由于我的網(wǎng)站只是一個(gè)博客(并沒有運(yùn)行很多 JavaScript ),所以我希望我能看到一個(gè)相當(dāng)不錯的結(jié)果。
小于 100 毫秒的輸入延遲通常被用戶視為即時(shí)響應(yīng),因此我們建議的性能目標(biāo)(以及我希望在我的分析中看到的數(shù)字)是:對于 99% 的頁面加載來說,F(xiàn)ID <100ms。
令我驚訝的是,我的網(wǎng)站在第 99 百分位數(shù)下的 FID 為 254 毫秒。雖然那并不可怕,但我的完美主義性格卻令我無法松懈。嗯,我必須解決它!
總而言之,在不刪除我網(wǎng)站的任何功能的情況下,我需要能夠在第 99 百分位數(shù)下將我的FID控制在 100 毫秒以下。但我確信,你們這些讀者更感興趣的是以下信息:
我是_如何_診斷問題的。
我用了_什么_具體的策略和技術(shù)來解決問題。
對于上面的第二點(diǎn),當(dāng)我試圖解決我的問題時(shí),我偶然發(fā)現(xiàn)了一個(gè)讓我想分享的,非常有趣的性能策略(這就是我寫這篇文章的主要原因)。
我將這個(gè)策略稱之為:_idle-until-urgent(閑置直到緊急)_?。
我的性能問題首次輸入延遲(FID)是一個(gè)度量標(biāo)準(zhǔn),用于衡量用戶首次與您的網(wǎng)站進(jìn)行交互的時(shí)間(對于像我這樣的博客來說,最有可能的情況是點(diǎn)擊鏈接)以及瀏覽器能夠響應(yīng)該互動的時(shí)間(對于我博客的點(diǎn)擊交互來說,就是請求加載下一頁)。
這里可能存在延遲的原因是瀏覽器的主線程正在忙于做其他事情(通常是執(zhí)行 JavaScript 代碼)。因此,要診斷出高于預(yù)期的 FID,您首先應(yīng)當(dāng)做的是在頁面加載時(shí)啟用站點(diǎn)的性能跟蹤(同時(shí)啟用 CPU 和網(wǎng)絡(luò)限制),然后在主線程上查找需要執(zhí)行很長時(shí)間的各個(gè)任務(wù)。一旦確定了這些長任務(wù),你就可以嘗試將它們分解為更小的任務(wù)。
以下是我在對網(wǎng)站進(jìn)行性能跟蹤時(shí)的發(fā)現(xiàn):
一份加載我網(wǎng)站時(shí)的 JavaScript 性能跟蹤(啟用網(wǎng)絡(luò)/ CPU限制)。
請注意,在 main 腳本 bundle 執(zhí)行時(shí),它作為單個(gè)任務(wù)需要 233 毫秒才能運(yùn)行完成。
執(zhí)行我網(wǎng)站的 main bundle 需要 233 毫秒。
這些代碼中的一些是 webpack boilerplate 和 babel polyfill,但大多數(shù)代碼來自我腳本中的?main()?入口函數(shù),它本身需要 183 毫秒才能完成:
執(zhí)行我的站點(diǎn)的main()入口函數(shù)需要 183 毫秒。
然而我并沒有在我的?main()?函數(shù)中做什么奇怪的事情。我在函數(shù)中只是初始化我的 UI 組件,然后運(yùn)行分析:
const main = () => { drawer.init(); contentLoader.init(); breakpoints.init(); alerts.init(); analytics.init(); };
那么到底是什么花了這么長時(shí)間在運(yùn)行?
好吧,如果你看一下這個(gè)火焰圖的尾部,你不會看到有任何函數(shù)在執(zhí)行時(shí)明顯地占據(jù)了大部分時(shí)間。大多數(shù)單個(gè)函數(shù)會在不到 1 毫秒的時(shí)間內(nèi)運(yùn)行,但是當(dāng)你將它們?nèi)刻砑悠饋碇?,在單個(gè)同步調(diào)用堆棧中運(yùn)行它們就需要花費(fèi)超過 100 毫秒。
這就是殺千刀的 JavaScript。
由于問題是所有這些功能都作為單個(gè)任務(wù)的一部分在運(yùn)行,因此瀏覽器必須等到此任務(wù)完成才能夠響應(yīng)用戶的交互。很明顯,解決方案是將這些代碼分解為多個(gè)任務(wù)。但這說起來容易,做起來難。
乍一看,似乎顯而易見的解決方案是給 main()?函數(shù)中的每個(gè)組件排個(gè)優(yōu)先級(它們實(shí)際上已經(jīng)按優(yōu)先級順序排列),最快初始化最高優(yōu)先級的組件,然后將其他組件的初始化推遲到后續(xù)的任務(wù)去做。
雖然這個(gè)方法可能對某些人有所幫助,但它并不是每個(gè)人都可以實(shí)施的通用解決方案,也不能很好地?cái)U(kuò)展到一個(gè)非常大的網(wǎng)站中。原因如下:
推遲 UI 組件初始化僅在組件尚未渲染時(shí)才有用。如果它已經(jīng)被渲染過了,那么延遲這個(gè)組件的初始化運(yùn)行,則會帶來在用戶交互時(shí)組件并未準(zhǔn)備好的風(fēng)險(xiǎn)。
在許多情況下,所有 UI 組件要么同等重要,要么彼此依賴,因此它們都需要同時(shí)進(jìn)行初始化。
有時(shí)單個(gè)組件需要足夠長的時(shí)間來初始化,此時(shí)即使它們只在自己的任務(wù)中運(yùn)行,它們也會阻塞主線程。
實(shí)際情況是,在自己的任務(wù)中初始化每個(gè)組件通常是不夠高效的,并且往往是不可能的。我們通常需要把任務(wù)分解到每個(gè)被初始化的組件中。
貪婪的組件一個(gè)真正需要將其初始化代碼分解的組件的完美示例可以通過將此性能跟蹤結(jié)果進(jìn)一步縮放觀察看到。在?main()?函數(shù)的中間,你會看到我的一個(gè)組件使用?Intl.DateTimeFormat API:
創(chuàng)建?Intl.DateTimeFormat 實(shí)例花了 13.47ms!
創(chuàng)建此對象需要 13.47 毫秒!
問題在于,?Intl.DateTimeFormat?實(shí)例雖然在組件的構(gòu)造函數(shù)中被創(chuàng)建了,但實(shí)際上并沒有被引用,直到其他組件將其用于格式化日期為止。但是,此組件不知道它什么時(shí)候會被引用,因此它只能謹(jǐn)慎行事,立即實(shí)例化?Int.DateTimeFormat?對象。
但這真的是正確的代碼求值策略嗎?如果不是,那正確的應(yīng)該是什么?
代碼求值策略在為可能執(zhí)行代價(jià)高昂的代碼選擇求值策略時(shí)?,大多數(shù)開發(fā)人員會選擇以下其中一項(xiàng):
及早求值(Eager evaluation):您可以立即運(yùn)行代價(jià)高昂的代碼。
惰性求值(Lazy evaluation):你等到程序的另一部分需要這段代價(jià)高昂代碼的結(jié)果時(shí),你才運(yùn)行它。
這也許是兩種最受歡迎??的求值策略,但在重構(gòu)完我的網(wǎng)站后,我現(xiàn)在認(rèn)為這些可能是你最糟糕的選擇。
及早求值的缺點(diǎn)我網(wǎng)站上的性能問題很好地說明了及早求值的一個(gè)缺點(diǎn),即如果用戶在代碼求值時(shí)嘗試與您的頁面進(jìn)行交互,瀏覽器必須等到代碼完成求值才能做出響應(yīng)。
如果您的頁面看起來已準(zhǔn)備好響應(yīng)用戶輸入,但實(shí)際上卻無法響應(yīng),在這種情況下,這尤其成問題。用戶會認(rèn)為您的頁面緩慢甚至完全是壞的。
你預(yù)先求值的代碼越多,您的頁面達(dá)到可以交互所需的時(shí)間就越長。
惰性求值的缺點(diǎn)如果立即運(yùn)行所有代碼是不好的,那么最明顯的解決方案就是等到實(shí)際需要它的時(shí)候再運(yùn)行。這樣就不會不必要地運(yùn)行代碼,特別是在用戶實(shí)際上從未需要它的情況下。
當(dāng)然,等到用戶需要該代碼的結(jié)果時(shí)再運(yùn)行代碼的問題在于,用戶輸入肯定會被你那些代價(jià)高昂的代碼給堵塞住。
對于某些事情(比如從網(wǎng)絡(luò)加載其他內(nèi)容),將其推遲到用戶請求時(shí)再執(zhí)行是有意義的。但是對于您正在運(yùn)行的大多數(shù)代碼(例如從 localStorage 讀取數(shù)據(jù),處理大型數(shù)據(jù)集等),您肯定希望在需要它的用戶交互開始之前就能開始執(zhí)行。
其他選擇你也可以在及早和惰性求值之間選取一種其它求值策略,我不確定以下兩種策略是否有官方名稱,但我會稱之為延遲求值和空閑求值:
__延遲求值(Deferred evaluation)__:使用類似于 setTimeout 之類的方法將代碼安排在一個(gè)未來的任務(wù)里執(zhí)行
__空閑求值(Idle evaluation)__:一種延遲求值,您可以使用像 requestIdleCallback 這樣的API來安排代碼執(zhí)行。
這兩個(gè)選項(xiàng)通常都比及早或惰性求值更好,因?yàn)樗鼈儾惶赡軐?dǎo)致阻止輸入的單個(gè)長任務(wù)發(fā)生。這是因?yàn)椋m然瀏覽器無法中斷任何一個(gè)任務(wù)以響應(yīng)用戶輸入時(shí)(這樣做將極有可能讓頁面崩潰),但它可以在計(jì)劃任務(wù)的隊(duì)列之間運(yùn)行任務(wù),大多數(shù)瀏覽器會將用戶輸入引發(fā)的任務(wù)這么安排。這稱為輸入優(yōu)先級?。
換句話說:如果能夠確保所有代碼都運(yùn)行在簡短,不同的任務(wù)中(最好少于 50 毫秒?),您的代碼將永遠(yuǎn)不會堵塞用戶輸入。
__重要!__?雖然瀏覽器可以在排隊(duì)任務(wù)之前執(zhí)行輸入的回調(diào),但它們無法在排隊(duì)的微任務(wù)之前運(yùn)行輸入回調(diào)。由于 promises 和 async 函數(shù)會作為微任務(wù)運(yùn)行,所以將同步代碼轉(zhuǎn)換為基于 promise 的代碼并不會避免它堵塞用戶輸入!
如果您不熟悉任務(wù)和微任務(wù)之間的區(qū)別,我強(qiáng)烈建議您觀看我的同事 Jake?關(guān)于事件循環(huán)的精彩演講 。
鑒于我剛才所說,我可以重構(gòu)我的 main() 函數(shù),使用 setTimeout() 和 requestIdleCallback() 將我的初始化代碼分解為獨(dú)立的任務(wù):
const main = () => { setTimeout(() => drawer.init(), 0); setTimeout(() => contentLoader.init(), 0); setTimeout(() => breakpoints.init(), 0); setTimeout(() => alerts.init(), 0); requestIdleCallback(() => analytics.init()); }; main();;
然而,雖然這比以前好了一點(diǎn)(許多小任務(wù) vs 一項(xiàng)長任務(wù)),但正如我上面解釋的那樣,它可能仍然不夠好。例如,如果我推遲我 UI 組件(特別是?contentLoader?和?drawer)的初始化,它們將不太可能堵塞用戶輸入,但是當(dāng)用戶嘗試與它們交互時(shí),它們也存在未準(zhǔn)備好的風(fēng)險(xiǎn)!
雖然使用?requestIdleCallback()?將我的 analytics 推遲可能是一個(gè)好主意,但在下一個(gè)空閑時(shí)段之前我關(guān)心的任何交互都將被遺漏。如果在用戶離開頁面之前沒有空閑時(shí)段,這些回調(diào)代碼可能永遠(yuǎn)不會運(yùn)行!
因此,如果所有的求值策略都有缺點(diǎn),你應(yīng)該選擇哪一個(gè)呢?
Idle Until Urgent (閑置直到緊急)在花了很多時(shí)間思考這個(gè)問題之后,我意識到我真正想要的求值策略是讓我的代碼在最初時(shí)被推遲到空閑時(shí)段,但是在需要時(shí)能夠立即運(yùn)行。換句話說:?_idle-until-urgent_?。
_idle-until-urgent_?策略避免了我在上一節(jié)中描述的大多數(shù)缺點(diǎn)。在最壞的情況下,它具有與延遲求值完全相同的性能特征,并且在最好的情況下它根本不堵塞交互性,因?yàn)榇a執(zhí)行發(fā)生在空閑期間。
我還應(yīng)當(dāng)提到的一點(diǎn)是,這種策略既適用于單個(gè)任務(wù)(閑時(shí)計(jì)算值),也適用于多個(gè)任務(wù)(可以在空閑時(shí)運(yùn)行的一個(gè)有序任務(wù)隊(duì)列)。我將首先解釋單任務(wù)(空閑值)形式,因?yàn)樗菀桌斫狻?/p> 空閑值
在上面,我向大家展示了初始化?Int.DateTimeFormat?對象可能代價(jià)非常昂貴,因此如果不是立即需要這個(gè)實(shí)例的話,最好在空閑期間初始化它。當(dāng)然,一旦當(dāng)它被需要時(shí),你就想讓它存在,所以這是一個(gè)?_idle-until-urgent_?求值策略的完美候選對象。
考慮以下我們要重構(gòu)以使用此新策略的簡化組件示例:
class MyComponent { constructor() { addEventListener("click", () => this.handleUserClick()); this.formatter = new Intl.DateTimeFormat("en-US", { timeZone: "America/Los_Angeles", }); } handleUserClick() { console.log(this.formatter.format(new Date())); } }
上面的?MyComponent?實(shí)例在它的構(gòu)造函數(shù)中做了兩件事:
為用戶交互添加事件偵聽器。
創(chuàng)建?Intl.DateTimeFormat?對象。
該組件完美地說明了為什么您經(jīng)常需要在單個(gè)組件內(nèi)部拆分任務(wù)(而不僅僅是在組件層面上拆分)。
在這種情況下,事件監(jiān)聽器的立即運(yùn)行非常重要,但在事件處理程序需要用到之前,是否創(chuàng)建了?Intl.DateTimeFormat?實(shí)例并不重要。當(dāng)然我們不想在事件處理程序中創(chuàng)建?Intl.DateTimeFormat?對象,因?yàn)樗凝斔贂七t該事件的運(yùn)行。
所以,這就是我們更新此代碼以使用?_idle-until-urgent_?策略的方法。注意,我正在使用?IdleValue?輔助類,我將在下面解釋它:
import {IdleValue} from "./path/to/IdleValue.mjs"; class MyComponent { constructor() { addEventListener("click", () => this.handleUserClick()); this.formatter = new IdleValue(() => { return new Intl.DateTimeFormat("en-US", { timeZone: "America/Los_Angeles", }); }); } handleUserClick() { console.log(this.formatter.getValue().format(new Date())); } }
正如你所見,這個(gè)代碼與以前的版本看起來沒有太多不同,但是與將?this.formatter?分配給一個(gè)新的?Intl.DateTimeFormat?對象相反,我將 this.formatter?分配給了一個(gè)?IdleValue?對象,在對象中我傳遞了一個(gè)初始化功能。
此?IdleValue?類起作用的方式是,它會調(diào)度在會下一個(gè)空閑期間運(yùn)行的初始化函數(shù)。如果空閑階段發(fā)生在 IdleValue 被引用實(shí)例之前,則不會發(fā)生阻塞,并且可以在請求時(shí)立即拿到該返回值。但另一方面,如果在下一個(gè)空閑周期_之前_引用該值,則安排好的空閑回調(diào)會被取消,并且初始化函數(shù)會被立即調(diào)用。
下面是如何實(shí)現(xiàn) IdleValue 類的要點(diǎn)(注意:我還發(fā)布了這段代碼作為?idlize?包 的一部分,其中包含了本文中顯示的所有 helper 類):
export class IdleValue { constructor(init) { this._init = init; this._value; this._idleHandle = requestIdleCallback(() => { this._value = this._init(); }); } getValue() { if (this._value === undefined) { cancelIdleCallback(this._idleHandle); this._value = this._init(); } return this._value; } // ... }}
雖然在上面的示例中引入?IdleValue?類并不需要很多更改,但它在技術(shù)上改變了公共API(?this.formatter?與?this.formatter.getValue()?)。
如果您處于想要使用?IdleValue?類但無法更改公共 API 的情況,則可以將?IdleValue?類與 ES2015 getter 一起使用:
class MyComponent { constructor() { addEventListener("click", () => this.handleUserClick()); this._formatter = new IdleValue(() => { return new Intl.DateTimeFormat("en-US", { timeZone: "America/Los_Angeles", }); }); } get formatter() { return this._formatter.getValue(); } // ... }}
或者,如果你不介意一點(diǎn)抽象,你可以使用?defineIdleProperty()?helper 類(它在底層使用了?Object.defineProperty()?):
import {defineIdleProperty} from "./path/to/defineIdleProperty.mjs"; class MyComponent { constructor() { addEventListener("click", () => this.handleUserClick()); defineIdleProperty(this, "formatter", () => { return new Intl.DateTimeFormat("en-US", { timeZone: "America/Los_Angeles", }); }); } // ... }}
對于計(jì)算成本可能很高的單個(gè)屬性值,實(shí)際上沒有理由不使用此策略,尤其是你可以在不更改API的情況下使用它!
雖然這個(gè)例子使用了 Intl.DateTimeFormat 對象,但它也可能是下列任一項(xiàng)操作的好候選方案:
處理大量值的集合。
從 localStorage(或 cookie )獲取值。
運(yùn)行?getComputedStyle()?,?getBoundingClientRect()?或任何其他可能需要在主線程上重新計(jì)算樣式或布局的 API。
空閑任務(wù)隊(duì)列上述技術(shù)適用于其值可以使用單個(gè)函數(shù)計(jì)算的獨(dú)立屬性,但在某些情況下,您的邏輯可能不適合用單個(gè)函數(shù)表達(dá),或者,即使技術(shù)上可行,您仍然希望將它分解為幾個(gè)更小的函數(shù),因?yàn)椴贿@樣做的話,你可能會長時(shí)間阻塞主線程。
在這種情況下,您真正需要的是一個(gè)隊(duì)列,您可以在其中安排多個(gè)任務(wù)(函數(shù)),在瀏覽器空閑時(shí)運(yùn)行。隊(duì)列將在可能的情況下運(yùn)行任務(wù),并且當(dāng)需要回到瀏覽器時(shí)(例如,如果用戶正在進(jìn)行交互),它將暫停執(zhí)行任務(wù)。
為了解決這個(gè)問題,我構(gòu)建了一個(gè)?IdleQueue?類,您可以像這樣使用它:
import {IdleQueue} from "./path/to/IdleQueue.mjs"; const queue = new IdleQueue(); queue.pushTask(() => { // Some expensive function that can run idly... }); queue.pushTask(() => { // Some other task that depends on the above // expensive function having already run... });
注意:將同步 JavaScript 代碼分解為可作為任務(wù)隊(duì)列的一部分異步運(yùn)行的多帶帶任務(wù)與代碼分割不同,后者是將大型 JavaScript bundle 分解為較小的文件(這對于提高性能也很重要)。
與上面顯示的空閑初始化屬性策略一樣,空閑任務(wù)隊(duì)列也可以在需要立即執(zhí)行結(jié)果的情況下立即運(yùn)行(即“緊急”情況下)。
同樣,最后一點(diǎn)非常重要:有時(shí)不僅是因?yàn)槟阈枰M快計(jì)算某些東西,而是通常因?yàn)槟阋c同步的第三方 API 集成,所以為了兼容,你需要能夠同步運(yùn)行你的任務(wù)。
在一個(gè)完美的世界中,所有 JavaScript API 都是非阻塞的,異步的,并且由可以隨意返回主線程的小塊代碼組成。但在現(xiàn)實(shí)世界中,由于遺留代碼庫或與我們無法控制的第三方庫的集成,我們通常別無選擇,只能保持同步。
正如我之前所說,這是 idle-until-urgent 模式的巨大優(yōu)勢之一。它可以輕松應(yīng)用于大多數(shù)程序,而無需大規(guī)模重寫架構(gòu)。
保證緊急情況我在上面提到過?requestIdleCallback()?并沒有保證回調(diào)將會運(yùn)行。在與開發(fā)人員討論?requestIdleCallback()?時(shí),這是我聽到的他們不使用它的主要原因。在許多情況下,代碼無法運(yùn)行的可能性足以成為不使用代碼的理由 - 為了使代碼安全運(yùn)行并保持代碼同步(當(dāng)然同時(shí)也會阻塞)。
一個(gè)完美的例子就是分析代碼。分析代碼的問題是在很多情況下需要在頁面卸載時(shí)運(yùn)行(例如跟蹤出站鏈接點(diǎn)擊等),在這種情況下,?requestIdleCallback()?根本無法作為一個(gè)選項(xiàng),因?yàn)榛卣{(diào)永遠(yuǎn)不會運(yùn)行。由于分析庫不知道他們的用戶何時(shí)會在頁面生命周期中調(diào)用他們的 API,故而他們也傾向于安全并同步運(yùn)行所有代碼(這很不幸,因?yàn)榉治龃a對于用戶體驗(yàn)來說并不關(guān)鍵)。
但是在?_idle-until-urgent_ 模式下,有一個(gè)簡單的解決方案。我們所要做的就是確保隊(duì)列只要當(dāng)頁面處于可能很快卸載的狀態(tài),就會立即運(yùn)行。
如果您熟悉我在最近關(guān)于?Page Lifecycle API 的文章中給出的建議,您就會知道在頁面被終止或丟棄之前,最后一個(gè)開發(fā)者可以依賴的可靠回調(diào)是 visibilitychange 事件(因?yàn)轫撁娴?visibilityState 變?yōu)殡[藏)。而且由于在隱藏狀態(tài)下用戶無法與頁面進(jìn)行交互,因此這是運(yùn)行任何排隊(duì)中的空閑任務(wù)的最佳時(shí)機(jī)。
實(shí)際上,如果使用?IdleQueue?類,則我們可以使用傳遞給構(gòu)造函數(shù)的簡單配置項(xiàng)來啟用此功能。
const queue = new IdleQueue( { ensureTasksRun : true } );
對于渲染等任務(wù),無需確保在頁面卸載之前運(yùn)行任務(wù),但對于保存用戶狀態(tài)和在會話結(jié)束時(shí)發(fā)送分析等任務(wù),你可能希望將此選項(xiàng)設(shè)置為 true。
注意:監(jiān)聽?visibilitychange?事件應(yīng)該足以確保在卸載頁面之前運(yùn)行任務(wù),但是由于 Safari 的漏洞,當(dāng)用戶關(guān)閉選項(xiàng)卡時(shí),?pagehide 和 visibilitychange 事件并不總是觸發(fā)?,你必須針對 Safari 實(shí)現(xiàn)一個(gè)應(yīng)急方法。這個(gè)解決方法在 IdleQueue?類中已經(jīng)為您實(shí)現(xiàn)?,但如果您自己實(shí)現(xiàn)它,則必須對它有足夠了解。
警告!?不要為了在頁面卸載之前運(yùn)行隊(duì)列而監(jiān)聽?unload?事件。unload 事件并不可靠,并且在某些情況下會有損性能。有關(guān)更多詳細(xì)信息,請參閱我的?Page Lifecycle API 文章?。
idle-until-urgent 的用例每當(dāng)你需要運(yùn)行可能代價(jià)高昂的代碼時(shí),你應(yīng)該嘗試將其分解為更小的任務(wù)。如果現(xiàn)在不需要立即使用該代碼,但未來某些時(shí)候可能需要該代碼,那么它就是一個(gè)完美的,可使用空閑直到緊急策略的用例 。
在你自己的代碼中,我建議做的第一件事就是查看你所有的構(gòu)造函數(shù),如果它們中的任何一個(gè)會運(yùn)行可能很耗時(shí)的操作,那么重構(gòu)它們以使用 IdleValue 對象來代替。
對于其他的一些邏輯,如果這些邏輯對于直接用戶交互是必要的,但并不一定是決定性的,那么請考慮將該邏輯添加到?IdleQueue?。不用擔(dān)心,如果你需要立即運(yùn)行該代碼,你隨時(shí)可以。
特別適合這種技術(shù)的兩個(gè)具體示例(并且與大部分網(wǎng)站相關(guān))是持久化應(yīng)用程序狀態(tài)(例如,使用Redux之類)和網(wǎng)站分析。
__注意__:這些用例想表明的意圖是任務(wù)應(yīng)該在空閑期間運(yùn)行,當(dāng)然,如果它們沒有立即運(yùn)行也沒有問題。如果您需要處理高優(yōu)先級的任務(wù),這些任務(wù)旨在盡快運(yùn)行(但仍然需要響應(yīng)輸入),那么?requestIdleCallback()?可能無法解決您的問題。
幸運(yùn)的是,我的一些同事提出了新的Web平臺API(?shouldYield()?和原生的?Scheduling API?),它們可能會對你有所幫助。
持久化應(yīng)用狀態(tài)考慮這樣一個(gè) Redux 應(yīng)用程序,它將應(yīng)用程序狀態(tài)存儲在內(nèi)存中,但也需要將其存儲在持久存儲(如 localStorage )中,以便下次用戶訪問頁面時(shí)可以重新加載。
在 localStorage 中存儲狀態(tài)的大多數(shù) Redux 應(yīng)用程序使用的 debounce 技術(shù)大致是這樣的:
let debounceTimeout; // Persist state changes to localStorage using a 1000ms debounce. store.subscribe(() => { // Clear pending writes since there are new changes to save. clearTimeout(debounceTimeout); // Schedule the save with a 1000ms timeout (debounce), // so frequent changes aren"t saved unnecessarily. debounceTimeout = setTimeout(() => { const jsonData = JSON.stringify(store.getState()); localStorage.setItem("redux-data", jsonData); }, 1000); });
雖然使用 debounce 技術(shù)肯定比什么都不用好,但它并不是一個(gè)完美的解決方案。問題是你無法保證當(dāng) debounced 函數(shù)運(yùn)行時(shí),它不會在對用戶來說很關(guān)鍵的時(shí)間點(diǎn)阻塞主線程。
在空閑時(shí)間安排 localStorage 寫入會好得多。你可以將上述代碼從 debounce 策略轉(zhuǎn)換為?_idle-until-urgent_?策略,如下所示:
const queue = new IdleQueue({ensureTasksRun: true}); // Persist state changes when the browser is idle, and // only persist the most recent changes to avoid extra work. store.subscribe(() => { // Clear pending writes since there are new changes to save. queue.clearPendingTasks(); // Schedule the save to run when idle. queue.pushTask(() => { const jsonData = JSON.stringify(store.getState()); localStorage.setItem("redux-data", jsonData); }); });
請注意,此策略肯定比使用 debounce 更好,因?yàn)榧词褂脩綦x開頁面,它也可以保證狀態(tài)得到保存。而使用 debounce 的例子,寫入可能會在這種情況下失敗。
網(wǎng)站分析_idle-until-urgent_?的另一個(gè)完美用例是分析代碼。下面是一個(gè)示例,說明如何使用?IdleQueue?類來安排發(fā)送分析數(shù)據(jù),以確保即使用戶關(guān)閉選項(xiàng)卡或在下一個(gè)空閑時(shí)段之前導(dǎo)航網(wǎng)頁,?也會發(fā)送分析數(shù)據(jù)。
const queue = new IdleQueue({ensureTasksRun: true}); const signupBtn = document.getElementById("signup"); signupBtn.addEventListener("click", () => { // Instead of sending the event immediately, add it to the idle queue. // The idle queue will ensure the event is sent even if the user // closes the tab or navigates away. queue.pushTask(() => { ga("send", "event", { eventCategory: "Signup Button", eventAction: "click", }); }); });
除了確保緊急時(shí)執(zhí)行之外,將此任務(wù)添加到空閑隊(duì)列還可確保它不會阻止任何其他響應(yīng)用戶單擊操作所需的代碼。
實(shí)際上,通常最好的方法是在閑暇時(shí)運(yùn)行所有分析代碼,包括初始化代碼。對于像 analytics.js 這樣的?API 已經(jīng)成為隊(duì)列的庫?,可以很容易地將這些命令添加到我們的?IdleQueue?實(shí)例中。
例如,您可以將?默認(rèn)analytics.js安裝代碼段?的最后一部分這樣轉(zhuǎn)換: 從:
ga("create", "UA-XXXXX-Y", "auto"); ga("send", "pageview");
轉(zhuǎn)成這樣:
const queue = new IdleQueue({ensureTasksRun: true}); queue.pushTask(() => ga("create", "UA-XXXXX-Y", "auto")); queue.pushTask(() => ga("send", "pageview"));
(你也可以給?ga()?函數(shù)創(chuàng)建一個(gè)能夠自動把命令加入隊(duì)列的 wrapper,這就是我所做的?)。
requestIdleCallback 的瀏覽器支持在撰寫本文時(shí),只有 Chrome 和 Firefox 支持?requestIdleCallback()?。雖然真正的 polyfill 是不可能實(shí)現(xiàn)的(因?yàn)橹挥袨g覽器自己可以知道它何時(shí)空閑),但是很容易編寫一個(gè)退回?setTimeout?的 fallback(本文中提到的所有 helper 類和方法都使用了這個(gè) fallback?)。
即使在不原生支持?requestIdleCallback()?的瀏覽器中, 使用?setTimeout?的 fallback 肯定比不使用此策略更好,因?yàn)闉g覽器仍然可以在通過?setTimeout()?進(jìn)行排隊(duì)的任務(wù)之前進(jìn)行輸入優(yōu)先級排序。
這實(shí)際上提高了多少性能?在本文開頭我提到我想出了這個(gè)策略,是因?yàn)槲以噲D提高我的網(wǎng)站的 FID 值。我試圖將 main bundle 加載后的立即運(yùn)行的所有代碼分割開,但我還需要確保我的網(wǎng)站繼續(xù)使用只有同步 API 的某些第三方庫(例如 analytics.js )。
我在實(shí)現(xiàn)?_idle-until-urgent_?之前做的跟蹤顯示出,我有一個(gè)包含所有初始化代碼的233ms 任務(wù)。在實(shí)現(xiàn)我在此描述的技術(shù)之后,你可以看到我有多個(gè)更短時(shí)間的任務(wù)。事實(shí)上,最長的一個(gè)現(xiàn)在只有37毫秒!
我網(wǎng)站的 JavaScript 的性能跟蹤顯示了許多簡短的任務(wù)。
這里要強(qiáng)調(diào)的一個(gè)非常重要的一點(diǎn)是,它完成的工作和之前是一樣的,只是現(xiàn)在分散在多個(gè)任務(wù)上并在閑置期間運(yùn)行。
因?yàn)闆]有任何一項(xiàng)任務(wù)超過 50 毫秒,所以沒有一個(gè)任務(wù)影響我的交互時(shí)間(TTI),這對我的 lighthouse 得分很有幫助:
我實(shí)施了 _idle-until-urget 之后的 Lighthouse 報(bào)告。_
最后,由于所有這些工作的重點(diǎn)是提高我的FID,在將這些更改發(fā)布到生產(chǎn)環(huán)境并查看結(jié)果后,我很高興發(fā)現(xiàn)我在第 99 百分位數(shù)下的 FID 值減少了67%!
代碼版本 | FID(p99) | FID(p95) | FID(p50) |
---|---|---|---|
在?_idle-until-urgent_?之前 | 254ms | 20ms | 3ms |
在?_idle-until-urgent_?之后 | 285ms | 16ms | 3ms |
在一個(gè)完美的世界中,我們的網(wǎng)站永遠(yuǎn)不會在不必要的時(shí)刻阻塞主線程。我們都使用 Web worker 來完成非 UI 的工作,并且我們在瀏覽器中內(nèi)置了?shouldYield()?和原生?Scheduling API?。
但是在我們當(dāng)前的世界中,我們的 Web 開發(fā)人員通常別無選擇,只能在主線程上運(yùn)行非 UI 代碼,這會導(dǎo)致無響應(yīng)。
希望本文能夠說服你切分長期運(yùn)行的 JavaScript 任務(wù)。而且,由于?_idle-until-urgent_?可以將看起來同步的 API 變成實(shí)際上在空閑時(shí)段運(yùn)行的代碼,因此它是一個(gè)很好的解決方案,并適用于我們今天廣泛使用的庫。
如果您喜歡這篇文章并認(rèn)為其他人也應(yīng)該閱讀它,請?jiān)?Twitter 上分享?。
文章可隨意轉(zhuǎn)載,但請保留此 原文鏈接。
非常歡迎有激情的你加入 ES2049 Studio,簡歷請發(fā)送至 caijun.hcj(at)alibaba-inc.com 。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/98113.html
摘要:銷售助手緩存優(yōu)化實(shí)驗(yàn)報(bào)告問題背景移動銷售助手又名外勤通,個(gè)別頁面訪問非常慢,急需優(yōu)化。結(jié)論二對于簡單的,無須從優(yōu)化為緩存。 銷售助手-緩存優(yōu)化實(shí)驗(yàn)報(bào)告 2015.4.7 by ouyida3 問題背景 移動APP-銷售助手(又名外勤通),個(gè)別頁面訪問非常慢,急需優(yōu)化。 實(shí)驗(yàn)環(huán)境 Oracle數(shù)據(jù)庫:測試環(huán)境32.121.2.132(性能還行,但發(fā)現(xiàn)偶爾會有問題) 后臺WEB應(yīng)...
摘要:而這個(gè)子進(jìn)程只是呆坐在那里,什么事也不做,每個(gè)子進(jìn)程白白消耗超過的內(nèi)存。這些子進(jìn)程主要是由這個(gè)配置選項(xiàng)產(chǎn)生的。只要設(shè)置,就會有空閑的子進(jìn)程等在那里等待被使用。事情做完之后,子進(jìn)程會留在內(nèi)存中秒鐘時(shí)間,然后自己退出。 標(biāo)題直譯:如何減少PHP-FPM (php5-fpm)內(nèi)存占用50%原標(biāo)題:How to reduce PHP-FPM (php5-fpm) RAM usage by ab...
摘要:鑒于大多數(shù)場景里不同數(shù)據(jù)項(xiàng)使用的都是固定的過期時(shí)長,采用了統(tǒng)一過期時(shí)間的方式。緩存能復(fù)用驅(qū)逐策略下的隊(duì)列以及下面將要介紹的并發(fā)機(jī)制,讓過期的數(shù)據(jù)項(xiàng)在緩存的維護(hù)階段被拋棄掉。的設(shè)計(jì)實(shí)現(xiàn)來自于大量的洞見和許多貢獻(xiàn)者的共同努力。 本文來自阿里集團(tuán)客戶體驗(yàn)事業(yè)群 簡直同學(xué)的投稿,簡直基于工作場景對于緩存做了一些研究,并翻譯了一篇文章供同道中人學(xué)習(xí)。 原文:http://highscalabi...
閱讀 3182·2021-11-23 09:51
閱讀 694·2021-10-14 09:43
閱讀 3221·2021-09-06 15:00
閱讀 2416·2019-08-30 15:54
閱讀 2572·2019-08-30 13:58
閱讀 1861·2019-08-29 13:18
閱讀 1390·2019-08-27 10:58
閱讀 526·2019-08-27 10:53