<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何減少網(wǎng)頁卡頓

          共 10532字,需瀏覽 22分鐘

           ·

          2024-04-11 17:26

          點(diǎn)擊上方 前端Q,關(guān)注公眾號(hào)

          回復(fù)加群,加入前端Q技術(shù)交流群

          前言

          經(jīng)常聽人說,“不要阻塞主線程”,或者 “減少長耗時(shí) ",該如何做呢?

          聊網(wǎng)站性能的文章有很多,通常為了提高 js 性能,避不開這兩點(diǎn):

          • 不要阻塞主線程

          • 減少長耗時(shí)

          該怎么做呢?很明顯,精簡 js 代碼有好處,但更少的代碼量是否就一定意味著用戶界面的體驗(yàn)會(huì)更順暢?可能會(huì),但也可能恰恰相反。

          要弄懂優(yōu)化 js 中任務(wù)的重要性,首先需要了解什么是任務(wù)、任務(wù)的角色以及瀏覽器的任務(wù)處理機(jī)制。

          瀏覽器中的任務(wù)

          瀏覽器執(zhí)行的任務(wù)之間是相互獨(dú)立的,像頁面渲染,html 和 css 的解析,以及執(zhí)行 js 代碼都屬于任務(wù)的范疇。雖然開發(fā)者不能直接控制這些任務(wù),但毫無疑問的是,瀏覽器中的任務(wù)主要源自開發(fā)者編寫和部署的代碼。

          圖 1

          上圖中的任務(wù)便是 chrome DevToos 性能剖析中點(diǎn)擊事件觸發(fā)的。從圖中能看到,任務(wù)在頂端,任務(wù)下面列出了點(diǎn)擊事件、調(diào)用的函數(shù),此外還調(diào)用很多其他方法。

          任務(wù)能影響性能的方式很多,比如在打開網(wǎng)站時(shí)下載 js 代碼,瀏覽器會(huì)把任務(wù)放到隊(duì)列中不執(zhí)行,而是準(zhǔn)備解析和編譯 js 而防止阻塞 js。之后網(wǎng)站上的任務(wù)才會(huì)因?yàn)橛脩艚换ヲ?qū)動(dòng)事件處理器、js 動(dòng)畫以及分析收集的后臺(tái)活
          動(dòng)等 js 活動(dòng)而觸發(fā)。(web worker 這種情況例外)

          什么是主線程?

          瀏覽器絕大多的任務(wù)都發(fā)生在主線程,其主線程名稱的由來也主要是因?yàn)椋簬缀跛?js 都在主線程運(yùn)行。

          主線程每次只能處理一個(gè)任務(wù),當(dāng)任務(wù)耗時(shí)超過特定時(shí)間,比如 50ms 就會(huì)被歸類為長耗時(shí)。如果發(fā)生長耗時(shí)時(shí)存在用戶交互,或者關(guān)鍵渲染更新時(shí),瀏覽器就會(huì)延后再處理用戶交互,這會(huì)直接導(dǎo)致用戶交互或者渲染出現(xiàn)延遲。

          谷歌性能剖析中的長耗時(shí)如圖所示,一般會(huì)在任務(wù)角上用紅色三角形標(biāo)出來,其中被阻塞的任務(wù)部分用紅色細(xì)斜條紋標(biāo)出來(如上圖所示)。

          優(yōu)化長耗時(shí),意味著將單個(gè)長耗時(shí)任務(wù)拆解成幾個(gè)耗時(shí)相對(duì)短的小任務(wù),可以查看下圖。

          在上圖中能看到單個(gè)長任務(wù)和被拆分成了 5 個(gè)短任務(wù)。

          為什么需要拆分任務(wù)非常重要?因?yàn)椴鸱珠L任務(wù)后,瀏覽器就有了更多的機(jī)會(huì),可以去處理優(yōu)先級(jí)別更高的工作,其中就包括用戶交互行為。

          如果任務(wù)非常長,瀏覽器對(duì)用戶交互的展示如圖所示,這時(shí)候?yàn)g覽器就沒法快速處理用戶交互,但拆分長任務(wù)后的從圖中能看到效果就不一樣。

          因?yàn)殚L任務(wù)的緣故,用戶交互產(chǎn)生的事件處理就必須排隊(duì),等待長任務(wù)執(zhí)行完后才能執(zhí)行。這個(gè)時(shí)候就會(huì)導(dǎo)致用戶交互的延遲。當(dāng)拆分成較短的任務(wù)后,事件處理器就有機(jī)會(huì)更快的觸發(fā)。

          因?yàn)槭录幚砥髂軌蛟诙倘蝿?wù)之間得以執(zhí)行,也就比長任務(wù)耗時(shí)更短。在長耗時(shí)的圖片中,用戶可能就會(huì)感到卡頓;長任務(wù)拆分后,用戶可能就感覺體驗(yàn)很流暢。

          然而問題來了,那就是 減少長耗時(shí)到底該怎么做,不要阻塞主線程寫的也不夠明確。這篇文章便為你解開這些神秘的面紗。

          任務(wù)管理策略

          軟件架構(gòu)中有時(shí)候會(huì)將一個(gè)任務(wù)拆分成多個(gè)函數(shù),這不僅能增強(qiáng)代碼可讀性,也讓項(xiàng)目更容易維護(hù),當(dāng)然這樣也更容易寫測試。

           function saveSettings () {
          validateForm();
          showSpinner();
          saveToDatabase();
          updateUI();
          sendAnalytics();
          }

          在上面的例子中,該函數(shù) saveSettings 調(diào)用了另外 5 個(gè)函數(shù),包括驗(yàn)證表單、展示加載的動(dòng)畫、發(fā)送數(shù)據(jù)到后端等。理論上講,這是很合理的架構(gòu)。如果需調(diào)試這些功能,也只需要在項(xiàng)目中查找每個(gè)函數(shù)即可。

          然而,這樣也有問題,就是 js 并不是為每個(gè)方法開辟一個(gè)單獨(dú)的任務(wù),因?yàn)檫@些方法都包含在 saveSetting 這個(gè)函數(shù)中,也就是說這五個(gè)方法在一個(gè)任務(wù)中執(zhí)行

          重點(diǎn)提示

          js 遵循執(zhí)行才編譯的原理,也就是說,只有一個(gè)任務(wù)結(jié)束才會(huì)執(zhí)行下一個(gè)任務(wù),而且不論這個(gè)任務(wù)會(huì)阻塞主線程多久。

          saveSetting 這個(gè)函數(shù)調(diào)用 5 個(gè)函數(shù),這個(gè)函數(shù)的執(zhí)行看起來就像一個(gè)特別長的長的任務(wù)。

          在很多場景中,單個(gè)函數(shù)耗時(shí)可能會(huì)超過 50ms,從而使得整體任務(wù)耗時(shí)更長。如果測試場景比較差,特別是在 “資源受限” 場景下測試的設(shè)備,每個(gè)函數(shù)可能耗時(shí)都會(huì)更久。接下來,將會(huì)分享優(yōu)化的策略。

          使用代碼延遲任務(wù)執(zhí)行

          為了拆分長任務(wù),開發(fā)者經(jīng)常使用定時(shí)器 setTimeout。通過把方法傳遞給 setTimeout,也就等同于重新創(chuàng)建
          了一個(gè)新的任務(wù),延遲了回調(diào)的執(zhí)行,而且使用該方法,即便是將 delay 時(shí)間設(shè)定成 0,也是有效的。

           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);
          }

          如果需執(zhí)行的函數(shù)先后關(guān)系是很明確,這個(gè)方法會(huì)非常有效,然而并不是所有場景都能使用這個(gè)方法。比如,如需要在循環(huán)中處理大數(shù)據(jù)量的數(shù)據(jù),這個(gè)任務(wù)的耗時(shí)可能就會(huì)非常長(假設(shè)有數(shù)百萬的數(shù)據(jù)量)

           function processData () {
          for (const item of largeDataArray) {
          // Process the individual item here.
          }
          }

          此時(shí),使用 setTimeout 就會(huì)出錯(cuò),因?yàn)樾试驘o法實(shí)行,而且雖然單獨(dú)處理每個(gè)數(shù)據(jù)耗時(shí)很短,但整個(gè)數(shù)組可能花費(fèi)特別長的時(shí)間。綜合來看,setTimeout 就不能算是特別有效的工具。

          除了 setTimeout 的方式,確有一些 api 能夠允許延遲代碼到隨后的任務(wù)中執(zhí)行。其中一個(gè)方式便是使用 postMessage 替代定時(shí)器;也可以使用 requestIdleCallback,但是需要注意這個(gè) api 編排的任務(wù)的優(yōu)先級(jí)別最低,而且只會(huì)在瀏覽器空閑時(shí)才會(huì)執(zhí)行。當(dāng)主線程繁忙時(shí),通過 requestIdleCallback 這個(gè) api 編排的任務(wù)可能永遠(yuǎn)不會(huì)執(zhí)行。

          使用 async、await 來創(chuàng)造讓步點(diǎn)

          在本文會(huì)出現(xiàn)一個(gè)新詞讓步,這個(gè)詞的定義、用法和意義可以通過代碼和介紹進(jìn)行闡述。

          重點(diǎn)提示

          當(dāng)讓步于主線程后,瀏覽器就有機(jī)會(huì)處理那些更重要的任務(wù),而不是放在隊(duì)列中排隊(duì)。理想狀態(tài)下,一旦出現(xiàn)用戶界面級(jí)別的任務(wù),就應(yīng)該讓步給主線程,讓任務(wù)更快的執(zhí)行完。讓步于主線程讓更重要的工作能更快的完成

          分解任務(wù)后,按照瀏覽器內(nèi)部的優(yōu)先級(jí)別劃分,其他的任務(wù)可能優(yōu)先級(jí)別調(diào)整的會(huì)更高。一種讓步于主線程的方式是配合用了 setTimeout 的 promise。

           function yieldToMain () {
          return new Promise(resolve => {
          setTimeout(resolve, 0);
          });
          }

          注意

          盡管這個(gè)例子在返回 promise 中通過 setimeout 來調(diào)用 resolve,但此時(shí)并不是新開一個(gè)任務(wù)讓 promise 執(zhí)行后續(xù)代碼,而是通過 setTimeout 調(diào)用。因?yàn)?promise 的回調(diào)屬于微任務(wù),因此不會(huì)讓步于主線程。

          在 saveSettings 的函數(shù)中,可以在每次 await 函數(shù) 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();
          }
          }

          重要提示

          并不是所有函數(shù)調(diào)用都要讓步于主線程。如果兩個(gè)函數(shù)的結(jié)果在用戶界面上有重要的更新,最好就不要這樣做。如果可以,可以想讓任務(wù)執(zhí)行,然后考慮在那些不重要的函數(shù)或者能在后臺(tái)運(yùn)行的函數(shù)之間讓步。

          這樣的好處是,就能看到單個(gè)大的長任務(wù)被拆分成了多個(gè)獨(dú)立的任務(wù)。

          現(xiàn)在能看到,saveSetting 這個(gè)函數(shù)內(nèi)的函數(shù)現(xiàn)在成為了單獨(dú)的任務(wù)。

          通過使用 promise 這種方式,和手動(dòng)寫 setTimeout 這種定時(shí)器方式相比,在工程上有跟多的好處。讓步的時(shí)間點(diǎn)變成了聲明式,因此這種代碼寫起來更容易,閱讀和理解也更輕松。

          只在必要時(shí)讓步

          假如有一堆的任務(wù),但是只想在用戶交互的時(shí)候才讓步,該怎么辦?正好有這種 api--isInputPending
          isInputPending 這個(gè)函數(shù)可以在任何時(shí)候調(diào)用,它能判斷用戶是否要與頁面元素進(jìn)行交互。調(diào)用
          isInputPending 會(huì)返回布爾值,true 代表要與頁面元素交互,false 則不交互。

          比如說,任務(wù)隊(duì)列中有很多任務(wù),但是不想阻擋用戶輸入,使用 isInputPending 和自定義方法 yieldToMain 方法,就能夠保證用戶交互時(shí)的 input 不會(huì)延遲。

           async function saveSettings () {
          // 函數(shù)隊(duì)列
          const tasks = [ validateForm, showSpinner, saveToDatabase, updateUI, sendAnalytics ];

          while (tasks.length > 0) {
          // 讓步于用戶輸入
          if (navigator.scheduling.isInputPending()) {
          // 如果有用戶輸入在等待,則讓步
          await yieldToMain();
          } else {
          // Shift the the task out of the queue:
          const task = tasks.shift();

          // Run the task:
          task();
          }
          }
          }

          在 saveSetting 執(zhí)行過程中,會(huì)逐個(gè)循環(huán)隊(duì)列中的任務(wù)。如果循環(huán)時(shí) isInputPending 結(jié)果返回真,saveSetting 就會(huì)調(diào)用 yieldToMain 函數(shù),這樣就能處理用戶輸入的事件,反之,就會(huì)走到隊(duì)列繼續(xù)執(zhí)行下一個(gè),直到隊(duì)列執(zhí)行完。

          saveSetting 這個(gè)任務(wù)隊(duì)列中有 5 個(gè)任務(wù),但此時(shí)如果正在執(zhí)行第二個(gè)任務(wù)而用戶想打開某個(gè)菜單,于是點(diǎn)擊了這個(gè)菜單,isInputPending 就會(huì)讓步,讓主線程處理交互事件,同時(shí)也會(huì)稍后執(zhí)行后面剩余的任務(wù)。

          用戶輸入后 isInputPending 的返回值不一定總是 “true”, 這是因?yàn)椴僮飨到y(tǒng)需要時(shí)間來通知瀏覽器交互結(jié)束,也就是說其他代碼可能已經(jīng)開始執(zhí)行,比如截圖例子中的 saveToDatabase 這個(gè)方法可能已經(jīng)在執(zhí)行了。即便使用 isInputPending,還是需要在每個(gè)方法限制任務(wù)中的方法數(shù)量。

          使用 isInputPending 配合讓步的策略,能讓瀏覽器有機(jī)會(huì)響應(yīng)用戶的重要交互,這在很多情況下,尤其是很多執(zhí)行很多任務(wù)時(shí),能夠提高頁面對(duì)用戶的響應(yīng)能力。

          另一種使用 isInputPending 的方式,特別是擔(dān)心瀏覽器不支持該策略,就可以使用另一種結(jié)合時(shí)間的方式。

           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 += 50;

          // Stop the execution of the current loop and
          // move onto the next iteration:
          continue;
          }

          // Shift the the task out of the queue:
          const task = tasks.shift();

          // Run the task:
          task();
          }
          }

          使用這種方式,通過結(jié)合時(shí)間來兼容不支持 isInputPending 的瀏覽器,尤其是使用截止時(shí)間或者在特定時(shí)間點(diǎn),讓工作能在適當(dāng)時(shí)候中斷,不論是通過讓步于用戶輸入還是在特定時(shí)間節(jié)點(diǎn)。

          幾個(gè) API 的差異

          目前提到的 api 對(duì)于拆解任務(wù)都有幫助,但也有弊端:讓步與主線程則意味著延遲代碼稍后執(zhí)行,即該部分代碼被添加到稍后的事件隊(duì)列中去了。

          如果能控制頁面中所有的代碼,就可以編排各個(gè)任務(wù)的優(yōu)先級(jí),但是第三方 js 腳本可能不會(huì)服從安排。實(shí)際上,也不可能真正意義上給所有的任務(wù)排優(yōu)先級(jí),而是只能讓他們成堆,或者是讓步于特定的用戶交互行為。

          幸運(yùn)的是,有一個(gè)專門編排優(yōu)先級(jí)的 api 正在開發(fā)中,相信能夠解決這些問題。

          專門編排優(yōu)先級(jí)的 api

          目前在書寫本文時(shí)該 api 提供 postTask 的功能,對(duì)于所有的 chromium 瀏覽器和 firefox 均可使用。

          postTask 允許更細(xì)粒度的編排任務(wù),該方法能讓瀏覽器編排任務(wù)的優(yōu)先級(jí),以便地優(yōu)先級(jí)別的任務(wù)能夠讓步于主線程。目前 postTask 使用 promise,接受優(yōu)先級(jí)這個(gè)參數(shù)設(shè)定。

          postTask 方法有三個(gè)優(yōu)先級(jí)別:

          • background 級(jí),適用于優(yōu)先級(jí)別最低的任務(wù)

          • user-visible 級(jí),適用于優(yōu)先級(jí)別中等的任務(wù),如果沒有入?yún)ⅲ彩窃摵瘮?shù)的默認(rèn)參數(shù)。

          • user-blocking 級(jí),適用于優(yōu)先級(jí)別最高的任務(wù)。

          拿下面的代碼來舉例,postTask 在三處分別都是最高優(yōu)先級(jí)別,其他的另外兩個(gè)任務(wù)優(yōu)先級(jí)別都是最低。

           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'});
          };

          在上面例子中,通過這些任務(wù)的優(yōu)先級(jí)的編排方式,能讓高瀏覽器級(jí)別的任務(wù),比如用戶交互等得以觸發(fā)。

          當(dāng) saveSettings 方法在執(zhí)行時(shí),會(huì)使用 postTask 來編排每個(gè)方法。關(guān)鍵的用戶側(cè)任務(wù)優(yōu)先級(jí)別高,當(dāng)然用戶并不知道的任務(wù)按照 background 的級(jí)別,這就可以 up 和提高優(yōu)先級(jí)。

          【第1920期】如何監(jiān)控網(wǎng)頁的卡頓?

          這是如何使用 postTask 的非常簡單的例子??梢杂貌煌?TaskController 對(duì)象來區(qū)分,這樣能在不同的人物之間共享優(yōu)先級(jí)別,也能為不同的 TaskController 的實(shí)例變更優(yōu)先級(jí)。

          重點(diǎn)提示
          postTask 并不是所有瀏覽器都支持??梢詸z測是否空,或者考慮使用 polyfill。

          內(nèi)置不中斷的讓步方法

          還有一個(gè)編排 api 目前還在提議階段,還沒有內(nèi)置到任何瀏覽器中。它的用法和本章和開始講到的 yieldToMain 這個(gè)方法類似。

           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,而是使用了 await scheduler.yield 方法。

          下面三幅圖分別是不使用 yield,使用 yield,以及使用 yield 且不中斷。不使用 yield,出現(xiàn)了長耗時(shí)任務(wù)。使用 yield,短任務(wù)數(shù)量變多了,而且還能被其他不相關(guān)的任務(wù)打斷。使用 yield 且不中斷,里面的短任務(wù)更多,但是執(zhí)行順序是固定的。

          上面便是三種情況的效果圖。使用 scheduler.yield 方法時(shí),任務(wù)能在每次讓步停止后重新開始。

          使用 scheduler.yield 的好處是不中斷,也就意味著如果是在一連串任務(wù)中 yield,那么從 yield 的時(shí)間點(diǎn)開始,其他編排好的任務(wù)的執(zhí)行會(huì)繼續(xù)。這就能避免第三方 js 腳本代碼阻塞代碼的執(zhí)行

          結(jié)語

          雖然管理任務(wù)富有挑戰(zhàn),但管理任務(wù)卻能受益頗多,網(wǎng)站能有更快的用戶交互體驗(yàn)。管理和調(diào)優(yōu)沒有萬靈藥,但確有一系列不同的技巧。最后總結(jié)重申一下,管理任務(wù)時(shí)主要需要考慮以下幾點(diǎn):

          • 遇到關(guān)鍵任務(wù)和用戶側(cè)的任務(wù)需要讓步于主線程

          • 使用 isInputPending 來讓步主線程讓用戶可以與頁面交互

          • 適應(yīng) postTask 來調(diào)整任務(wù)的優(yōu)先級(jí)

          • 最后,每個(gè)函數(shù)盡可能地減少活動(dòng)

          使用以上一個(gè)或多個(gè)方法,就能夠?qū)?yīng)用中的任務(wù)進(jìn)行管理,根據(jù)用戶需要調(diào)整優(yōu)先級(jí),同時(shí)能保證相對(duì)不那么重要的工作得以繼續(xù)執(zhí)行,這樣給創(chuàng)造更好的用戶體驗(yàn),網(wǎng)站響應(yīng)更快,使用更令人愉悅。

          關(guān)于本文
          譯者:@劉剛
          譯文:https://juejin.cn/post/7159807927908302884
          原文:
          https://web.dev/articles/optimize-long-tasks?hl=zh-cn


          往期推薦


          突破項(xiàng)目瓶頸:2024 年 Monorepo 工具選擇和實(shí)踐
          SessionStorage 90%的前端都未知的小秘密!
          rust 正在全面入侵前端

          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧

          瀏覽 108
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  狠狠人妻久久久久久综合99浪潮 | 亚洲综合免费 | 国产精品秘 入口免费直播大尺度 | 无码电影视频 | 操操网伊人网 |