構(gòu)建更快的 Web 體驗(yàn) - 使用 postTask 調(diào)度器
共 7726字,需瀏覽 16分鐘
·
2024-07-15 09:10
前言
介紹了如何利用 postTask 調(diào)度器來提高網(wǎng)頁的用戶體驗(yàn)和響應(yīng)速度,通過高效地調(diào)度任務(wù)和處理優(yōu)先級(jí)來優(yōu)化頁面性能。使用 postTask 可以拆分長(zhǎng)任務(wù)、預(yù)加載資源和提高頁面交互性能,讓頁面更具響應(yīng)性。同時(shí),文章還介紹了如何在 React 中集成 postTask 調(diào)度器來執(zhí)行不同模式或策略,以進(jìn)一步優(yōu)化網(wǎng)頁性能。今日前端早讀課文章由 @古茗科技翻譯分享。
正文從這開始~~
你有沒有經(jīng)歷過打開一個(gè)網(wǎng)頁,在頁面上點(diǎn)擊多次才有反應(yīng)?或者在輪播圖上滑動(dòng)圖片時(shí)卡頓和不自然?雖然這種經(jīng)歷經(jīng)常發(fā)生,但是我們可以利用工具來提高用戶的體驗(yàn)和響應(yīng)速度。高效地調(diào)度和優(yōu)先處理任務(wù)可能會(huì)產(chǎn)生快速響應(yīng)的體驗(yàn)和感覺遲緩之間的巨大差異。
Airbnb 一直在與 Chrome 團(tuán)隊(duì)合作,利用優(yōu)先級(jí) postTask 調(diào)度器來實(shí)現(xiàn)新的模式,并提高現(xiàn)有模式的性能,以提高性能。在許多性能方面的努力集中在頁面的初始加載上,Airbnb 的目標(biāo)是提高頁面加載后的用戶體驗(yàn)。他們?cè)谠S多方面使用 postTask 調(diào)度器,包括預(yù)加載輪播圖中的圖像和使地圖更具響應(yīng)性。
初識(shí) postTask 調(diào)度器
postTask 調(diào)度器旨在為我們提供更靈活和強(qiáng)大的方式,以高效地調(diào)度任務(wù)。類似于 requestIdleCallback 和 setTimeout,有效地使用 postTask 調(diào)度器可以幫助減少總阻塞時(shí)間、FCP、輸入延遲和其他關(guān)鍵指標(biāo)。
在許多情況下,頁面的性能不僅僅取決于初始加載的速度,而是取決于頁面的響應(yīng)速度和交互性能。通過使用 postTask 調(diào)度器,我們可以更好地管理任務(wù)和處理優(yōu)先級(jí),從而優(yōu)化網(wǎng)頁的性能。例如,在處理輪播圖時(shí),我們可以使用 postTask 調(diào)度器將圖像預(yù)加載任務(wù)放入低優(yōu)先級(jí)隊(duì)列中,以確保關(guān)鍵任務(wù)得到優(yōu)先處理。類似地,在處理地圖時(shí),我們可以使用 postTask 調(diào)度器來確保關(guān)鍵任務(wù)得到優(yōu)先處理,從而提高地圖的響應(yīng)速度和交互性能。
Airbnb 為了評(píng)估他們的進(jìn)展,創(chuàng)建了新的實(shí)時(shí)用戶監(jiān)測(cè)性能指標(biāo),并利用 WebPageTest 和 Lighthouse 等工具提供的現(xiàn)有實(shí)驗(yàn)室基準(zhǔn)測(cè)試指標(biāo)。
優(yōu)化前(加載搜索結(jié)果頁,總的阻塞時(shí)間大約為 16s 左右)
優(yōu)化后 (總的阻塞時(shí)間縮短了 10s 左右)
postTask 調(diào)度器是什么
與 requestAnimationFrame、setTimeout 或 requestIdleCallback 類似,scheduler.postTask 允許我們?cè)跒g覽器的事件循環(huán)中安排一個(gè)函數(shù)。然后瀏覽器會(huì)對(duì)該函數(shù)進(jìn)行優(yōu)先級(jí)排序并運(yùn)行它。
注:微任務(wù)(microtask)' 和不要暫停(don't yield)。這兩個(gè)優(yōu)先級(jí)可能會(huì)與調(diào)度和提高應(yīng)用程序的響應(yīng)能力的目標(biāo)背道而馳。
微任務(wù)是一小部分代碼,會(huì)在當(dāng)前任務(wù)完成后立即執(zhí)行。它們被優(yōu)先執(zhí)行,可能會(huì)導(dǎo)致其他計(jì)劃任務(wù)的延遲。不要暫停是一種優(yōu)先級(jí),用于長(zhǎng)時(shí)間運(yùn)行的任務(wù),這些任務(wù)在執(zhí)行過程中不應(yīng)中斷或暫停。這也可能會(huì)導(dǎo)致其他計(jì)劃任務(wù)的延遲。
雖然這些優(yōu)先級(jí)可以幫助開發(fā)人員管理任務(wù)的執(zhí)行順序,但它們也可能會(huì)導(dǎo)致響應(yīng)能力降低和調(diào)度問題。因此,開發(fā)人員需要在使用這些優(yōu)先級(jí)時(shí)與提高應(yīng)用程序響應(yīng)能力的整體目標(biāo)之間取得平衡。*
最新版本的 chrome 瀏覽器已經(jīng)支持了 scheduler api,對(duì)于那些不支持的瀏覽器也可以使用 https://www.npmjs.com/package/scheduler-polyfill 這個(gè)補(bǔ)丁
scheduler.postTask(() => console.log('Hello, postTask'));
// We’re also able to pass a set of options such as priority or
// delay to influence when it will be scheduled to run.
scheduler.postTask(() => console.log('Hello, postTask'), {
delay: 1000,
priority: 'background',
});
在上面的例子中,我們向 postTask 傳遞了一個(gè)延遲時(shí)間和優(yōu)先級(jí)參數(shù),告訴它我們想要在等待 1 秒后在后臺(tái)運(yùn)行我們的任務(wù)。postTask 調(diào)度程序目前支持 3 種不同的優(yōu)先級(jí)。
| 優(yōu)先級(jí) | 描述 | 補(bǔ)丁兼容版本 |
|---|---|---|
| user-blocking | 最高優(yōu)先級(jí)是用于阻止用戶與頁面交互的任務(wù),例如渲染核心體驗(yàn)或響應(yīng)用戶輸入。 | 在支持的情況下,它使用 MessageChannel 盡可能快地調(diào)度任務(wù)。如果不支持,則退回到 setTimeout |
| user-visible | 第二高優(yōu)先級(jí)是用于用戶可見但不一定阻止用戶操作的任務(wù),例如呈現(xiàn)頁面的次要部分。這是默認(rèn)優(yōu)先級(jí)。 | 在支持的情況下,它也使用 MessageChannel 并退回到 setTimeout,但將排在任何具有用戶阻止優(yōu)先級(jí)的調(diào)用之后。 |
| background | 最低優(yōu)先級(jí)是用于不是時(shí)間緊迫的任務(wù),例如后臺(tái)日志處理或初始化某些第三方庫(kù) | 通常使用 requestIdleCallback,并在不支持 requestIdleCallback 的情況下退回到 setTimeout (0)。 |
postTask 調(diào)度程序的一個(gè)好處是它建立在 Abort Signals 之上,使我們能夠取消已排隊(duì)但尚未執(zhí)行的任務(wù)。該 API 還定義了一個(gè)新的 TaskController,它允許通過信號(hào)使用優(yōu)先級(jí)來控制任務(wù)和優(yōu)先級(jí)。
const controller = new TaskController('background');
window.addEventListener('beforeunload', () => controller.abort());
scheduler.postTask(() => console.log('Hello, postTask'), {
signal: controller.signal,
});
拆解長(zhǎng)任務(wù)
我們應(yīng)該拆分長(zhǎng)任務(wù)以提高應(yīng)用程序的響應(yīng)能力。下面是一個(gè)錯(cuò)誤和行為記錄上報(bào)的長(zhǎng)任務(wù)示例。請(qǐng)注意瀏覽器如何將任務(wù)標(biāo)記為長(zhǎng)任務(wù)。
長(zhǎng)任務(wù)(Long tasks)是指執(zhí)行時(shí)間超過 50 毫秒(或者某些瀏覽器中可能是 100 毫秒)的任務(wù)
一旦我們確定了一個(gè)長(zhǎng)任務(wù),我們就可以使用 postTask 將任務(wù)分解成更小的任務(wù)。
// By using postTask, each method will execute in its own individual task context,
// breaking the one large task into multiple smaller tasks that allow the browser
// to respond to input and do rendering in between them if necessary.
await scheduler.postTask(() => initViewportWidthProperty());
await scheduler.postTask(() => initCriticalTracking());
使用 scheduler.postTask 后,我們不再有任何長(zhǎng)時(shí)間任務(wù),只有小于 “長(zhǎng)任務(wù)閾值” 的較小任務(wù)。
用例:資源預(yù)加載
預(yù)加載輪播圖中的下一個(gè)圖像或者在用戶加載頁面之前加載詳細(xì)信息可以顯著提高站點(diǎn)的性能和用戶的感知性能。我們最近使用 postTask 調(diào)度程序?qū)崿F(xiàn)了一個(gè)延遲、分階段和可取消的圖像預(yù)加載程序,用于我們的主搜索圖像輪播。讓我們看看如何使用 postTask 構(gòu)建一個(gè)簡(jiǎn)單版本。
圖片輪播預(yù)加載的觸發(fā)時(shí)機(jī):
列表在屏幕上顯示大約 50% 時(shí)
延遲一秒;如果用戶仍在查看它,則在輪播中加載下一張圖片
如果用戶滑動(dòng)圖像,則預(yù)加載下三張圖像,每張圖片之間間隔 100ms
如果輪播在一秒計(jì)時(shí)器結(jié)束之前的任何時(shí)候離開視口,我們應(yīng)該取消所有尚未完成的預(yù)加載任務(wù)。如果用戶導(dǎo)航到另一個(gè)頁面,也取消所有預(yù)加載任務(wù)
當(dāng)下一張幻燈片滾動(dòng)到視圖中時(shí),將加載第二張圖片。一旦我們滑動(dòng),接下來的 3 次加載,每次都在前一次加載后 100 毫秒開始
讓我們首先看一下這個(gè)問題的第一部分,即用戶將卡片滾動(dòng)到視圖中一半以上且維持一秒鐘以上,則預(yù)加載輪播中的下一張圖像。雖然在接下來的幾個(gè)示例中我們使用 React,但這并非必需的。這里所有的概念也可以使用其他框架,甚至你也可以不用任何框架。
我們假設(shè)有一個(gè)名為 preloadImages 的方法,它開始獲取下一張圖片并在完成預(yù)加載圖片時(shí)切換一個(gè)布爾值。
const [hasPreloadedNextImage, setHasPreloadedNextImage] = useState(false);
const preloadImages = useCallback((imageUrls) => {
imageUrls.forEach((url) => preloadImage(url))
setHasPreloadedNextImage(true);
}, []);
我們可以將 Intersection Observer 和 postTask 調(diào)度程序相結(jié)合,實(shí)現(xiàn)在視圖中 50% 一秒后加載第二張圖像。
const controller = useRef<TaskController | null>(null);
const [carouselDomRef, carouselIsInView] = useInView({
skip: hasPreloadedNextImage,
threshold: 0.5,
});
useEffect(() => {
if (carouselIsInView) {
controller.current = new TaskController('background');
scheduler.postTask(() => preloadImages([cardPhotoUrls[1]]), { delay: 1000, signal: controllerRef.current?.signal });
} else {
controller.current?.abort();
controller.current = null;
}
}, [carouselIsInView, preloadImages]);
這里我們使用了 useInView 用于檢測(cè)元素是否在視圖中。我們?cè)O(shè)置了一個(gè)閾值為 0.5 ,這意味著元素的一半必須在視圖中才會(huì)被視為 “可見”。我們還設(shè)置了 skip 屬性,以便在我們預(yù)加載下一張圖片時(shí)跳過這個(gè)元素。
當(dāng)元素進(jìn)入視圖時(shí),我們創(chuàng)建了一個(gè)新的 TaskController ,用于控制預(yù)加載任務(wù)的優(yōu)先級(jí)。然后,我們使用 postTask 調(diào)度程序調(diào)用 preloadImages,預(yù)加載下一張圖片。我們?cè)O(shè)置了一個(gè)延遲參數(shù)為 1000ms,這意味著用戶必須在視圖中至少停留 1 秒鐘,然后才會(huì)開始預(yù)加載下一張圖片。我們還將 TaskController 的信號(hào)傳遞給 postTask,以便在用戶滾動(dòng)出視圖時(shí)可以取消預(yù)加載任務(wù)。
當(dāng)元素不再在視圖中時(shí),我們使用 TaskController 的 abort 方法取消任何掛起的預(yù)加載任務(wù)。
將網(wǎng)絡(luò)資源分階段載入
我們需要實(shí)現(xiàn)的最后一個(gè)要求是,在用戶滑動(dòng)輪播圖后,每個(gè)圖像請(qǐng)求之間間隔 100 毫秒。讓我們看看如何使用 postTask 調(diào)度程序修改現(xiàn)有代碼以應(yīng)對(duì)這種情況。首先,讓我們添加一個(gè) hook,在用戶與之交互時(shí)調(diào)用我們的預(yù)加載邏輯,以預(yù)加載三個(gè)圖像。我們將跳過第一張圖像,因?yàn)槲覀円呀?jīng)加載了它。
useEffect(() => {
if (hasInteractedWithCarousel) {
preloadImages(imageUrls.slice(1, 4));
}
}, [hasInteractedWithCarousel]);
// We use the list index combined with delay to
// stagger the call to preload each image by 100ms each.
const preloadImages = useCallback((imageUrls) => {
imageUrls.forEach((url, index) => {
scheduler.postTask(() => preloadImage(url), {
delay: index * 100,
signal: controller.current.signal,
});
});
setHasPreloadedNextImages(true);
}, []);
postTask 調(diào)度程序的一個(gè)目標(biāo)是提供一個(gè)低級(jí)別的 API,以便在其之上構(gòu)建。我們已經(jīng)構(gòu)建了一個(gè)集成,使我們?cè)?React 中使用時(shí)可以執(zhí)行許多不同的模式或策略,我們認(rèn)為這非常有用。
在 React 中使用 postTask
盡管與 React、Vue、Angular、Lit 等進(jìn)行自定義集成并不是必需的,但這樣做可以獲得一些重大的好處。例如,在 React 中,當(dāng)一個(gè)組件卸載時(shí),我們通常希望取消任何仍在排隊(duì)的任務(wù)。
我們可以在 useEffect 的返回的函數(shù)中做到這一點(diǎn)。然而,每次都靠人去這樣做是一項(xiàng)不小的挑戰(zhàn),而不這樣做可能會(huì)導(dǎo)致內(nèi)存泄漏。還有一個(gè)挑戰(zhàn)是記得在調(diào)用 abort () 時(shí)捕獲調(diào)度程序拋出的任何 AbortError,因?yàn)檫@些錯(cuò)誤是非??深A(yù)期的,但我們不能為其做出全面的異常處理。
以下是一個(gè) usePostTaskScheduler 鉤子的一些希望具備的功能,這將使它更容易使用:
傳遞一個(gè) enabled 標(biāo)志,允許繞過調(diào)度程序以便于 A/B 測(cè)試;
允許輕松取消任務(wù),包括在卸載時(shí)自動(dòng)取消;
自動(dòng)將信號(hào)傳播到 scheduler.postTask 和 scheduler.wait;
捕獲和抑制 AbortErrors 或類似的錯(cuò)誤;
支持強(qiáng)大的調(diào)試功能;
允許為通用模式指定策略,例如我們?cè)诒疚闹薪榻B的兩個(gè)模式;
添加一個(gè)等待延遲完成的鉤子。
雖然本文不會(huì)深入討論如何實(shí)現(xiàn)這個(gè)鉤子,但是我們可以看到,它簡(jiǎn)化了在 React 中使用 postTask 調(diào)度程序的過程。例如,我們可以使用 postTask 調(diào)度程序來延遲加載一個(gè)成本高、重要性低的 React 組件,直到 load 事件觸發(fā)后,并清理一些舊的 localStorage 狀態(tài)。
const hasLoadingCompleted = useWaitForDelay({ event: 'load' }, () => {
cleanupLocalStorageKeys();
});
return (
<>
{hasLoadingCompleted && <ExpensiveComponent />}
<ExistingComponents />
</>
);
在上面的例子中,如果在事件發(fā)生之前卸載了該組件,我們將取消清理 localStorageKeys 的任務(wù),并且不會(huì)渲染 <ExpensiveComponent />。在我們的情況下,ExpensiveComponent 是異步加載的,因此通過延遲它,我們顯著降低了初始水合成本,包括阻塞時(shí)間和 bundle 大小的成本。
讓我們看看如何在后臺(tái) load 事件觸發(fā)后延遲 5s 加載我們的 service worker
在這里,我們可以看到如何使用 postTask 調(diào)度程序來延遲加載我們的 service worker。它將在 load 事件觸發(fā) 5 秒后加載,從而減少初始加載的成本。
const { scheduler } = usePostTaskScheduler({ priority: 'background' });
useEffect(() => {
scheduler.postTask(() => initializeServiceWorkers(path), {
delay: 5000,
event: 'load',
})
}, []);
未來展望
Chromium 是第一個(gè)實(shí)現(xiàn)和原型化這個(gè)新 API 的瀏覽器,但是該 API 正在 WICG 中公開開發(fā),旨在被所有瀏覽器標(biāo)準(zhǔn)化和采用。值得注意的是,即使沒有本地支持,我們也可以通過使用 polyfill 在 Safari 和 Chrome 等瀏覽器中看到許多性能改進(jìn),因?yàn)樗梢酝ㄟ^調(diào)度靈活的控制事件的優(yōu)先級(jí)。
參考:https://web.dev/optimize-long-tasks
譯者:@古茗科技
譯文:https://juejin.cn/post/7208732065696497723
作者:@Callie
原文:https://medium.com/airbnb-engineering/building-a-faster-web-experience-with-the-posttask-scheduler-
