整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          前端進階都應該了解的知識點 - INP

          介紹了交互至下一次繪制(INP)取代了首次輸入延遲(FID)作為核心 Web 性能指標。INP 衡量用戶與頁面元素交互時的頓挫感,考慮了交互的每個部分對性能的影響,包括輸入延遲、處理時間和呈現延遲。還指出了如何理解 JavaScript 的主線程執行模型以及如何優化 INP 以提升網頁性能。今日文章由 @飄飄翻譯分享。

          從 2023 年 3 月 12 日起,INP 將取代 "首次輸入延遲"(FID),成為核心 Web Vital 指標。

          FID 和 INP 在瀏覽器中衡量的是相同情況:當用戶與頁面上的元素交互時,感覺有多笨拙?對于 Web 及其用戶來說,好消息是 INP 通過考慮交互的每個部分和渲染響應,提供了更好的真實性能表現。

          對您來說這也是一個好消息:您為確保 FID 的良好分數而采取的措施,將使您在獲得可靠的 INP 分數的道路上邁出堅實的一步。當然,任何數字 -- 不管是令人舒心的綠色還是令人擔憂的紅色 -- 在不知道其確切來源的情況下,都不會有任何特別的用處。事實上,理解替換的最佳方法是更好地理解被替換的內容。正如前端性能的許多方面一樣,關鍵在于了解 JavaScript 如何使用主線程。正如您所想象的那樣,每個瀏覽器管理和優化任務的方式都略有不同,因此本文會將簡化一些概念,但請不要誤會,您對 JavaScript 的事件循環了解得越深入,就越能更好地處理各種前端性能工作。

          主線程

          您可能在過去聽說過將 JavaScript 描述為 "單線程" 的說法,雖然自 Web Worker 出現后,這種說法已不完全正確,但它仍然是描述 JavaScript 同步執行模式的有用方式。在一個給定的 "領域" 內,比如 iframe、瀏覽器標簽頁或 Web Worker,一次只能執行一個任務。在瀏覽器選項卡的上下文中,這種順序執行被稱為主線程,它與其他瀏覽器任務共享,如解析 HTML、某些 CSS 動畫以及渲染和重新渲染頁面的某些部分。

          JavaScript 使用一種名為 "調用棧"(或簡稱 "棧")的數據結構來管理 "執行上下文"-- 即當前主線程正在執行的代碼。當腳本啟動時,JavaScript 解釋器會創建一個 "全局上下文" 來執行代碼的主體 -- 任何存在于 JavaScript 函數之外的代碼。全局上下文被推送到調用棧,并在那里執行。

          當解釋器在全局上下文的執行過程中遇到函數調用時,它會暫停全局執行上下文,為該函數調用創建一個 "函數上下文"(有時也稱為 "局部上下文"),并將其推送到堆棧頂部,然后執行該函數。如果該函數調用包含一個函數調用,則會為其創建一個新的函數上下文,將其推到棧頂并立即執行。棧中最高的上下文總是當前正在執行的上下文,當執行結束時,它會從棧中彈出,這樣下一個最高的執行上下文就可以繼續執行 --"后進先出"。最終,執行將回到全局上下文,要么遇到另一個函數調用,執行將通過該函數調用和調用所包含的任何函數逐次向上和向下執行,要么全局上下文結束,調用棧清空。

          如果 "按照遇到的順序一個一個地執行每個函數" 就是全部內容,那么執行任何異步任務(例如從服務器獲取數據或觸發事件處理程序的回調函數)的函數都將是性能災難。該函數的執行上下文要么會阻塞執行,直到異步任務完成,該任務的回調函數啟動,要么會突然中斷任務完成時調用堆棧中的任何函數上下文。因此,除了堆棧之外,JavaScript 還使用了由 "事件循環" 和 "回調隊列"(或 "消息隊列")組成的事件驅動 "并發模型"。

          當異步任務完成并調用其回調函數時,該回調函數的函數上下文會被放入回調隊列,而不是調用棧的頂部 -- 它不會立即接管執行。位于回調隊列和調用棧之間的是事件循環,它不斷輪詢回調隊列中是否存在函數執行上下文,以及調用棧中是否有空位。如果回調隊列中有函數執行上下文在等待,而事件循環確定調用堆棧是空的,那么該函數執行上下文就會被推送到調用堆棧,并像同步調用一樣執行。

          例如,我們有一個腳本,使用老式的 setTimeout 在 500 毫秒后向控制臺記錄日志:

           setTimeout( function myCallback() {
               console.log( "Done." );
           }, 500 );
          
           // Output: Done.

          首先,為腳本正文創建一個全局上下文并執行。全局執行上下文會調用 setTimeout 方法,因此會在調用棧頂部創建 setTimeout 的函數上下文,并執行該函數 -- 這樣計時器就開始滴答作響了。然而,myCallback 函數并沒有被添加到堆棧中,因為它還沒有被調用。由于 setTimeout 沒有其他事情可做,它被從堆棧中彈出,全局執行上下文恢復。在全局上下文中沒有其他事情可做,所以它從堆棧中彈出,而堆?,F在是空的。

          現在,在這一系列事件中的任何時候,我們的定時器都會過期,從而調用 myCallback。此時,回調函數將被添加到回調隊列中,而不是被添加到堆棧中并中斷其他正在執行的操作。一旦調用棧為空,事件循環就會將 myCallback 的執行上下文推送到棧中執行。在這種情況下,主線程早在定時器結束前就完成了工作,而我們的回調函數會立即添加到空的調用棧中:

           const rightNow = performance.now();
          
           setTimeout( () => {
               console.log( `The callback function was executed after ${ performance.now() - rightNow } milliseconds.` );
           }, 500);
          
           // Output: The callback function was executed after 501.7000000476837 milliseconds.

          在主線程沒有其他事情可做的情況下,我們的回調會準時觸發,大約需要一兩毫秒。但是,一個復雜的 JavaScript 應用程序在全局執行上下文結束之前,可能有數以萬計的函數上下文需要執行,而瀏覽器的速度再快,這些事情也需要時間。因此,讓全局執行上下文通過一個 while 循環保持忙碌,并快速計數到 5 億 -- 這是一項漫長的任務,從而偽造出一個擁擠不堪的主線程。

           const rightNow = performance.now();
           let i = 0;
          
           setTimeout( function myCallback() {
             console.log( `The callback function was executed after ${ performance.now() - rightNow } milliseconds.`);
           }, 500);
          
           while( i < 500000000 ) {
             i++;
           }
           // Output: The callback function was executed after 1119.5999999996275 milliseconds.

          全局執行上下文再次創建并執行。幾行代碼后,它調用了 setTimeout 方法,因此在調用棧頂部創建了 setTimeout 的函數執行上下文,計時器開始滴答作響。setTimeout 的執行上下文完成并從堆棧中彈出,全局執行上下文恢復,我們的 while 循環開始計數。

          與此同時,500 毫秒計時器計時結束,myCallback 被添加到回調隊列中,但這次調用棧并不是空的,事件循環必須等待全局執行上下文的剩余時間,才能將 myCallback 移到棧中。與處理整個客戶端渲染的網頁所需的復雜處理相比,對于在現代筆記本電腦上運行的現代瀏覽器來說,"數到一個相當大的數字" 并不是最繁重的工作,但我們仍然可以看到結果上的巨大差異:在我的例子中,輸出顯示所需的時間是預期時間的兩倍多。

          現在,我們使用 setTimeout 是出于可預測性的考慮,但事件處理程序的工作方式也是一樣的:當 JavaScript 解釋器在全局或函數上下文中遇到事件處理程序時,事件就會被綁定,但與該事件監聽器相關的回調函數不會被添加到調用堆棧中,因為該回調函數尚未被調用 -- 直到事件觸發為止。一旦事件觸發,該回調函數就會添加到回調隊列中,就像我們的計時器耗盡一樣。那么,如果一個事件回調開始,比如說,當主線程被埋在長任務中時,會發生什么?為了讓一個 JavaScript 密集的頁面啟動并運行,需要進行數兆字節的函數調用。

          如果用戶立即點擊了這個 button 元素,回調函數的執行上下文就會創建并添加到回調隊列中,但在堆棧中有足夠空間之前,它無法被移動到堆棧中。從紙面上看,幾百毫秒似乎并不算什么,但用戶交互與交互結果之間的任何延遲都會對感知性能造成巨大影響 -- 問問小時候玩過任天堂的人就知道了。這就是 "首次輸入延遲":在主線程閑置的情況下,對用戶觸發事件處理程序的第一個點與調用事件處理程序回調函數的第一個機會之間的延遲進行測量。一個頁面在解析和執行大量 JavaScript 以獲得渲染和功能時會陷入困境,因此在調用堆棧中沒有空間讓事件處理程序的回調函數立即排隊,這意味著用戶交互與回調函數被調用之間的延遲會更長,頁面也會感覺緩慢、滯后。

          這就是 "首次輸入延遲"-- 一個非常重要的指標,但它并不能反映用戶體驗頁面的全部情況。

          什么是 INP?

          毫無疑問,事件與事件處理程序的回調函數執行之間的長時間延遲是不好的,但在現實世界中,"回調函數的執行上下文被移動到調用堆棧的機會" 并不是用戶點擊按鈕時想要的結果。真正重要的是交互與交互的可見結果之間的延遲。

          這就是 "下一次繪制的交互" 所要測量的內容:用戶交互與瀏覽器下一次繪制之間的延遲,即向用戶提供交互結果可視化反饋的最早機會。在用戶訪問頁面期間測量的所有交互中,交互延遲最差的交互將作為 INP 分數顯示,畢竟在追蹤和修復性能問題時,我們最好先處理壞消息。

          總而言之,交互有三個部分,所有這些部分都會影響頁面的 INP:輸入延遲、處理時間和呈現延遲。

          輸入延遲

          事件處理程序的回調函數從回調隊列到主線程需要多長時間?

          現在你對這個問題已經了如指掌 -- 它與 FID 曾經捕捉到的指標是一樣的。不過,INP 比 FID 更進一步:FID 僅基于用戶的第一次交互,而 INP 則考慮了用戶在頁面上的所有交互,以便更準確地反映頁面的總體響應速度。INP 跟蹤硬件或屏幕鍵盤上的任何點擊、敲擊和按鍵操作 -- 這些互動最有可能促使頁面發生可見的變化。

          處理時間

          與事件相關的回調函數運行需要多長時間?

          即使事件處理程序的回調函數立即啟動,該回調函數也會調用更多的函數,從而填滿調用堆棧,并與主線程上的其他工作競爭。

           const myButton = document.querySelector( "button" );
           const rightNow = performance.now();
          
           myButton.addEventListener( "click", () => {
               let i = 0;
               console.log( `The button was clicked ${ performance.now() - rightNow } milliseconds after the page loaded.` );
               while( i < 500000000 ) {
                   i++;
               }
               console.log( `The callback function was completed ${ performance.now() - rightNow } milliseconds after the page loaded.` );
           });
          
           // Output: The button was clicked 615.2000000001863 milliseconds after the page loaded.
           // Output: The callback function was completed 927.1000000000931 milliseconds after the page loaded.

          假設主線程中沒有其他阻塞并妨礙該事件處理程序的回調函數,那么該點擊處理程序的 FID 分數就會很高,但回調函數本身包含一個龐大而緩慢的任務,可能需要很長時間才能運行并向用戶顯示結果。緩慢的用戶體驗,用一個歡快的綠色結果來概括是不準確的。

          與 FID 不同,INP 將這些延遲也考慮在內。用戶交互會觸發多個事件 -- 例如,鍵盤交互會觸發按下、按上和按下事件。對于任何給定的交互,INP 都會捕捉 "交互延遲" 最長的事件的結果,即用戶交互與渲染響應之間的延遲。

          演示延遲

          主線程進行渲染和合成工作的速度如何?

          請記住,主線程不僅要處理 JavaScript,還要處理渲染。處理事件處理程序創建的所有任務所花費的時間現在都在與主線程的其他進程競爭,所有這些進程現在都在與繪制結果所需的布局和樣式計算競爭。

          測試與 INP 的相互作用

          現在,您已經對 INP 的測量方法有了更好的了解,是時候開始在現場收集數據和在實驗室進行修補了。

          對于 Chrome 瀏覽器用戶體驗報告數據集中包含的任何網站,PageSpeed Insights 都是開始了解網頁 INP 的好地方。要從各種不可知的連接速度、設備能力和用戶行為中收集真實世界的數據,Chrome 瀏覽器團隊的網絡生命周期 JavaScript 庫(或專注于性能的第三方用戶監控服務)可能是最好的選擇。

          然后,一旦您從現場測試中了解到您的網頁最大的 INP 問題,Web Vitals Chrome 擴展就可以讓您在瀏覽器中對交互進行測試、修補和重新測試 -- 雖然不如現場數據那樣具有代表性,但對于處理現場測試中出現的任何棘手的時間問題卻至關重要。

          優化 INP

          現在,您已經對 INP 在幕后的工作原理有了更好的了解,并能找出網頁中最大的 INP 問題所在,是時候開始整頓了。從理論上講,INP 的優化非常簡單:去掉那些冗長的任務,避免復雜的布局重新計算讓瀏覽器不堪重負。

          遺憾的是,簡單的概念在實踐中并不能轉化為快速、簡單的技巧。就像大多數前端性能工作一樣,優化 "Next Paint" 也是一個 "寸進" 的游戲 -- 測試、修補、重新測試,逐步將頁面調整到更小、更快、更尊重用戶時間和耐心的程度。

          Next Paint 的交互 (INP) 是一個新的 Core Web Vital 指標,專注于響應能力,計劃于 2024 年 3 月 12 日取代首次輸入延遲。使用正確的工具來監控和跟蹤 INP 可以更輕松地進行優化。

          什么是 Interaction to Next Paint (INP)?

          INP 衡量網站訪問者在執行單擊按鈕或鍵入等操作后等待的時間以及網站提供視覺反饋所需的時間。INP 是一個指標,顯示用戶交互后視覺反饋被阻止的時間量。

          廣告

          這個指標背后的想法是,一個無響應的網頁是一種糟糕的用戶體驗。例如,將產品添加到購物車中應立即產生視覺反饋響應,向網站訪問者顯示交互已得到響應。在該特定示例中,INP 不測量將產品添加到購物車所需的時間,它僅測量該操作的視覺反饋被阻止的時間。

          較低的 INP 分數意味著快速響應時間,這是目標。良好的 INP 分數是 200 毫秒以下的分數。

          JavaScript 和 CSS 是 INP 優化的主要目標。

          INP 測量以下用戶交互:

          • 鼠標點擊
          • 點擊具有觸摸屏的設備。
          • 按下鍵盤(物理鍵盤和虛擬鍵盤)

          廣告

          INP 測量和優化工具

          沒有工具可以單槍匹馬地解決 INP 問題,因為問題源于網頁上使用的主題、插件、特性和額外功能所使用的 JavaScript 和 CSS。

          例如,安裝和使用圖像輪播或動畫效果將加載額外的 JavaScript 和 CSS 代碼,這可能會對 INP 分數產生負面影響??s小 JavaScript 和 CSS 并不總是解決方案,這意味著優化 Interaction To Next Paint 的一個關鍵步驟是審核代碼并識別任何無助于網頁和用戶實現其目的的內容。

          因此,INP 優化工具的關鍵功能是識別阻礙或延遲用戶交互視覺反饋的原因。

          廣告

          下一個油漆診斷工具的五種交互

          1. Google 的 Site Kit – Analytics、Search Console、AdSense、Speed
          WordPress Plugin by Google

          Google 的 Site Kit 擁有超過 400 萬次 WordPress 安裝,是將 Google 搜索數據集成到 WordPress 儀表板中的最強大方法之一,以便在 WordPress 中輕松訪問。

          此工具顯示 PageSpeed Insights 和 Search Console 數據,包括有關改進措施的可操作建議。

          2. DebugBear 與下一個畫圖工具的交互(免費版和付費版)
          免費INP調試器

          DebugBear 是一種流行的頁面速度監控工具,它有一個專業版,提供計劃測試、事件通知、性能測試,在實時部署之前預覽影響,這是另一個好處。

          但它也提供免費工具,例如這個出色的 Interaction to Next Paint 工具,該工具將抓取網頁并診斷問題,并提供解決 Interaction To Next Paint 問題的可操作提示。

          3. Web Vitals Chrome 擴展程序

          此 Chrome 擴展程序提供核心 Web Vitals 指標,包括 INP。此擴展的一個有用功能是覆蓋網頁的獨特平視顯示器 (HUD),這在開發或更改網頁時會很有幫助。

          4. TREO網站速度

          Treo 網站速度工具提供令人難以置信的快速頁面速度工具,具有易于閱讀和理解的有吸引力的用戶界面。

          Treo 的一個有用功能是用戶可以調整選擇以查看特定國家/地區的指標。

          5. Chrome Web 指標庫

          有一個高級工具用于衡量來自實際網站訪問者的核心 Web 生命指標,這些指標可以由各個發布者部署在他們自己的 Web 服務器上。此工具可以使發布者查看真實的核心 Web 指標分數,這些分數對于解決實際網頁問題非常有用。此處提供了概述和解釋。

          獎勵工具

          Looker Studio(旁觀者工作室)

          Looker Studio(以前稱為 Google Data Studio)是一種數據可視化工具,使用戶能夠連接到數據源并通過易于理解的報告和儀表板可視化信息。只需導航到此處即可找到 Chrome UX 報告連接器,該連接器可以將 Chrome UX (CrUX) 原始數據轉換為報告,從而輕松查看真實世界的 CWV 趨勢。

          Google 在面向開發人員的 Chrome 頁面中發布了一個解釋器,展示了如何在 Looker 上設置 CrUX 儀表板??纯窗?,使用這個工具很容易成為超級明星 SEO!

          網站建設者平臺已準備就緒

          網站建設者平臺 Duda 預測了 Next Paint 的互動,并且已經努力確保使用其平臺的網站在新指標上得分很高。

          根據平臺戰略總監Russ Jeffery的說法:

          “網站性能對于依賴 Duda 為其客戶網站提供服務的代理商和 SaaS 公司來說非常重要。幫助他們的客戶在搜索中排名更高,提供更好的轉化率和用戶體驗,并最終推動更好的結果。

          在撰寫本文時,在Duda上構建的所有網站中,約有75%已經在INP的“良好”范圍內。

          基于 Duda 構建的網站還可以使用 Duda App Store 中內置的 WooRank 等第三方工具跟蹤和優化這些指標??蛻艨梢栽O置自定義 Google Analytics/Google 跟蹤代碼管理器事件,將這些指標發送到 GA4 中。

          為 INP 做好準備

          雖然 INP 可能不是直接的排名因素,但 INP 仍然是創建最快頁面體驗的有用指標,因為眾所周知,網站速度可以提高銷售額、點擊次數和廣告瀏覽量,并且它與 Google 用于排名的信號一致。

          情是這樣的:大家都知道“內存泄露”這回事吧。它有幾個常見的場景:

          1.閉包使用不當引起內存泄漏

          2.(未聲明的)全局變量

          3.分離的DOM節點

          4.(隨意的)控制臺的打印

          5.遺忘的定時器

          6.循環引用

          內存泄漏需要重視,它是如此嚴重甚至會導致頁面卡頓,影響用戶體驗!

          其中第 3 點引起了我的注意 —— 我當然清楚地知道它說的是比如:“假設你手動移除了某個dom節點,本應釋放該dom節點所占用的內存,但卻因為疏忽導致某處代碼仍對該被移除節點有引用,最終導致該節點所占內存無法被釋放”的情況

          <div id="root">
              <div class="child">我是子元素</div>
              <button>移除</button>
          </div>
          <script>
              let btn = document.querySelector('button')
              let child = document.querySelector('.child')
              let root = document.querySelector('#root')
              
              btn.addEventListener('click', function() {
                  root.removeChild(child)
              })
          </script>

          該代碼所做的操作就是點擊按鈕后移除.child的節點,雖然點擊后,該節點確實從dom被移除了,但全局變量child仍對該節點有引用,所以導致該節點的內存一直無法被釋放。

          解決辦法:我們可以將對.child節點的引用移動到click事件的回調函數中,那么當移除節點并退出回調函數的執行上文后就會自動清除對該節點的引用,自然也就不會存在內存泄漏的情況了。(這實際上是在事件中實時檢測該節點是否存在,如果不存在則瀏覽器必不會觸發remove函數的執行)

          <div id="root">
              <div class="child">我是子元素</div>
              <button>移除</button>
          </div>
          <script>
              let btn = document.querySelector('button')
              btn.addEventListener('click', function() {  
                  let child = document.querySelector('.child')
                  let root = document.querySelector('#root')
                  root.removeChild(child)
              })
          </script>


          這段代碼很完美么?不。因為它在每次事件觸發后都創建了對child和root節點的引用。消耗了內存(你完全可以想象一些人會狂點按鈕的情況…)。

          其實還有一種辦法:我們在click中去判斷當前root節點中是否還存在child的子節點,如果存在,則執行remove函數,否則什么也不做!

          這就引發了標題中所說的行為。

          怎么判斷?

          遍歷?不,太過麻煩!

          不知怎的,我突然想到了 for...in 中的 in 操作符,它可以基于原型鏈遍歷對象!

          我們來還原一下當時的場景:打開GitHub,隨便找一個父節點,并獲取它:

          圖中畫紅框的就是我們要取的父元素,橘紅色框的就是要判斷是否存在的子元素。

          let parent=document.querySelector('.position-relative');
          let child=document.querySelector('.progress-pjax-loader');


          這里注意,因為獲取到的是DOM節點(類數組對象),所以我們在操作前一定要先處理一下:

          let p_child=[...parent.children];

          然后

          console.log(child in p_child);

          為什么呢?(此時筆者還沒有意識到事情的嚴重性)

          我想,是不是哪里出了問題,用es6的includes API驗證一下:

          console.log(p_child.includes(child));


          沒錯?。?br>再用一般的數組驗證一下:

          再用一般的數組驗證一下:

          ???

          此時,筆者才想起到MDN上查閱一番:

          進而我發現:in操作符單獨使用時它檢測的是左側的值(作為索引)對應的值是否在右側的對象內部(屬性 & 原型上)!

          回到上面的代碼中,我們發現:

          這驗證了我們的結論。

          很顯然,“子元素”并不等同于“存在于原型鏈上” —— 這又引出了一個知識點:attribute和property的區別!

          所以經過一番“折騰”,源代碼還是應該直接這樣寫:

          <div id="root">
              <div class="child">我是子元素</div>
              <button>移除</button>
          </div>
          <script>
              let btn = document.querySelector('button')
              let child = document.querySelector('.child')
              let root = document.querySelector('#root')
              let r_child = [...root.children]
              
              btn.addEventListener('click', function() {
                  if(r_child.includes(child)){   // 或者你這里直接判斷child是否為null也可以...吧
                  	root.removeChild(child)
                  }
              })
          </script>

          原文鏈接:https://blog.csdn.net/qq_43624878/article/details/115591008


          主站蜘蛛池模板: 亚洲欧美国产国产一区二区三区| 国产精品一区二区av| 一区二区三区电影在线观看| 国产日韩AV免费无码一区二区 | 亚洲不卡av不卡一区二区| 伊人久久精品一区二区三区| 国产成人精品一区二区三区无码| 夜精品a一区二区三区| 亚洲一区电影在线观看| 国产凹凸在线一区二区| 亚洲日韩国产一区二区三区| 精品国产一区二区三区久久影院| 亚洲av日韩综合一区在线观看| 日韩一区二区精品观看| 天堂Av无码Av一区二区三区| 中文字幕日韩一区二区三区不卡| 波多野结衣免费一区视频 | 午夜精品一区二区三区在线视| 日本一区二区三区爆乳| 亚洲国产国产综合一区首页| 中文字幕在线精品视频入口一区| 亚洲成av人片一区二区三区 | 91精品一区二区三区在线观看| 国产在线观看一区二区三区精品 | 精品少妇ay一区二区三区| 久久国产免费一区二区三区 | 免费一区二区三区在线视频| 国产一区二区三区小说| 精品国产一区二区三区无码| 精品视频无码一区二区三区 | 少妇人妻精品一区二区| 亚洲一区二区三区丝袜| 亚洲熟妇av一区二区三区漫画| 亚洲视频免费一区| 天堂Aⅴ无码一区二区三区| 精品国产一区二区三区av片| 精品一区二区三区在线视频| 日韩色视频一区二区三区亚洲| 亚洲av一综合av一区| 一区二区三区免费视频播放器| 中文字幕av人妻少妇一区二区|