<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                &emsp;&emsp;前端性能監控是個老話題了,各個團隊都會對其有所關注,因為關注性能是工程師的本分。 &emsp;&emsp;頁面性能對用戶體驗而言十分關鍵,每次重構或優化,僅靠手中的幾個設備或模擬的測試,缺少說服力,需要有大量的真實數據來做驗證。 &emsp;&emsp;在2016年,我就寫過一篇《[前端頁面性能參數搜集](https://www.cnblogs.com/strick/p/5750022.html)》的文章,當時采用的還是W3C性能參數的[第一版](https://www.w3.org/blog/2012/09/performance-timing-information/),現在已有[第二版](https://www.w3.org/TR/navigation-timing-2/)了。 &emsp;&emsp;在2020年,根據自己所學整理了一套監控系統,代號[菠蘿](https://github.com/pwstrick/pineapple),不過并沒有正式上線,所以只能算是個玩具。 &emsp;&emsp;這次不同,公司急切的需要一套性能監控系統,用于分析線上的活動,要扎扎實實的提升用戶體驗。 &emsp;&emsp;整個系統大致的運行流程如下: :-: ![](https://img.kancloud.cn/3f/d9/3fd9afa204a73c24cdc4e33b6d93706b_627x225.png =500x) &emsp;&emsp;2023-01-16 經過 TypeScript 整理重寫后,正式將監控系統的腳本開源,命名為?[shin-monitor](https://github.com/pwstrick/shin-monitor)。 ## 一、SDK &emsp;&emsp;性能參數搜集的代碼仍然寫在前面的監控 [shin.js](https://github.com/pwstrick/shin-admin/blob/main/public/shin.js)(SDK) 中,為了兼容兩個版本的性能標準,專門編寫了一個函數。 ~~~ function _getTiming() { var timing = performance.getEntriesByType("navigation")[0] || performance.timing; var now = 0; if (!timing) { return { now: now }; } var navigationStart; if (timing.startTime === undefined) { navigationStart = timing.navigationStart; /** * 之所以老版本的用 Date,是為了防止出現負數 * 當 performance.now 是最新版本時,數值的位數要比 timing 中的少很多 */ now = new Date().getTime() - navigationStart; } else { navigationStart = timing.startTime; now = shin.now() - navigationStart; } return { timing: timing, navigationStart: navigationStart, now: _rounded(now) }; } ~~~ &emsp;&emsp;其實兩種方式得當的參數類似,第二版中的參數比第一版來的多,下面兩張圖是官方給的參數示意圖,粗看的話下面兩種差不多。 :-: ![](https://box.kancloud.cn/2016-05-18_573be5286b049.png =800x) W3C第一版的性能參數 :-: ![](https://img.kancloud.cn/81/53/81536b373d68112de61e4e7de6472804_1582x502.png =800x) W3C第二版的性能參數 &emsp;&emsp;但其實在將 performance.getEntriesByType("navigation")\[0\] 打印出來后,就會發現它還會包含頁面地址、傳輸的數據量、協議等字段。 **1)統計的參數** &emsp;&emsp;網上有很多種統計性能參數的計算方式,大部分都差不多,我選取了其中較為常規的參數。 ~~~ shin.getTimes = function () { // 出于對瀏覽器兼容性的考慮,仍然引入即將淘汰的 performance.timing var currentTiming = _getTiming(); var timing = currentTiming.timing; // var timing = performance.timing; var api = {}; // 時間單位 ms if (!timing) { return api; } var navigationStart = currentTiming.navigationStart; /** * http://javascript.ruanyifeng.com/bom/performance.html * 頁面加載總時間,有可能為0,未觸發load事件 * 這幾乎代表了用戶等待頁面可用的時間 * loadEventEnd(加載結束)-navigationStart(導航開始) */ api.loadTime = timing.loadEventEnd - navigationStart; /** * Unload事件耗時 */ api.unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart; /** * 執行 onload 回調函數的時間 * 是否太多不必要的操作都放到 onload 回調函數里執行了,考慮過延遲加載、按需加載的策略么? */ api.loadEventTime = timing.loadEventEnd - timing.loadEventStart; /** * 首次可交互時間 */ api.interactiveTime = timing.domInteractive - timing.fetchStart; /** * 用戶可操作時間(DOM Ready時間) * 在初始HTML文檔已完全加載和解析時觸發(無需等待圖像和iframe完成加載) * 緊跟在DOMInteractive之后。 * https://www.dareboost.com/en/doc/website-speed-test/metrics/dom-content-loaded-dcl */ api.domReadyTime = timing.domContentLoadedEventEnd - timing.fetchStart; /** * 白屏時間 * FP(First Paint)首次渲染的時間 */ var paint = performance.getEntriesByType("paint"); if (paint && timing.entryType && paint[0]) { api.firstPaint = paint[0].startTime - timing.fetchStart; api.firstPaintStart = paint[0].startTime; // 記錄白屏時間點 } else { api.firstPaint = timing.responseEnd - timing.fetchStart; } /** * FCP(First Contentful Paint)首次有實際內容渲染的時間 */ if (paint && timing.entryType && paint[1]) { api.firstContentfulPaint = paint[1].startTime - timing.fetchStart; api.firstContentfulPaintStart = paint[1].startTime; // 記錄白屏時間點 } else { api.firstContentfulPaint = 0; } /** * 解析DOM樹結構的時間 * DOM中的所有腳本,包括具有async屬性的腳本,都已執行。加載DOM中定義的所有頁面靜態資源(圖像、iframe等) * loadEventStart緊跟在domComplete之后。在大多數情況下,這2個指標是相等的。 * 在加載事件開始之前可能引入的唯一額外延遲將由onReadyStateChange的處理引起。 * https://www.dareboost.com/en/doc/website-speed-test/metrics/dom-complete */ api.parseDomTime = timing.domComplete - timing.domInteractive; /** * 請求完畢至DOM加載耗時 * 在加載DOM并執行網頁的阻塞腳本時觸發 * 在這個階段,具有defer屬性的腳本還沒有執行,某些樣式表加載可能仍在處理并阻止頁面呈現 * https://www.dareboost.com/en/doc/website-speed-test/metrics/dom-interactive */ api.initDomTreeTime = timing.domInteractive - timing.responseEnd; /** * 準備新頁面耗時 */ api.readyStart = timing.fetchStart - navigationStart; /** * 重定向次數(新) */ api.redirectCount = timing.redirectCount || 0; /** * 傳輸內容壓縮百分比(新) */ api.compression = (1 - timing.encodedBodySize / timing.decodedBodySize) * 100 || 0; /** * 重定向的時間 * 拒絕重定向!比如,http://example.com/ 就不該寫成 http://example.com */ api.redirectTime = timing.redirectEnd - timing.redirectStart; /** * DNS緩存耗時 */ api.appcacheTime = timing.domainLookupStart - timing.fetchStart; /** * DNS查詢耗時 * DNS 預加載做了么?頁面內是不是使用了太多不同的域名導致域名查詢的時間太長? * 可使用 HTML5 Prefetch 預查詢 DNS,參考:http://segmentfault.com/a/1190000000633364 */ api.lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart; /** * SSL連接耗時 */ var sslTime = timing.secureConnectionStart; api.connectSslTime = sslTime > 0 ? timing.connectEnd - sslTime : 0; /** * TCP連接耗時 */ api.connectTime = timing.connectEnd - timing.connectStart; /** * 內容加載完成的時間 * 頁面內容經過 gzip 壓縮了么,靜態資源 css/js 等壓縮了么? */ api.requestTime = timing.responseEnd - timing.requestStart; /** * 請求文檔 * 開始請求文檔到開始接收文檔 */ api.requestDocumentTime = timing.responseStart - timing.requestStart; /** * 接收文檔(內容傳輸耗時) * 開始接收文檔到文檔接收完成 */ api.responseDocumentTime = timing.responseEnd - timing.responseStart; /** * 讀取頁面第一個字節的時間,包含重定向時間 * TTFB 即 Time To First Byte 的意思 * 維基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte */ api.TTFB = timing.responseStart - timing.redirectStart; /** * 僅用來記錄當前 performance.now() 獲取到的時間格式 * 用于追溯計算 */ api.now = shin.now(); // 全部取整 for (var key in api) { api[key] = _rounded(api[key]); } /** * 瀏覽器讀取到的性能參數,用于排查,并保留兩位小數 */ api.timing = {}; for (var key in timing) { const timingValue = timing[key]; const type = typeof timingValue; if (type === "function") { continue; } api.timing[key] = timingValue; if (type === "number") { api.timing[key] = _rounded(timingValue, 2); } } return api; }; ~~~ &emsp;&emsp;所有的性能參數最終都要被取整,以毫秒作單位。兼容的 timing 對象也會被整個傳遞到后臺,便于分析性能參數是怎么計算出來的。 &emsp;&emsp;compression(傳輸內容壓縮百分比)是一個[新的參數](https://developer.mozilla.org/en-US/docs/Web/Performance/Navigation_and_resource_timings#compression)。 &emsp;&emsp;白屏時間的計算有兩種: 1. 第一種是調用 [performance.getEntriesByType("paint")](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformancePaintTiming)方法,再減去 fetchStart; 2. 第二種是用 responseEnd 來與 fetchStart 相減。 &emsp;&emsp;在實踐中發現,每天有大概 2 千條記錄中的白屏時間為 0,而且清一色的都是蘋果手機,一番搜索后,了解到。 &emsp;&emsp;當 iOS 設備通過瀏覽器的前進或后退按鈕進入頁面時,fetchStart、responseEnd 等性能參數很可能為 0。 &emsp;&emsp;2023-01-19 發現當初始頁面的結構中,若包含漸變的效果時,1 秒內的白屏占比會從最高 94% 降低到 85%。 &emsp;&emsp;loadTime(頁面加載總時間)有可能為0,就是當頁面資源還沒加載完,觸發 load 事件前將頁面關閉。 &emsp;&emsp;如果這種很多,那就很有可能頁面被阻塞在某個位置,可能是接收時間過長、可能是DOM解析過長等。 &emsp;&emsp;當這個頁面加載時間超過了用戶的心理承受范圍時,就需要抽出時間來做各個方面的頁面優化了。 &emsp;&emsp;注意,在調用 performance.getEntriesByType("paint") 方法后,可以得到一個數組,第一個元素是白屏對象,第二個元素是[FCP](https://developer.mozilla.org/en-US/docs/Glossary/First_contentful_paint)對象。 &emsp;&emsp;FCP(First Contentful Paint)是首次有實際內容渲染的時間,于 2022-08-16 新增該指標。 &emsp;&emsp;在上線一段時間后,發現有大概 50% 的記錄,load 和 ready 是小于等于 0。查看原始的性能參數,如下所示。 ~~~ { "unloadEventStart": 0, "unloadEventEnd": 0, "domInteractive": 0, "domContentLoadedEventStart": 0, "domContentLoadedEventEnd": 0, "domComplete": 0, "loadEventStart": 0, "loadEventEnd": 0, "type": "navigate", "redirectCount": 0, "initiatorType": "navigation", "nextHopProtocol": "h2", "workerStart": 0, "redirectStart": 0, "redirectEnd": 0, "fetchStart": 0.5, "domainLookupStart": 3.7, "domainLookupEnd": 12.6, "connectStart": 12.6, "connectEnd": 33.3, "secureConnectionStart": 20.4, "requestStart": 33.8, "responseStart": 44.1, "responseEnd": 46, "transferSize": 1207, "encodedBodySize": 879, "decodedBodySize": 2183, "serverTiming": [], "name": "https://www.xxx.me/xx.html", "entryType": "navigation", "startTime": 0, "duration": 0 } ~~~ &emsp;&emsp;發現 domContentLoadedEventEnd 和 loadEventEnd 都是 0,一開始懷疑參數的問題。 &emsp;&emsp;舊版的[performance.timing](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming)已經被廢棄,新版通過 performance.getEntriesByType('navigation')\[0\] 得到一個[PerformanceNavigationTiming](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming)對象,它繼承自[PerformanceResourceTiming](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming)類,但沒查到有什么問題。 &emsp;&emsp;還有一種情況,就是 DOMContentLoaded 與 load 兩個事件都沒有觸發。關于兩者的觸發時機,網上的[一篇文章](https://juejin.cn/post/6844903623583891469)總結道: * load 事件會在頁面的 HTML、CSS、JavaScript、圖片等靜態資源都已經加載完之后才觸發。 * DOMContentLoaded 事件會在 HTML 加載完畢,并且 HTML 所引用的內聯 JavaScript、以及外鏈 JavaScript 的同步代碼都執行完畢后觸發。 &emsp;&emsp;在網上搜索一圈后,沒有發現阻塞的原因,那很有可能是自己代碼的問題。 &emsp;&emsp;經查,是調用 JSBridge 的一種同步方式阻塞了兩個事件的觸發。代碼中的 t 就是一條鏈接。 ~~~ window.location.href = t ~~~ &emsp;&emsp;在加載腳本時,就會觸發某些 JSBridge,而有些手機就會被阻塞,有些并不會。解決方案就是將同步的跳轉改成異步的,如下所示。[鏈接]() ~~~ var iframe = document.createElement("iframe"); iframe.src = t; document.body.append(iframe); ~~~ &emsp;&emsp;值得一提的是,在將此問題修復后,首屏 1 秒內的占比從 66.7% 降到了 48.4%,2 秒內的占比從 20.7% 升到了 25.5%,3、4、4+ 秒的占比也都提升了。 **2)首屏時間** &emsp;&emsp;首屏時間很難計算,一般有幾種計算方式。 &emsp;&emsp;第一種是算出首屏頁面中所有圖片都加載完后的時間,這種方法難以覆蓋所有場景(例如 CSS 中的背景圖、Image 元素等),并且計算結果并不準。 ~~~ /** * 計算首屏時間 * 記錄首屏圖片的載入時間 * 用戶在沒有滾動時候看到的內容渲染完成并且可以交互的時間 */ doc.addEventListener( "DOMContentLoaded", function () { var isFindLastImg = false, allFirsrImgsLoaded = false, firstScreenImgs = []; //用一個定時器差值頁面中的圖像元素 var interval = setInterval(function () { //如果自定義了 firstScreen 的值,就銷毀定時器 if (shin.firstScreen) { clearInterval(interval); return; } if (isFindLastImg) { allFirsrImgsLoaded = firstScreenImgs.every(function (img) { return img.complete; }); //當所有的首屏圖像都載入后,關閉定時器并記錄首屏時間 if (allFirsrImgsLoaded) { shin.firstScreen = _calcCurrentTime(); clearInterval(interval); } return; } var imgs = doc.querySelectorAll("img"); imgs = [].slice.call(imgs); //轉換成數組 //遍歷頁面中的圖像 imgs.forEach(function (img) { if (isFindLastImg) return; //當圖像離頂部的距離超過屏幕寬度時,被認為找到了首屏的最后一張圖 var rect = img.getBoundingClientRect(); if (rect.top + rect.height > firstScreenHeight) { isFindLastImg = true; return; } //若未超過,則認為圖像在首屏中 firstScreenImgs.push(img); }); }, 0); }, false ); ~~~ &emsp;&emsp;第二種是自定義首屏時間,也就是自己來控制何時算首屏全部加載好了,這種方法相對來說要精確很多。 ~~~ shin.setFirstScreen = function() { this.firstScreen = _calcCurrentTime(); } /** * 計算當前時間與 fetchStart 之間的差值 */ function _calcCurrentTime() { return _getTiming().now; } /** * 標記時間,單位毫秒 */ shin.now = function () { return performance.now(); } ~~~ &emsp;&emsp;之所以未用 Date.now() 是因為它會受系統程序執行阻塞的影響, 而performance.now() 的時間是以恒定速率遞增的,不受系統時間的影響(系統時間可被人為或軟件調整)。 &emsp;&emsp;在頁面關閉時還未獲取到首屏時間,那么它就默認是 domReadyTime(用戶可操作時間)。 &emsp;&emsp;首屏時間(screen)有可能是負數,例如返回上一頁、刷新當前頁后,馬上將頁面關閉,此時 screen 的值取自 domReadyTime。 &emsp;&emsp;domReadyTime 是由 domContentLoadedEventEnd 和 fetchStart 相減而得到,domContentLoadedEventEnd 可能是 0,fetchStart 是個非 0 值。 &emsp;&emsp;這樣就會得到一個負值,不過總體占比并不高,每天在 300 條上下,0.3% 左右。 &emsp;&emsp;2023-01-06 去掉了這兩種首屏算法,因為默認會采用 LCP 或 FMP 的計算結果。 **3)上報** &emsp;&emsp;本次上報與之前不同,需要在頁面關閉時上報。而在此時普通的請求可能都無法發送成功,那么就需要[navigator.sendBeacon()](https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon)的幫忙了。 &emsp;&emsp;它能將少量數據異步 POST 到后臺,并且支持跨域,而少量是指多少并沒有特別指明,由瀏覽器控制,網上查到的資料說一般在 64KB 左右。 &emsp;&emsp;在接收數據時遇到個問題,由于后臺使用的是 KOA 框架,解析請求數據使用了 koa-bodyparser 庫,而它默認不會接收?Content-Type: text 的數據,因此要額外配置一下,具體可[參考此處](https://stackoverflow.com/questions/53591683/how-to-access-request-payload-in-koa-web-framework)。 ~~~ /** * 在頁面卸載之前,推送性能信息 */ window.addEventListener("beforeunload", function () { var data = shin.getTimes(); if (shin.param.rate > Math.random(0, 1) && shin.param.pkey) { navigator.sendBeacon(shin.param.psrc, _paramifyPerformance(data)); } }, false ); ~~~ &emsp;&emsp;在上報時,還限定了一個采樣率,默認只會把 50% 的性能數據上報到后臺,并且必須定義 pkey 參數,這其實就是一個用于區分項目的 token。 &emsp;&emsp;本來一切都是這么的順利,但是在實際使用中發現,在 iOS 設備上調試發現不會觸發 beforeunload 事件,安卓會將其觸發,一番查找后,根據[iOS支持的事件](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html#//apple_ref/doc/uid/TP40006511-SW5)和[社區的解答](https://stackoverflow.com/questions/3239834/window-onbeforeunload-not-working-on-the-ipad),發現得用[pagehide](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/pagehide_event)事件替代。 &emsp;&emsp;以為萬事大吉,但還是太年輕,在微信瀏覽器中的確能觸發 pagehide 事件,但是在自己公司APP中,表現不盡如意,無法觸發,若要監控關閉按鈕,得發一次版本。 &emsp;&emsp;無奈,只能自己想了個比較迂回的方法,那就是在后臺跑個定時器,每 200ms 緩存一次要搜集的性能數據,在第二次進入時,再上報到后臺。 ~~~ /** * 組裝性能變量 */ function _paramifyPerformance(obj) { obj.token = shin.param.token; obj.pkey = shin.param.pkey; obj.identity = getIdentity(); obj.referer = location.href; // 來源地址 // 取 FCM、LCP 和用戶可操作時間中的最大值 obj.firstScreen = Math.max.call( undefined, shin.fmp.time, shin.lcp.time, obj.domReadyTime ); obj.timing.lcp = shin.lcp; //記錄LCP對象 obj.timing.fmp = shin.fmp; //記錄FMP對象 obj.timing.fid = shin.fid; //記錄FID對象 // 靜態資源列表 var resources = performance.getEntriesByType("resource"); var newResources = []; resources && resources.forEach(function (value) { // 過濾 fetch 請求 if (value.initiatorType === "fetch") return; // 只存儲 1 分鐘內的資源 if (value.startTime > 60000) return; newResources.push({ name: value.name, duration: _rounded(value.duration), startTime: _rounded(value.startTime) }); }); obj.resource = newResources; return JSON.stringify(obj); } /** * 均勻獲得兩個數字之間的隨機數 */ function _randomNum(max, min) { return Math.floor(Math.random() * (max - min + 1) + min); } /** * iOS 設備不支持 beforeunload 事件,需要使用 pagehide 事件 * 在頁面卸載之前,推送性能信息 */ var isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); var eventName = isIOS ? "pagehide" : "beforeunload"; var isNeedHideEvent = true; // 是否需求觸發隱藏事件 window.addEventListener(eventName, function () { isNeedHideEvent && sendBeacon(); }, false ); /** * 在 load 事件中,上報性能參數 * 該事件不可取消,也不會冒泡 */ window.addEventListener("load", function () { /** * 監控頁面奔潰情況 * 原先是在 DOMContentLoaded 事件內觸發,經測試發現,當因為腳本錯誤出現白屏時,兩個事件的觸發時機會很接近 * 在線上監控時發現會有一些誤報,HTML是有內容的,那很可能是 DOMContentLoaded 觸發時,頁面內容還沒渲染好 */ setTimeout(function () { monitorCrash(shin.param); }, 1000); // 加定時器是避免在上報性能參數時,loadEventEnd 為 0,因為事件還沒執行完畢 setTimeout(function () { sendBeacon(); }, 0); }); // var SHIN_PERFORMANCE_DATA = 'shin_performance_data'; // var heartbeat; // 心跳定時器 /** * 發送 64KB 以內的數據 */ function sendBeacon(existData) { // 如果傳了數據就使用該數據,否則讀取性能參數,并格式化為字符串 var data = existData || _paramifyPerformance(shin.getTimes()); var rate = _randomNum(10, 1); // 選取1~10之間的整數 if (shin.param.rate >= rate && shin.param.pkey) { navigator.sendBeacon(shin.param.psrc, data); } // clearTimeout(heartbeat); // localStorage.removeItem(SHIN_PERFORMANCE_DATA); // 移除性能緩存 isNeedHideEvent = false; } /** * 發送已存在的性能數據 */ // function sendExistData() { // var exist = localStorage.getItem(SHIN_PERFORMANCE_DATA); // if (!exist) { return; } // setTimeout(function() { // sendBeacon(exist); // }, 0); // } // sendExistData(); /** * 一個心跳回調函數,緩存性能參數 * 適用于不能觸發 pagehide 和 beforeunload 事件的瀏覽器 */ // function intervalHeartbeat() { // localStorage.setItem(SHIN_PERFORMANCE_DATA, _paramifyPerformance(shin.getTimes())); // } // heartbeat = setInterval(intervalHeartbeat, 200); ~~~ &emsp;&emsp;2023-01-06 去掉了對性能參數的緩存,因為在 load 事件中也會上報性能參數。 &emsp;&emsp;并且也是為了減少對 FMP 的計算次數,消除不必要的性能損耗,避免錯誤的計算結果,故而做出了此決定。 &emsp;&emsp;注意,首屏最終會取 FMP、LCP 和用戶可操作時間中的最大值。 **4)LCP** &emsp;&emsp;2022-07-12 新增該指標,LCP(Largest Contentful Paint)是指最大的內容在可視區域內變得可見的時間點。 &emsp;&emsp;在 MDN 網站中,有一段[LCP](https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint)的計算示例,在此基礎之上,做了些兼容性判斷。 &emsp;&emsp;通過[PerformanceObserver.observe()](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver/observe)監控 LCP。entries 是一組[LargestContentfulPaint](https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint)類型的對象,它有一個 url 屬性,如果記錄的元素是個圖像,那么會存儲其地址。 ~~~ var lcp; function getLCP() { var types = PerformanceObserver.supportedEntryTypes; var lcpType = 'largest-contentful-paint'; // 瀏覽器兼容判斷 if(types.indexOf(lcpType) === -1) { return; } new PerformanceObserver((entryList) => { var entries = entryList.getEntries(); var lastEntry = entries[entries.length - 1]; lcp = lastEntry.renderTime || lastEntry.loadTime; // 斷開此觀察者的連接,因為回調僅觸發一次 obs.disconnect(); // buffered 為 true 表示調用 observe() 之前的也算進來 }).observe({type: lcpType, buffered: true}); } getLCP(); ~~~ &emsp;&emsp;在 iOS 的 WebView 中,只支持三種類型的 entryType,不包括 largest-contentful-paint,所以加了段瀏覽器兼容判斷。并且 entries 是一組[PerformanceResourceTiming](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceResourceTiming)類型的對象。 &emsp;&emsp;在《[Largest Contentful Paint 最大內容繪制](https://web.dev/i18n/zh/lcp/)》中提到,選項卡和頁面轉移到后臺后,得停止 LCP 的計算,因此需要找到隱藏到后臺的時間。 ~~~ let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity; // 記錄頁面隱藏時間 iOS 不會觸發 visibilitychange 事件 const onVisibilityChange = (event) => { // 頁面不可見狀態 if (lcp && document.visibilityState === 'hidden') { firstHiddenTime = event.timeStamp; // 移除事件 document.removeEventListener('visibilitychange', onVisibilityChange, true); } } document.addEventListener('visibilitychange', onVisibilityChange, true); ~~~ &emsp;&emsp;利用[visibilitychange](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/visibilitychange_event)事件,就能準備得到隱藏時間,然后在讀取 LCP 時,大于這個時間的就直接忽略掉。不過在實踐中發現,在 iOS 的 WebView 中并不支持此事件。 &emsp;&emsp;公司要監測的 H5 頁面主要在移動平臺,選項卡的情況比較少,并且頁面結構比較簡單,一般都不會很久就能加載完成。 &emsp;&emsp;largest-contentful-paint 不會計算 iframe 中的元素,返回上一頁也不會重新計算。不過,我們的頁面中基本不會加載 iframe,并且頁面都是以單頁的活動為主,跳轉也比較少。 &emsp;&emsp;有個成熟的庫:[web-vitals](https://github.com/GoogleChrome/web-vitals),提供了 LCP、FID、CLS、FCP 和 TTFB 指標,對上述所說的特殊場景做了處理,若要了解原理,可以參考其中的計算過程。 &emsp;&emsp;注意,LCP 會被一直監控(其監控的元素如下所列),這樣會影響結果的準確性。例如有個頁面首次進入是個彈框,確定后會出現動畫,增加些圖片,DOM結構也都會跟著改變。 * img 元素 * 內嵌在 svg 中的 image 元素 * video 元素(使用到封面圖片) * 擁有背景圖片的元素(調用 CSS 的 url() 函數) * 包含文本節點或或行內文本節點的塊級元素 &emsp;&emsp;如果在關閉頁面時上報,那么 LCP 將會很長,所以需要選擇合適的上報時機,例如 load 事件中。 ~~~ window.addEventListener("load", function () { sendBeacon(); }, false ); ~~~ &emsp;&emsp;優化后還有 5、6 千條記錄中的 load 是 0。查看參數記錄發現 loadEventEnd 是 0,而 loadEventStart 有時候是 0,有時候有值。 &emsp;&emsp;可以在 load 事件中加個定時器,避免在上報性能參數時,loadEventEnd 為 0,因為此時事件還沒執行完畢。 ~~~ window.addEventListener("load", function () { setTimeout(function () { sendBeacon(); }, 0); }, false ); ~~~ &emsp;&emsp;優化后,白屏 1 秒內的占比從 74.2% 提升到了 92.4%,首屏 1 秒內的占比從 48.6% 提升到了 78.8%。 &emsp;&emsp;2022-11-30 發現讓 PerformanceObserver 的回調僅觸發一次而得到的結果會不準,例如有一個頁面,默認展示的是一張表示空內容的圖片,然后再去請求列表信息。 &emsp;&emsp;那么第一次回調中選取的最大內容將是這張圖片,而用戶真正關心的其實是那個后請求的列表。 &emsp;&emsp;所以暫停 LCP 的讀取時機要修改一下,參考[web-vitals](https://github.com/GoogleChrome/web-vitals/blob/main/src/onLCP.ts#L92)的代碼,當有按鍵或點擊(包括滾動)時,就停止 LCP 的采樣。 ~~~ function getLCP() { var lcpType = "largest-contentful-paint"; var isSupport = checkSupportPerformanceObserver(lcpType); // 瀏覽器兼容判斷 if (!isSupport) { return; } var po = new PerformanceObserver(function (entryList) { var entries = entryList.getEntries(); var lastEntry = entries[entries.length - 1]; shin.lcp = { time: _rounded(lastEntry.renderTime || lastEntry.loadTime), // 取整 url: lastEntry.url, element: lastEntry.element ? lastEntry.element.outerHTML : "" }; }); // buffered 為 true 表示調用 observe() 之前的也算進來 po.observe({ type: lcpType, buffered: true }); /** * 當有按鍵或點擊(包括滾動)時,就停止 LCP 的采樣 * once 參數是指事件被調用一次后就會被移除 */ ["keydown", "click"].forEach((type) => { window.addEventListener( type, function () { // 斷開此觀察者的連接 po.disconnect(); }, { once: true, capture: true } ); }); } ~~~ **5)FID** &emsp;&emsp;這個[FID](https://developer.mozilla.org/en-US/docs/Glossary/First_input_delay)(First Input Delay)是用戶第一次與頁面交互(例如點擊鏈接、按鈕等操作)到瀏覽器對交互作出響應的時間,于 2022-08-16 新增該指標。 &emsp;&emsp;延遲時間越長,用戶體驗越差。減少站點初始化時間和消除冗長的任務有助于消除首次輸入延遲。 &emsp;&emsp;計算方式和 LCP 類似,也是借助 PerformanceObserver 實現,提煉了一個通用的判斷方法 checkSupportPerformanceObserver()。 ~~~ /** * 判斷當前宿主環境是否支持 PerformanceObserver * 并且支持某個特定的類型 */ function checkSupportPerformanceObserver(type) { if (!PerformanceObserver) return false; var types = PerformanceObserver.supportedEntryTypes; // 瀏覽器兼容判斷 if (types.indexOf(type) === -1) { return false; } return true; } /** * 瀏覽器 FID 計算 * FID(First Input Delay)用戶第一次與頁面交互到瀏覽器對交互作出響應的時間 * https://developer.mozilla.org/en-US/docs/Glossary/First_input_delay */ function getFID() { var fidType = "first-input"; var isSupport = checkSupportPerformanceObserver(fidType); // 瀏覽器兼容判斷 if (!isSupport) { return; } new PerformanceObserver(function (entryList, obs) { const firstInput = entryList.getEntries()[0]; // 測量第一個輸入事件的延遲 shin.fid = _rounded(firstInput.processingStart - firstInput.startTime); // 斷開此觀察者的連接,因為回調僅觸發一次 obs.disconnect(); }).observe({ type: fidType, buffered: true }); } getFID(); ~~~ &emsp;&emsp;還有一個與交互有關的指標:[TTI](https://developer.mozilla.org/en-US/docs/Glossary/Time_to_interactive),TTI(Time to Interactive)可測量頁面從開始加載到主要子資源完成渲染,并能夠快速、可靠地響應用戶輸入所需的時間。 &emsp;&emsp;它的計算規則比較繁瑣: 1. 先找到 FCP 的時間點。 2. 沿時間軸正向搜索時長至少為 5 秒的安靜窗口,其中安靜窗口的定義為:沒有長任務([Long Task](https://developer.mozilla.org/en-US/docs/Web/API/Long_Tasks_API))且不超過兩個正在處理的網絡 GET 請求。 3. 沿時間軸反向搜索安靜窗口之前的最后一個長任務,如果沒有找到長任務,則在 FCP 處終止。 4. TTI 是安靜窗口之前最后一個長任務的結束時間,如果沒有找到長任務,則與 FCP 值相同。 &emsp;&emsp;下圖有助于更直觀的了解上述步驟,其中數字與步驟對應,豎的橙色虛線就是 TTI 的時間點。 :-: ![](https://img.kancloud.cn/33/d2/33d2c362f6556e444962965eacffc07b_1654x993.png =600x) &emsp;&emsp;TBT(Total Blocking Time)是指頁面從 FCP 到 TTI 之間的阻塞時間,一般用來量化主線程在空閑之前的繁忙程度。 &emsp;&emsp;它的計算方式就是取 FCP 和 TTI 之間的所有長任務消耗的時間總和。 &emsp;&emsp;不過網上[有些資料](https://web.dev/tti/)認為 TTI 可能會受當前環境的影響而導致測量結果不準確,因此更適合在實驗工具中測量,例如[LightHouse](https://github.com/GoogleChrome/lighthouse)、[WebPageTest](https://www.webpagetest.org/)等。 &emsp;&emsp;Google 的[TTI Polyfill](https://github.com/GoogleChromeLabs/tti-polyfill)庫的第一句話就是不建議在線上搜集 TTI,建議使用 FID。 &emsp;&emsp;下表是關鍵指標的基準線,參考字節的標準。 | Metric Name | Good(ms) | Needs Improvement(ms) | Poor(ms) | | --- | --- | --- | --- | | FP | 0-1000 | 1000-2500 | Over 2500 | | FCP | 0-1800 | 1800-3000 | Over 3000 | | LCP | 0-2500 | 2500-4000 | Over 4000 | | TTI | 0-3800 | 3800-7300 | Over 7300 | | FID | 0-100 | 100-300 | Over 300 | **6)FMP** &emsp;&emsp;2023-01-06 研究了網上開源的各類算法中,初步總結了一套計算 FMP 的步驟。 &emsp;&emsp;首先,通過[MutationObserver](https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver)監聽每一次頁面整體的 DOM 變化,觸發 MutationObserver 的回調。 &emsp;&emsp;然后在回調中,為每個 HTML 元素(不包括忽略的元素)打上標記,記錄元素是在哪一次回調中增加的,并且用數組記錄每一次的回調時間。 ~~~ var IGNORE_TAG_SET = ["SCRIPT", "STYLE", "META", "HEAD", "LINK"]; var WW = window.innerWidth; var WH = window.innerHeight; var cacheTrees = []; // 緩存每次更新的DOM元素 var FMP_ATTRIBUTE = "_ts"; var fmpObserver; var callbackCount = 0; /** * 開始監控DOM的變化 */ function initFMP() { fmpObserver = new MutationObserver(() => { var mutationsList = []; // 為 HTML 元素打標記,記錄是哪一次的 DOM 更新 var doTag = function (target, callbackCount) { var childrenLen = target.children ? target.children.length : 0; // 結束遞歸 if (childrenLen === 0) return; for (var children = target.children, i = childrenLen - 1; i >= 0; i--) { var child = children[i]; var tagName = child.tagName; if ( child.getAttribute(FMP_ATTRIBUTE) === null && IGNORE_TAG_SET.indexOf(tagName) === -1 // 過濾掉忽略的元素 ) { child.setAttribute(FMP_ATTRIBUTE, callbackCount); mutationsList.push(child); // 記錄更新的元素 } // 繼續遞歸 doTag(child, callbackCount); } }; // 從 body 元素開始遍歷 document.body && doTag(document.body, callbackCount++); cacheTrees.push({ ts: performance.now(), children: mutationsList }); }); fmpObserver.observe(document, { childList: true, // 監控子元素 subtree: true // 監控后代元素 }); } initFMP(); ~~~ &emsp;&emsp;接著在觸發 load 事件時,先過濾掉首屏外和沒有高度的元素,以及元素列表之間有包括關系的祖先元素,再計算各次變化時剩余元素的總分。 &emsp;&emsp;之前是只記錄沒有后代的元素,但是后面發現有時候 DOM 變化時,沒有這類元素。 ~~~ /** * 是否超出屏幕外 */ function isOutScreen(node) { var { left, top } = node.getBoundingClientRect(); return WH < top || WW < left; } /** * 讀取 FMP 信息 */ function getFMP() { fmpObserver.disconnect(); // 停止監聽 var maxObj = { score: -1, //最高分 elements: [], // 首屏元素 ts: 0 // DOM變化時的時間戳 }; // 遍歷DOM數組,并計算它們的得分 cacheTrees.forEach((tree) => { var score = 0; // 首屏內的元素 var firstScreenElements = []; tree.children.forEach((node) => { // 只記錄元素 if (node.nodeType !== 1 || IGNORE_TAG_SET.indexOf(node.tagName) >= 0) { return; } var { height } = node.getBoundingClientRect(); // 過濾高度為 0,在首屏外的元素 if (height > 0 && !isOutScreen(node)) { firstScreenElements.push(node); } }); // 若首屏中的一個元素是另一個元素的后代,則過濾掉該祖先元素 firstScreenElements = firstScreenElements.filter((node) => { // 只要找到一次包含關系,就過濾掉 var notFind = !firstScreenElements.some( (item) => node !== item && node.contains(item) ); // 計算總得分 if (notFind) { score += caculateScore(node); } return notFind; }); // 得到最高值 if (maxObj.score < score) { maxObj.score = score; maxObj.elements = firstScreenElements; maxObj.ts = tree.ts; } }); // 在得分最高的首屏元素中,找出最長的耗時 return getElementMaxTimeConsuming(maxObj.elements, maxObj.ts); } ~~~ &emsp;&emsp;不同類型的元素,權重也是不同的,權重越高,對頁面呈現的影響也越大。 &emsp;&emsp;在 caculateScore() 函數中,通過[getComputedStyle](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/getComputedStyle)得到 CSS 類中的背景圖屬性,注意,node.style 只能得到內聯樣式中的屬性。 ~~~ var TAG_WEIGHT_MAP = { SVG: 2, IMG: 2, CANVAS: 4, OBJECT: 4, EMBED: 4, VIDEO: 4 }; /** * 計算元素分值 */ function caculateScore(node) { var { width, height } = node.getBoundingClientRect(); var weight = TAG_WEIGHT_MAP[node.tagName] || 1; if ( weight === 1 && window.getComputedStyle(node)["background-image"] && // 讀取CSS樣式中的背景圖屬性 window.getComputedStyle(node)["background-image"] !== "initial" ) { weight = TAG_WEIGHT_MAP["IMG"]; //將有圖片背景的普通元素 權重設置為img } return width * height * weight; } ~~~ &emsp;&emsp;最后在得到分數最大值后,從這些元素中挑選出最長的耗時,作為 FMP。 ~~~ /** * 讀取首屏內元素的最長耗時 */ function getElementMaxTimeConsuming(elements, observerTime) { // 記錄靜態資源的響應結束時間 var resources = {}; // 遍歷靜態資源的時間信息 performance.getEntries().forEach((item) => { resources[item.name] = item.responseEnd; }); var maxObj = { ts: observerTime, element: "" }; elements.forEach((node) => { var stage = node.getAttribute(FMP_ATTRIBUTE); ts = stage ? cacheTrees[stage].ts : 0; // 從緩存中讀取時間 switch (node.tagName) { case "IMG": ts = resources[node.src]; break; case "VIDEO": ts = resources[node.src]; !ts && (ts = resources[node.poster]); // 讀取封面 break; default: // 讀取背景圖地址 var match = window.getComputedStyle(node)["background-image"].match(/url\(\"(.*?)\"\)/); if (!match) break; var src; // 判斷是否包含協議 if (match && match[1]) { src = match[1]; } if (src.indexOf("http") == -1) { src = location.protocol + match[1]; } ts = resources[src]; break; } if (ts > maxObj.ts) { maxObj.ts = ts; maxObj.element = node; } }); return maxObj; } ~~~ ## 二、存儲 **1)性能數據日志** &emsp;&emsp;性能數據會被存儲到 web\_performance 表中,同樣在接收時會通過隊列來異步新增。 ~~~ CREATE TABLE `web_performance` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `load` int(11) NOT NULL DEFAULT '0' COMMENT '頁面加載總時間', `ready` int(11) NOT NULL DEFAULT '0' COMMENT '用戶可操作時間', `paint` int(11) NOT NULL DEFAULT '0' COMMENT '白屏時間', `screen` int(11) NOT NULL DEFAULT '0' COMMENT '首屏時間', `measure` varchar(1000) COLLATE utf8mb4_bin NOT NULL COMMENT '其它測量參數,用JSON格式保存', `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `day` int(11) NOT NULL COMMENT '格式化的天(冗余字段),用于排序,20210322', `hour` tinyint(2) NOT NULL COMMENT '格式化的小時(冗余字段),用于分組,11', `minute` tinyint(2) DEFAULT NULL COMMENT '格式化的分鐘(冗余字段),用于分組,20', `identity` varchar(30) COLLATE utf8mb4_bin NOT NULL COMMENT '身份', `project` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '項目關鍵字,關聯 web_performance_project 表中的key', `ua` varchar(600) COLLATE utf8mb4_bin NOT NULL COMMENT '代理信息', `referer` varchar(200) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '來源地址', `referer_path` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '來源地址中的路徑', `timing` text COLLATE utf8mb4_bin COMMENT '瀏覽器讀取到的性能參數,用于排查', `resource` text COLLATE utf8mb4_bin COMMENT '靜態資源信息', PRIMARY KEY (`id`), KEY `idx_project_day` (`project`,`day`), KEY `idx_project_day_hour` (`project`,`day`,`hour`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='性能監控' ~~~ &emsp;&emsp;表中的 project 字段會關聯 web\_performance\_project 表中的key。 &emsp;&emsp;2023-01-09 增加 referer_path 字段,用于分析指定頁面的性能。 **2)性能項目** &emsp;&emsp;性能項目就是要監控的頁面,與之前不同,性能的監控粒度會更細,因此需要有個后臺專門管理這類數據。 ~~~ CREATE TABLE `web_performance_project` ( `id` int(11) NOT NULL AUTO_INCREMENT, `key` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '唯一值', `name` varchar(45) COLLATE utf8mb4_bin NOT NULL COMMENT '項目名稱', `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1:正常 0:刪除', PRIMARY KEY (`id`), UNIQUE KEY `name_UNIQUE` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='性能監控項目'; ~~~ &emsp;&emsp;目前做的也比較簡單,通過名稱得到 16位MD5 字符串,引入 Node.js 的 cryto 庫。 ~~~ const crypto = require('crypto'); const key = crypto.createHash('md5').update(name).digest('hex').substring(0, 16); ~~~ &emsp;&emsp;2022-11-25 新增查詢、分頁和編輯,允許更改項目名稱,但是 key 要保持不變。 &emsp;&emsp;可以對長期維護的網頁創建單獨的性能項目,對于那些臨時活動可以共用一個項目。 :-: ![](https://img.kancloud.cn/f5/63/f5636f7c5be6f22f2402305caa9ea352_1914x1244.png =800x) ## 三、分析 **1)性能看板** &emsp;&emsp;在性能看板中,會有四張折線圖,當要統計一天的數據時,橫坐標為小時(0~23),縱坐標為在這個小時內正序后處于 95% 位置的日志,也就是 95% 的用戶打開頁面的時間。 &emsp;&emsp;這種寫法也叫 TP95,TP 是 Top Percentile 的縮寫,不用性能平均數是因為那么做不科學。 :-: ![](https://img.kancloud.cn/44/42/444255b86a98cef43fed79d353c850c4_1872x1270.png =800x) &emsp;&emsp;過濾條件還可以選擇具體的小時,此時橫坐標為分鐘,縱坐標為在這個分鐘內正序后處于 95% 位置的日志。 &emsp;&emsp;點擊圖表的 label 部分,可以在后面列表中顯示日志細節,其中原始參數就是從瀏覽器中得到的計算前的性能數據。 :-: ![](https://img.kancloud.cn/06/cb/06cbfae371ce714df1b4703260bf9727_2962x888.png =800x) &emsp;&emsp;后面又增加了對比功能,就是將幾天的數據放在一起對比,可更加直觀的展示趨勢。 :-: ![](https://img.kancloud.cn/ba/58/ba584023a63e72c7623bff690ef4d4c7_2946x998.png =800x) **2)定時任務** &emsp;&emsp;在每天的凌晨 3點30 分,統計昨天的日志信息。 &emsp;&emsp;本來是計劃 web\_performance\_statis 表中每天只有一條記錄,所有性能項目的統計信息都塞到 statis 字段中,并且會包含各個對應的日志。 &emsp;&emsp;但奈何數據量實在太大,超出了 MySQL 中 TEXT 類型的范圍,沒辦法塞進去,后面就只存儲 id 并且一個項目每天各一條記錄。 &emsp;&emsp;數據結構如下,其中 loadZero 是指未執行load事件的數量。 ~~~ { hour: { x: [11, 14], load: ["158", "162"], ready: ["157", "162"], paint: ["158", "162"], screen: ["157", "162"], loadZero: 1 }, minute: { 11: { x: [11, 18, 30], load: ["157", "159", "160"], ready: ["156", "159", "160"], paint: ["157", "159", "160"], screen: ["156", "159", "160"], loadZero: 1 }, 14: { x: [9, 16, 17, 18], load: ["161", "163", "164", "165"], ready: ["161", "163", "164", "165"], paint: ["161", "163", "164", "165"], screen: ["161", "163", "164", "165"], loadZero: 0 } } } ~~~ &emsp;&emsp;還有個定時任務會在每天的凌晨 4點30 分執行,將四周前的 web\_performance\_statis 和 web\_performance 兩張表中的數據清除。 **3)資源瀑布圖** &emsp;&emsp;2022-07-08 新增了資源瀑布圖的功能,就是查看當時的資源加載情況。 &emsp;&emsp;在上報性能參數時,將靜態資源的耗時,也一起打包。[getEntriesByType()](https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/getEntriesByType)?方法可返回給定類型的 PerformanceEntry 列表。 ~~~ const resources = performance.getEntriesByType('resource'); const newResources = []; resources && resources.forEach(value => { const { name, duration, startTime, initiatorType} = value; // 過濾 fetch 請求 if(initiatorType === 'fetch') return; // 只存儲 1 分鐘內的資源 if(startTime > 60000) return; newResources.push({ name, duration: Math.round(duration), startTime: Math.round(startTime), }) }); obj.resource = newResources; ~~~ &emsp;&emsp;代碼中會過濾掉 fetch 請求,因為我本地業務請求使用的是[XMLHTTPRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest),只在上報監控數據時采用了 fetch() 函數。 &emsp;&emsp;并且我只會搜集 1 分鐘內的資源,1 分鐘以外的資源我都會舍棄,只記錄名稱、耗時和開始時間。 &emsp;&emsp;最終的效果如下圖所示,包含一個橫向的柱狀圖和查詢區域,目前只開放了根據 ID 查詢。 :-: ![](https://img.kancloud.cn/c7/5c/c75c43f3d4040d49c8424bc87db4b0de_1714x1074.png =800x) &emsp;&emsp;2022-08-17 在資源瀑布圖中標注白屏和首屏的時間點,可對資源的加載做更直觀的比較,便于定位性能問題。 &emsp;&emsp;2022-12-28 在資源瀑布圖中,增加 load 和 DOMContentLoaded 兩個事件觸發的時間點。 :-: ![](https://img.kancloud.cn/ca/ce/caceb72013f65604ca7c5239866c0e81_2030x1006.png =800x) **4)堆疊柱狀圖** &emsp;&emsp;先將所有的性能記錄統計出來,然后分別統計白屏和首屏 1 秒內的數量、1-2 秒內、2-3 秒內、3-4 秒內、4+秒的數量,白屏的 SQL 如下所示。 ~~~ SELECT COUNT(*) FROM `web_performance` WHERE `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` <= 1000 and `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` > 1000 and `paint` <= 2000 and `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` > 2000 and `paint` <= 3000 and `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` > 3000 and `paint` <= 4000 and `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` > 4000 `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; ~~~ &emsp;&emsp;算出后,分母為總數,分子為上述五個值,組成一張堆疊柱狀圖,類似于下面這樣,每種顏色代碼一個占比。 :-: ![](https://img.kancloud.cn/c1/a8/c1a8c221a50165ea7c4d050c37a3d67f_800x531.png =600x) &emsp;&emsp;這樣就能直觀的看到優化后的性能變化了,更快的反饋優化結果。 &emsp;&emsp;2024-03-19 將白屏和首屏的堆疊柱狀圖修改成堆疊面積圖,為了能更好的查看變化趨勢。 :-: ![](https://img.kancloud.cn/f0/90/f0902ad8a4a7613dae5f9e4cc68deae8_1682x796.png =600x) **5)階段時序圖** &emsp;&emsp;在將統計的參數全部計算出來后,為了能更直觀的發現性能瓶頸,設計了一張階段時序圖。 &emsp;&emsp;描繪出 TTFB、responseDocumentTime、initDomTreeTime、parseDomTime 和 loadEventTime 所占用的時間,如下所示。 &emsp;&emsp;橙色豎線表示白屏時間,黑色豎線表示首屏時間。移動到 id 或來源地址,就會提示各類參數。 :-: ![](https://img.kancloud.cn/d9/47/d947b7f1216baf7e50101fff03017710_2978x1082.png =800x) &emsp;&emsp;2023-01-09 增加身份和來源路徑兩個條件,當查到某條錯誤日志后,可以通過身份將兩類日志關聯,查看當時的性能數據。 &emsp;&emsp;來源路徑便于查看某一張頁面的性能日志,便于做針對性的優化。 :-: ![](https://img.kancloud.cn/75/8d/758d46f9d79ae88b42a6736856e26802_1456x282.png =800x) &emsp;&emsp;TTFB 的計算包括 redirectTime、appcacheTime、lookupDomainTime、connectTime 以及 requestDocumentTime 的和。 &emsp;&emsp;并且因為 requestStart 和 connectEnd 之間的時間(即 TCP 連接建立后到發送請求這段時間)沒有算,所以會比這個和大。 &emsp;&emsp;responseDocumentTime 就是接收響應內容的時間。initDomTreeTime 是構建 DOM 樹并執行網頁阻塞的腳本的時間,在這個階段,具有 defer 屬性的腳本還沒有執行。 &emsp;&emsp;parseDomTime 是解析 DOM 樹結構的時間,DOM 中的所有腳本,包括具有 async 屬性的腳本也會執行,還會加載頁面中的靜態資源,例如圖像、iframe 等。 &emsp;&emsp;parseDomTime 是 domComplete 和 domInteractive 相減得到的差。loadEventStart 會緊跟在 domComplete 之后,而在大多數情況下,這 2 個指標是相等的。 &emsp;&emsp;loadEventTime 就是執行 onload 事件的時間,一般都比較短。 &emsp;&emsp;觀察下來,如果是 TTFB 比較長,那么就是 NDS 查詢、TCP 連接后的請求等問題。 &emsp;&emsp;initDomTreeTime 過長的話,就需要給腳本瘦身了;parseDomTime過長的話,就需要減少資源的請求。 **參考:** [前端性能監控及推薦幾個開源的監控系統](https://cloud.tencent.com/developer/news/682347) [如何進行 web 性能監控?](http://www.alloyteam.com/2020/01/14184/) [螞蟻金服如何把前端性能監控做到極致?](https://www.infoq.cn/article/dxa8am44oz*lukk5ufhy) [5 分鐘擼一個前端性能監控工具](https://juejin.cn/post/6844903662020460552) [10分鐘徹底搞懂前端頁面性能監控](https://zhuanlan.zhihu.com/p/82981365) [Navigation\_and\_resource\_timings](https://developer.mozilla.org/en-US/docs/Web/Performance/Navigation_and_resource_timings) [PerformanceNavigationTiming](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceNavigationTiming) [Time to Interactive: Focusing on the Human-Centric Metrics](https://calibreapp.com/blog/time-to-interactive) [Time to Interactive Explainer](https://github.com/WICG/time-to-interactive) [前端監控 SDK 的技術要點原理分析](https://www.freecodecamp.org/chinese/news/tech-analysis-of-front-end-monitoring-sdk) ***** > 原文出處: [博客園-從零開始搞系列](https://www.cnblogs.com/strick/category/1928903.html) 已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。 ![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200) 推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看