摘要:我將這個策略稱之為閑置直到緊急。請注意,在腳本執行時,它作為單個任務需要毫秒才能運行完成。很明顯,解決方案是將這些代碼分解為多個任務。原因如下推遲組件初始化僅在組件尚未渲染時才有用。這稱為輸入優先級。
Idle Until Urgent(閑置直到緊急)
譯者注:大家耳熟能詳的優化策略已經談論了好多年了,用 Chrome 性能分析工具發現瓶頸并針對性優化的文章網絡上也有不少,但是從運行時調度策略來思考優化方式的卻鳳毛麟角,正如我們之前只知道使用 setTimeout 來進行 throttling 和 debounce。因此在偶然看到這篇文章時,我有一種__豁然開朗__的感覺:原來我們還可以在這么細致的粒度上進行調度。
原文:https://philipwalton.com/articles/idle-until-urgent/
幾周前,我正著手查看我網站的一些性能指標。具體來說,我想看看我在我們最新的性能標準,即首次輸入延遲?(FID)上的表現。由于我的網站只是一個博客(并沒有運行很多 JavaScript ),所以我希望我能看到一個相當不錯的結果。
小于 100 毫秒的輸入延遲通常被用戶視為即時響應,因此我們建議的性能目標(以及我希望在我的分析中看到的數字)是:對于 99% 的頁面加載來說,FID <100ms。
令我驚訝的是,我的網站在第 99 百分位數下的 FID 為 254 毫秒。雖然那并不可怕,但我的完美主義性格卻令我無法松懈。嗯,我必須解決它!
總而言之,在不刪除我網站的任何功能的情況下,我需要能夠在第 99 百分位數下將我的FID控制在 100 毫秒以下。但我確信,你們這些讀者更感興趣的是以下信息:
我是_如何_診斷問題的。
我用了_什么_具體的策略和技術來解決問題。
對于上面的第二點,當我試圖解決我的問題時,我偶然發現了一個讓我想分享的,非常有趣的性能策略(這就是我寫這篇文章的主要原因)。
我將這個策略稱之為:_idle-until-urgent(閑置直到緊急)_?。
我的性能問題首次輸入延遲(FID)是一個度量標準,用于衡量用戶首次與您的網站進行交互的時間(對于像我這樣的博客來說,最有可能的情況是點擊鏈接)以及瀏覽器能夠響應該互動的時間(對于我博客的點擊交互來說,就是請求加載下一頁)。
這里可能存在延遲的原因是瀏覽器的主線程正在忙于做其他事情(通常是執行 JavaScript 代碼)。因此,要診斷出高于預期的 FID,您首先應當做的是在頁面加載時啟用站點的性能跟蹤(同時啟用 CPU 和網絡限制),然后在主線程上查找需要執行很長時間的各個任務。一旦確定了這些長任務,你就可以嘗試將它們分解為更小的任務。
以下是我在對網站進行性能跟蹤時的發現:
一份加載我網站時的 JavaScript 性能跟蹤(啟用網絡/ CPU限制)。
請注意,在 main 腳本 bundle 執行時,它作為單個任務需要 233 毫秒才能運行完成。
執行我網站的 main bundle 需要 233 毫秒。
這些代碼中的一些是 webpack boilerplate 和 babel polyfill,但大多數代碼來自我腳本中的?main()?入口函數,它本身需要 183 毫秒才能完成:
執行我的站點的main()入口函數需要 183 毫秒。
然而我并沒有在我的?main()?函數中做什么奇怪的事情。我在函數中只是初始化我的 UI 組件,然后運行分析:
const main = () => { drawer.init(); contentLoader.init(); breakpoints.init(); alerts.init(); analytics.init(); };
那么到底是什么花了這么長時間在運行?
好吧,如果你看一下這個火焰圖的尾部,你不會看到有任何函數在執行時明顯地占據了大部分時間。大多數單個函數會在不到 1 毫秒的時間內運行,但是當你將它們全部添加起來之后,在單個同步調用堆棧中運行它們就需要花費超過 100 毫秒。
這就是殺千刀的 JavaScript。
由于問題是所有這些功能都作為單個任務的一部分在運行,因此瀏覽器必須等到此任務完成才能夠響應用戶的交互。很明顯,解決方案是將這些代碼分解為多個任務。但這說起來容易,做起來難。
乍一看,似乎顯而易見的解決方案是給 main()?函數中的每個組件排個優先級(它們實際上已經按優先級順序排列),最快初始化最高優先級的組件,然后將其他組件的初始化推遲到后續的任務去做。
雖然這個方法可能對某些人有所幫助,但它并不是每個人都可以實施的通用解決方案,也不能很好地擴展到一個非常大的網站中。原因如下:
推遲 UI 組件初始化僅在組件尚未渲染時才有用。如果它已經被渲染過了,那么延遲這個組件的初始化運行,則會帶來在用戶交互時組件并未準備好的風險。
在許多情況下,所有 UI 組件要么同等重要,要么彼此依賴,因此它們都需要同時進行初始化。
有時單個組件需要足夠長的時間來初始化,此時即使它們只在自己的任務中運行,它們也會阻塞主線程。
實際情況是,在自己的任務中初始化每個組件通常是不夠高效的,并且往往是不可能的。我們通常需要把任務分解到每個被初始化的組件中。
貪婪的組件一個真正需要將其初始化代碼分解的組件的完美示例可以通過將此性能跟蹤結果進一步縮放觀察看到。在?main()?函數的中間,你會看到我的一個組件使用?Intl.DateTimeFormat API:
創建?Intl.DateTimeFormat 實例花了 13.47ms!
創建此對象需要 13.47 毫秒!
問題在于,?Intl.DateTimeFormat?實例雖然在組件的構造函數中被創建了,但實際上并沒有被引用,直到其他組件將其用于格式化日期為止。但是,此組件不知道它什么時候會被引用,因此它只能謹慎行事,立即實例化?Int.DateTimeFormat?對象。
但這真的是正確的代碼求值策略嗎?如果不是,那正確的應該是什么?
代碼求值策略在為可能執行代價高昂的代碼選擇求值策略時?,大多數開發人員會選擇以下其中一項:
及早求值(Eager evaluation):您可以立即運行代價高昂的代碼。
惰性求值(Lazy evaluation):你等到程序的另一部分需要這段代價高昂代碼的結果時,你才運行它。
這也許是兩種最受歡迎??的求值策略,但在重構完我的網站后,我現在認為這些可能是你最糟糕的選擇。
及早求值的缺點我網站上的性能問題很好地說明了及早求值的一個缺點,即如果用戶在代碼求值時嘗試與您的頁面進行交互,瀏覽器必須等到代碼完成求值才能做出響應。
如果您的頁面看起來已準備好響應用戶輸入,但實際上卻無法響應,在這種情況下,這尤其成問題。用戶會認為您的頁面緩慢甚至完全是壞的。
你預先求值的代碼越多,您的頁面達到可以交互所需的時間就越長。
惰性求值的缺點如果立即運行所有代碼是不好的,那么最明顯的解決方案就是等到實際需要它的時候再運行。這樣就不會不必要地運行代碼,特別是在用戶實際上從未需要它的情況下。
當然,等到用戶需要該代碼的結果時再運行代碼的問題在于,用戶輸入肯定會被你那些代價高昂的代碼給堵塞住。
對于某些事情(比如從網絡加載其他內容),將其推遲到用戶請求時再執行是有意義的。但是對于您正在運行的大多數代碼(例如從 localStorage 讀取數據,處理大型數據集等),您肯定希望在需要它的用戶交互開始之前就能開始執行。
其他選擇你也可以在及早和惰性求值之間選取一種其它求值策略,我不確定以下兩種策略是否有官方名稱,但我會稱之為延遲求值和空閑求值:
__延遲求值(Deferred evaluation)__:使用類似于 setTimeout 之類的方法將代碼安排在一個未來的任務里執行
__空閑求值(Idle evaluation)__:一種延遲求值,您可以使用像 requestIdleCallback 這樣的API來安排代碼執行。
這兩個選項通常都比及早或惰性求值更好,因為它們不太可能導致阻止輸入的單個長任務發生。這是因為,雖然瀏覽器無法中斷任何一個任務以響應用戶輸入時(這樣做將極有可能讓頁面崩潰),但它可以在計劃任務的隊列之間運行任務,大多數瀏覽器會將用戶輸入引發的任務這么安排。這稱為輸入優先級?。
換句話說:如果能夠確保所有代碼都運行在簡短,不同的任務中(最好少于 50 毫秒?),您的代碼將永遠不會堵塞用戶輸入。
__重要!__?雖然瀏覽器可以在排隊任務之前執行輸入的回調,但它們無法在排隊的微任務之前運行輸入回調。由于 promises 和 async 函數會作為微任務運行,所以將同步代碼轉換為基于 promise 的代碼并不會避免它堵塞用戶輸入!
如果您不熟悉任務和微任務之間的區別,我強烈建議您觀看我的同事 Jake?關于事件循環的精彩演講 。
鑒于我剛才所說,我可以重構我的 main() 函數,使用 setTimeout() 和 requestIdleCallback() 將我的初始化代碼分解為獨立的任務:
const main = () => { setTimeout(() => drawer.init(), 0); setTimeout(() => contentLoader.init(), 0); setTimeout(() => breakpoints.init(), 0); setTimeout(() => alerts.init(), 0); requestIdleCallback(() => analytics.init()); }; main();;
然而,雖然這比以前好了一點(許多小任務 vs 一項長任務),但正如我上面解釋的那樣,它可能仍然不夠好。例如,如果我推遲我 UI 組件(特別是?contentLoader?和?drawer)的初始化,它們將不太可能堵塞用戶輸入,但是當用戶嘗試與它們交互時,它們也存在未準備好的風險!
雖然使用?requestIdleCallback()?將我的 analytics 推遲可能是一個好主意,但在下一個空閑時段之前我關心的任何交互都將被遺漏。如果在用戶離開頁面之前沒有空閑時段,這些回調代碼可能永遠不會運行!
因此,如果所有的求值策略都有缺點,你應該選擇哪一個呢?
Idle Until Urgent (閑置直到緊急)在花了很多時間思考這個問題之后,我意識到我真正想要的求值策略是讓我的代碼在最初時被推遲到空閑時段,但是在需要時能夠立即運行。換句話說:?_idle-until-urgent_?。
_idle-until-urgent_?策略避免了我在上一節中描述的大多數缺點。在最壞的情況下,它具有與延遲求值完全相同的性能特征,并且在最好的情況下它根本不堵塞交互性,因為代碼執行發生在空閑期間。
我還應當提到的一點是,這種策略既適用于單個任務(閑時計算值),也適用于多個任務(可以在空閑時運行的一個有序任務隊列)。我將首先解釋單任務(空閑值)形式,因為它更容易理解。
空閑值在上面,我向大家展示了初始化?Int.DateTimeFormat?對象可能代價非常昂貴,因此如果不是立即需要這個實例的話,最好在空閑期間初始化它。當然,一旦當它被需要時,你就想讓它存在,所以這是一個?_idle-until-urgent_?求值策略的完美候選對象。
考慮以下我們要重構以使用此新策略的簡化組件示例:
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?實例在它的構造函數中做了兩件事:
為用戶交互添加事件偵聽器。
創建?Intl.DateTimeFormat?對象。
該組件完美地說明了為什么您經常需要在單個組件內部拆分任務(而不僅僅是在組件層面上拆分)。
在這種情況下,事件監聽器的立即運行非常重要,但在事件處理程序需要用到之前,是否創建了?Intl.DateTimeFormat?實例并不重要。當然我們不想在事件處理程序中創建?Intl.DateTimeFormat?對象,因為它的龜速會推遲該事件的運行。
所以,這就是我們更新此代碼以使用?_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())); } }
正如你所見,這個代碼與以前的版本看起來沒有太多不同,但是與將?this.formatter?分配給一個新的?Intl.DateTimeFormat?對象相反,我將 this.formatter?分配給了一個?IdleValue?對象,在對象中我傳遞了一個初始化功能。
此?IdleValue?類起作用的方式是,它會調度在會下一個空閑期間運行的初始化函數。如果空閑階段發生在 IdleValue 被引用實例之前,則不會發生阻塞,并且可以在請求時立即拿到該返回值。但另一方面,如果在下一個空閑周期_之前_引用該值,則安排好的空閑回調會被取消,并且初始化函數會被立即調用。
下面是如何實現 IdleValue 類的要點(注意:我還發布了這段代碼作為?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?類并不需要很多更改,但它在技術上改變了公共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(); } // ... }}
或者,如果你不介意一點抽象,你可以使用?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", }); }); } // ... }}
對于計算成本可能很高的單個屬性值,實際上沒有理由不使用此策略,尤其是你可以在不更改API的情況下使用它!
雖然這個例子使用了 Intl.DateTimeFormat 對象,但它也可能是下列任一項操作的好候選方案:
處理大量值的集合。
從 localStorage(或 cookie )獲取值。
運行?getComputedStyle()?,?getBoundingClientRect()?或任何其他可能需要在主線程上重新計算樣式或布局的 API。
空閑任務隊列上述技術適用于其值可以使用單個函數計算的獨立屬性,但在某些情況下,您的邏輯可能不適合用單個函數表達,或者,即使技術上可行,您仍然希望將它分解為幾個更小的函數,因為不這樣做的話,你可能會長時間阻塞主線程。
在這種情況下,您真正需要的是一個隊列,您可以在其中安排多個任務(函數),在瀏覽器空閑時運行。隊列將在可能的情況下運行任務,并且當需要回到瀏覽器時(例如,如果用戶正在進行交互),它將暫停執行任務。
為了解決這個問題,我構建了一個?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 代碼分解為可作為任務隊列的一部分異步運行的多帶帶任務與代碼分割不同,后者是將大型 JavaScript bundle 分解為較小的文件(這對于提高性能也很重要)。
與上面顯示的空閑初始化屬性策略一樣,空閑任務隊列也可以在需要立即執行結果的情況下立即運行(即“緊急”情況下)。
同樣,最后一點非常重要:有時不僅是因為你需要盡快計算某些東西,而是通常因為你要與同步的第三方 API 集成,所以為了兼容,你需要能夠同步運行你的任務。
在一個完美的世界中,所有 JavaScript API 都是非阻塞的,異步的,并且由可以隨意返回主線程的小塊代碼組成。但在現實世界中,由于遺留代碼庫或與我們無法控制的第三方庫的集成,我們通常別無選擇,只能保持同步。
正如我之前所說,這是 idle-until-urgent 模式的巨大優勢之一。它可以輕松應用于大多數程序,而無需大規模重寫架構。
保證緊急情況我在上面提到過?requestIdleCallback()?并沒有保證回調將會運行。在與開發人員討論?requestIdleCallback()?時,這是我聽到的他們不使用它的主要原因。在許多情況下,代碼無法運行的可能性足以成為不使用代碼的理由 - 為了使代碼安全運行并保持代碼同步(當然同時也會阻塞)。
一個完美的例子就是分析代碼。分析代碼的問題是在很多情況下需要在頁面卸載時運行(例如跟蹤出站鏈接點擊等),在這種情況下,?requestIdleCallback()?根本無法作為一個選項,因為回調永遠不會運行。由于分析庫不知道他們的用戶何時會在頁面生命周期中調用他們的 API,故而他們也傾向于安全并同步運行所有代碼(這很不幸,因為分析代碼對于用戶體驗來說并不關鍵)。
但是在?_idle-until-urgent_ 模式下,有一個簡單的解決方案。我們所要做的就是確保隊列只要當頁面處于可能很快卸載的狀態,就會立即運行。
如果您熟悉我在最近關于?Page Lifecycle API 的文章中給出的建議,您就會知道在頁面被終止或丟棄之前,最后一個開發者可以依賴的可靠回調是 visibilitychange 事件(因為頁面的 visibilityState 變為隱藏)。而且由于在隱藏狀態下用戶無法與頁面進行交互,因此這是運行任何排隊中的空閑任務的最佳時機。
實際上,如果使用?IdleQueue?類,則我們可以使用傳遞給構造函數的簡單配置項來啟用此功能。
const queue = new IdleQueue( { ensureTasksRun : true } );
對于渲染等任務,無需確保在頁面卸載之前運行任務,但對于保存用戶狀態和在會話結束時發送分析等任務,你可能希望將此選項設置為 true。
注意:監聽?visibilitychange?事件應該足以確保在卸載頁面之前運行任務,但是由于 Safari 的漏洞,當用戶關閉選項卡時,?pagehide 和 visibilitychange 事件并不總是觸發?,你必須針對 Safari 實現一個應急方法。這個解決方法在 IdleQueue?類中已經為您實現?,但如果您自己實現它,則必須對它有足夠了解。
警告!?不要為了在頁面卸載之前運行隊列而監聽?unload?事件。unload 事件并不可靠,并且在某些情況下會有損性能。有關更多詳細信息,請參閱我的?Page Lifecycle API 文章?。
idle-until-urgent 的用例每當你需要運行可能代價高昂的代碼時,你應該嘗試將其分解為更小的任務。如果現在不需要立即使用該代碼,但未來某些時候可能需要該代碼,那么它就是一個完美的,可使用空閑直到緊急策略的用例 。
在你自己的代碼中,我建議做的第一件事就是查看你所有的構造函數,如果它們中的任何一個會運行可能很耗時的操作,那么重構它們以使用 IdleValue 對象來代替。
對于其他的一些邏輯,如果這些邏輯對于直接用戶交互是必要的,但并不一定是決定性的,那么請考慮將該邏輯添加到?IdleQueue?。不用擔心,如果你需要立即運行該代碼,你隨時可以。
特別適合這種技術的兩個具體示例(并且與大部分網站相關)是持久化應用程序狀態(例如,使用Redux之類)和網站分析。
__注意__:這些用例想表明的意圖是任務應該在空閑期間運行,當然,如果它們沒有立即運行也沒有問題。如果您需要處理高優先級的任務,這些任務旨在盡快運行(但仍然需要響應輸入),那么?requestIdleCallback()?可能無法解決您的問題。
幸運的是,我的一些同事提出了新的Web平臺API(?shouldYield()?和原生的?Scheduling API?),它們可能會對你有所幫助。
持久化應用狀態考慮這樣一個 Redux 應用程序,它將應用程序狀態存儲在內存中,但也需要將其存儲在持久存儲(如 localStorage )中,以便下次用戶訪問頁面時可以重新加載。
在 localStorage 中存儲狀態的大多數 Redux 應用程序使用的 debounce 技術大致是這樣的:
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 技術肯定比什么都不用好,但它并不是一個完美的解決方案。問題是你無法保證當 debounced 函數運行時,它不會在對用戶來說很關鍵的時間點阻塞主線程。
在空閑時間安排 localStorage 寫入會好得多。你可以將上述代碼從 debounce 策略轉換為?_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 更好,因為即使用戶離開頁面,它也可以保證狀態得到保存。而使用 debounce 的例子,寫入可能會在這種情況下失敗。
網站分析_idle-until-urgent_?的另一個完美用例是分析代碼。下面是一個示例,說明如何使用?IdleQueue?類來安排發送分析數據,以確保即使用戶關閉選項卡或在下一個空閑時段之前導航網頁,?也會發送分析數據。
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", }); }); });
除了確保緊急時執行之外,將此任務添加到空閑隊列還可確保它不會阻止任何其他響應用戶單擊操作所需的代碼。
實際上,通常最好的方法是在閑暇時運行所有分析代碼,包括初始化代碼。對于像 analytics.js 這樣的?API 已經成為隊列的庫?,可以很容易地將這些命令添加到我們的?IdleQueue?實例中。
例如,您可以將?默認analytics.js安裝代碼段?的最后一部分這樣轉換: 從:
ga("create", "UA-XXXXX-Y", "auto"); ga("send", "pageview");
轉成這樣:
const queue = new IdleQueue({ensureTasksRun: true}); queue.pushTask(() => ga("create", "UA-XXXXX-Y", "auto")); queue.pushTask(() => ga("send", "pageview"));
(你也可以給?ga()?函數創建一個能夠自動把命令加入隊列的 wrapper,這就是我所做的?)。
requestIdleCallback 的瀏覽器支持在撰寫本文時,只有 Chrome 和 Firefox 支持?requestIdleCallback()?。雖然真正的 polyfill 是不可能實現的(因為只有瀏覽器自己可以知道它何時空閑),但是很容易編寫一個退回?setTimeout?的 fallback(本文中提到的所有 helper 類和方法都使用了這個 fallback?)。
即使在不原生支持?requestIdleCallback()?的瀏覽器中, 使用?setTimeout?的 fallback 肯定比不使用此策略更好,因為瀏覽器仍然可以在通過?setTimeout()?進行排隊的任務之前進行輸入優先級排序。
這實際上提高了多少性能?在本文開頭我提到我想出了這個策略,是因為我試圖提高我的網站的 FID 值。我試圖將 main bundle 加載后的立即運行的所有代碼分割開,但我還需要確保我的網站繼續使用只有同步 API 的某些第三方庫(例如 analytics.js )。
我在實現?_idle-until-urgent_?之前做的跟蹤顯示出,我有一個包含所有初始化代碼的233ms 任務。在實現我在此描述的技術之后,你可以看到我有多個更短時間的任務。事實上,最長的一個現在只有37毫秒!
我網站的 JavaScript 的性能跟蹤顯示了許多簡短的任務。
這里要強調的一個非常重要的一點是,它完成的工作和之前是一樣的,只是現在分散在多個任務上并在閑置期間運行。
因為沒有任何一項任務超過 50 毫秒,所以沒有一個任務影響我的交互時間(TTI),這對我的 lighthouse 得分很有幫助:
我實施了 _idle-until-urget 之后的 Lighthouse 報告。_
最后,由于所有這些工作的重點是提高我的FID,在將這些更改發布到生產環境并查看結果后,我很高興發現我在第 99 百分位數下的 FID 值減少了67%!
代碼版本 | FID(p99) | FID(p95) | FID(p50) |
---|---|---|---|
在?_idle-until-urgent_?之前 | 254ms | 20ms | 3ms |
在?_idle-until-urgent_?之后 | 285ms | 16ms | 3ms |
在一個完美的世界中,我們的網站永遠不會在不必要的時刻阻塞主線程。我們都使用 Web worker 來完成非 UI 的工作,并且我們在瀏覽器中內置了?shouldYield()?和原生?Scheduling API?。
但是在我們當前的世界中,我們的 Web 開發人員通常別無選擇,只能在主線程上運行非 UI 代碼,這會導致無響應。
希望本文能夠說服你切分長期運行的 JavaScript 任務。而且,由于?_idle-until-urgent_?可以將看起來同步的 API 變成實際上在空閑時段運行的代碼,因此它是一個很好的解決方案,并適用于我們今天廣泛使用的庫。
如果您喜歡這篇文章并認為其他人也應該閱讀它,請在 Twitter 上分享?。
文章可隨意轉載,但請保留此 原文鏈接。
非常歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj(at)alibaba-inc.com 。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98113.html
摘要:而這個子進程只是呆坐在那里,什么事也不做,每個子進程白白消耗超過的內存。這些子進程主要是由這個配置選項產生的。只要設置,就會有空閑的子進程等在那里等待被使用。事情做完之后,子進程會留在內存中秒鐘時間,然后自己退出。 標題直譯:如何減少PHP-FPM (php5-fpm)內存占用50%原標題:How to reduce PHP-FPM (php5-fpm) RAM usage by ab...
摘要:鑒于大多數場景里不同數據項使用的都是固定的過期時長,采用了統一過期時間的方式。緩存能復用驅逐策略下的隊列以及下面將要介紹的并發機制,讓過期的數據項在緩存的維護階段被拋棄掉。的設計實現來自于大量的洞見和許多貢獻者的共同努力。 本文來自阿里集團客戶體驗事業群 簡直同學的投稿,簡直基于工作場景對于緩存做了一些研究,并翻譯了一篇文章供同道中人學習。 原文:http://highscalabi...
閱讀 3166·2021-11-23 09:51
閱讀 678·2021-10-14 09:43
閱讀 3200·2021-09-06 15:00
閱讀 2403·2019-08-30 15:54
閱讀 2557·2019-08-30 13:58
閱讀 1840·2019-08-29 13:18
閱讀 1372·2019-08-27 10:58
閱讀 506·2019-08-27 10:53