從一個假死頁面引發的思考: 作為前端開發,除了要攻克頁面難點,也要有更深的自我目標,效能優化是自我提升中很重要的一環; 在前端開發中,會偶遇到頁面假死的現象, 是因為當js有大量計算時,會造成 UI 阻塞,出現介面卡頓、掉幀等情況,嚴重時會出現頁面卡死的情況;
程序與執行緒的區別
執行緒具有許多傳統程序所具有的特徵,故又稱為輕型程序(Light—Weight Process)或程序元;而把傳統的程序稱為重型程序(Heavy—Weight Process),它相當於只有一個執行緒的任務。在引入了執行緒的作業系統中,通常一個程序都有若干個執行緒,至少包含一個執行緒。
◦根本區別:程序是作業系統資源分配的基本單位,而執行緒是處理器任務排程和執行的基本單位;
◦資源開銷:每個程序都有獨立的程式碼和資料空間(程式上下文),程式之間的切換會有較大的開銷;執行緒可以看做輕量級的程序,同一類執行緒共用程式碼和資料空間,每個執行緒都有自己獨立的執行棧和程式計數器(PC),執行緒之間切換的開銷小。
◦包含關係:如果一個程序內有多個執行緒,則執行過程不是一條線的,而是多條線(執行緒)共同完成的;執行緒是程序的一部分,所以執行緒也被稱為輕權程序或者輕量級程序。
◦記憶體分配:同一程序的執行緒共用本程序的地址空間和資源,而程序之間的地址空間和資源是相互獨立的;
◦影響關係:一個程序崩潰後,在保護模式下不會對其他程序產生影響,但是一個執行緒崩潰整個程序都死掉。所以多程序要比多執行緒健壯。
◦執行過程:每個獨立的程序有程式執行的入口、順序執行序列和程式出口。但是執行緒不能獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制,兩者均可並行執行;
=> 回到阻塞原因分析
瀏覽器有GUI渲染執行緒與JS引擎執行緒,這兩個執行緒是互斥的關係,當JS引擎執行時GUI執行緒會被掛起(相當於被凍結了),GUI更新會被儲存在一個佇列中,等到JS引擎空閒時,立即被執行。js引擎不是每次在執行更新dom語句時,都會停下來等Gui渲染引擎更新完dom再執行後面的js程式碼; 詳見GUI渲染執行緒與JS引擎執行緒
1、performance Api
錄製步驟: 重新整理頁面載入流水線詳情頁面,然後找到某個用例技術端,進行結果更新;因此下面的圖分3個部分,第一個是詳情頁渲染; 第二段是需要大量計算的用例渲染部分; 第三段是操作結果,資料重新重新整理,重新計算和渲染的部分;
其中,FPS上方顯示紅色條的時候,意味著影格率特別低,使用者體驗特別不好。一般來說,綠色條越高,FPS越高。 分析效能圖示第一眼看FPS;效能面板底部,圖形圖表的色彩越多,意味著CPU效能已經達到極限。當我們看到CPU長時間處於最大值狀態,就需要考慮怎樣去優化
介面的監控 + 詳情頁渲染 + 用例渲染;
上面的Group面板非常有用。我們可以很清晰明瞭得分析按照活動,目錄,域,子域,URL和Frame進行分組的前端效能;
Bottom-Up: 是The Heavy (Bottom Up) view is available in the Bottom-Up tab,類似事件冒泡;
Call Tree:是And the Tree (Top Down) view is available in the Call Tree tab,類似事件捕獲;
更新結果,頁面重新繪製,計算;
黃色(Scripting):JavaScript執行
紫色(Rendering):樣式計算和佈局,即重排
藍色(Loading):網路通訊和HTML解析
綠色(Painting):重繪
灰色(System):其它事件花費的時間
白色(Idle):空閒時間: 可能是同時請求了很多介面,promise.all需要等待所有介面返回成功後才會渲染頁面,idle時間變長了很多,瀏覽器一直在等待介面全部返回;
用例列表和新增按鈕部分產生了偏移, 紅色Layout Shift;
2、lightHouse分析資料;分數很低;
3、performace monitor: 載入過程中,CPU飆80%;
主要可改善指標如下:
相關名詞解析:https://web.dev/metrics/;
指標 | 指標解析 |
---|---|
Self Time | Self Time代表函數本身執行消耗時間 |
Total Time | Total Time則是函數本身消耗再加上在呼叫它的函數中消耗的總時間 |
Activity | 瀏覽器活動的意思 |
DOM GC | DOM垃圾回收 |
Timer Fried | 銷燬計時器 |
XMR Load | 非同步載入物件載入 |
Major GC | 清理年老區(Tenured space) |
Minor GC | 每次Minor GC只會清理年輕代 |
Run Microtasks | 執行微服務 |
Recalculate Style | |
HitTest | https://www.jianshu.com/p/f6aff12fc08b |
DCLDomContentloaded | 當 HTML 檔案被完全載入和解析完成之後,DOMContentLoaded 事件被觸發,無需等待樣式表、影象和子框架的完成載入. |
SI (Speed Index) | 指標用於顯示頁面可見部分的顯示速度, 單位是時間, |
FPFirst Paint 首次繪製 | 首次繪製(FP)這個指標用於記錄頁面第一次繪製畫素的時間,如顯示頁面背景色。FP不包含預設背景繪製,但包含非預設的背景繪製。 |
FCPFirst contentful paint | 首次內容繪製 (FCP): LCP是指頁面開始載入到最大文字塊內容或圖片顯示在頁面中的時間。如果 FP 及 FCP 兩指標在 2 秒內完成的話我們的頁面就算體驗優秀。 |
LCPLargest contentful paint | 最大內容繪製 (LCP): 用於記錄視窗內最大的元素繪製的時間,該時間會隨著頁面渲染變化而變化,因為頁面中的最大元素在渲染過程中可能會發生改變,另外該指標會在使用者第一次互動後停止記錄。官方推薦的時間區間,在 2.5 秒內表示體驗優秀 |
FID(First input delay) | 首次輸入延遲,FID(First Input Delay),記錄在 FCP 和 TTI 之間使用者首次與頁面互動時響應的延遲 |
TTITime to Interactive | 可互動時間 (TTI)首次可互動時間,TTI(Time to Interactive)。這個指標計算過程略微複雜,它需要滿足以下幾個條件:1、從 FCP 指標後開始計算2、持續 5 秒內無長任務(執行時間超過 50 ms)且無兩個以上正在進行中的 GET 請求往前回溯至 5 秒前的最後一個長任務結束的時間3、對於使用者互動(比如點選事件),推薦的響應時間是 100ms 以內。那麼為了達成這個目標,推薦在空閒時間裡執行任務不超過 50ms( W3C 也有這樣的標準規定),這樣能在使用者無感知的情況下響應使用者的互動,否則就會造成延遲感。 |
TBTTotal blocking Time | 總阻塞時間 (TBT)阻塞總時間,TBT(Total Blocking Time),記錄在 FCP 到 TTI 之間所有長任務的阻塞時間總和。 |
CLSCumulative Layout Shift | 記錄了頁面上非預期的位移波動。頁面渲染過程中突然插入一張巨大的圖片或者說點選了某個按鈕突然動態插入了一塊內容等等相當影響使用者體驗的網站。這個指標就是為這種情況而生的,計算方式為:位移影響的面積 * 位移距離。 |
上述的指標太多了,哪些才是核心指標呢? Google 在2020年五月提出了網站使用者體驗的三大核心指標
1、Largest Contentful Paint (LCP)
LCP:最大內容繪製 , 代表了頁面的速度指標,雖然還存在其他的一些體現速度的指標,但LCP能體現的東西更多一些。一是指標實時更新,資料更精確,二是代表著頁面最大元素的渲染時間,通常來說頁面中最大元素的快速載入能讓使用者感覺效能還挺好。
最大元素:
◦ 標籤
◦ 在svg中的image標籤
◦
◦CSS background url()載入的圖片
◦包含內聯或文字的塊級元素
影響元素:
◦伺服器端響應時間----介面效能
◦Javascript和CSS引起的渲染卡頓 ✨✨✨---webWork.js計算問題
◦資源載入時間✨✨✨---CDN
◦使用者端渲染✨✨---SSR
2、First Input Delay (FID):
FID:首次輸入延遲, 代表了頁面的互動體驗指標,就是看使用者互動事件觸發到頁面響應中間耗時多少,如果其中有長任務發生的話那麼勢必會造成響應時間變長,推薦響應使用者互動在 100ms 以內。
影響因素
◦減少第三方程式碼的影響
◦減少Javascript的執行時間
◦最小化主執行緒工作
◦減小請求數量和請求檔案大小
3、Cumulative Layout Shift (CLS)
CLS代表了頁面的穩定指標,它能衡量頁面是否排版穩定。尤其在手機上這個指標更為重要,因為手機螢幕挺小,CLS值一大的話會讓使用者覺得頁面體驗做的很差。CLS的分數在0.1或以下,則為Good。
影響因素: 通過下面的原則避免非預期佈局移動:
◦圖片或視屏元素有大小屬性,或者給他們保留一個空間大小,設定width、height,或者使用 unsized-media feature policy 。
◦不要在一個已存在的元素上面插入內容,除了相應使用者輸入。
◦使用animation或transition而不是直接觸發佈局改變。
上述講了網路請求,渲染過程,那下面我們看下一個簡單的頁面的整個過程是怎麼樣的;
1. 導航階段,請求 HTML 資料階段:
◦該任務的第一個子過程就是 Send request,該過程表示網路請求已被傳送。然後該任務進入了等待狀態。
◦接著由網路程序負責下載資源,當接收到響應頭的時候,該任務便執行 Receive Respone 過程,該過程表示接收到 HTTP 的響應頭了。
◦接著執行 DOM 事件:pagehide、visibilitychange 和 unload 等事件,如果你註冊了這些事件的回撥函數,那麼這些回撥函數會依次在該任務中被呼叫。
◦些事件被處理完成之後,那麼接下來就接收 HTML 資料了,這體現在了 Recive Data 過程,Recive Data 過程表示請求的資料已被接收,如果 HTML 資料過多,會存在多個 Receive Data 過程。
◦等到所有的資料都接收完成之後,渲染程序會觸發另外一個任務,該任務主要執行 Finish load 過程,該過程表示網路請求已經完成。
2. 解析 HTML 資料階段
其中一個主要的過程是 HTMLParser:解析的上個階段接收到的 HTML 資料。
◦在 ParserHTML 的過程中,如果解析到了 script 標籤,那麼便進入了指令碼執行過程,也就是圖中的 Evalute Script。
◦DOM 生成完成之後,會觸發相關的 DOM 事件,比如:典型的 DOMContentLoaded,還有 readyStateChanged。
◦DOM 生成之後,ParserHTML 過程繼續計算樣式表,也就是 Reculate Style,這就是生成 CSSOM 的過程
3. 生成可顯示點陣圖階段
該階段需要經歷佈局 (Layout)、分層、繪製、合成等一系列操作:
在生成完了 DOM 和 CSSOM 之後,渲染主執行緒首先執行了一些 DOM 事件,諸如 readyStateChange、load、pageshow。
總而言之,大致過程如下:
◦首先執行佈局,這個過程對應圖中的 Layout。
◦然後更新層樹 (LayerTree),這個過程對應圖中的 Update LayerTree。
◦有了層樹之後,就需要為層樹中的每一層準備繪製列表了,這個過程就稱為 Paint。
◦準備每層的繪製列表之後,就需要利用繪製列表來生成相應圖層的點陣圖了,這個過程對應圖中的 Composite Layers。走到了這步,主執行緒的任務就完成了。
接下來主執行緒會將合成的任務完全教給合成執行緒來執行,下面是具體的過程,也可以對照著 Composite、Raster 和 GPU 這三個指標來分析
對於前端應用來說,網路耗時、頁面載入耗時、指令碼執行耗時、渲染耗時等耗時情況會影響使用者的等待時長,
而 CPU佔用、記憶體佔用、本地快取佔用等則可能會導致頁面卡頓甚至卡死。
因此,效能優化可以分別從耗時和資源佔用兩方面來解決,也可以理解成時間和空間兩個方面。
時間角度(耗時)
在時間角度進行優化主要是減少耗時,瀏覽器在頁面載入的過程中,主要會進行以下的步驟:
耗時優化的著手點:
1、網路請求優化: 目標在於減少網路資源的請求和載入耗時,如果考慮 HTTP 請求過程,顯然我們可以從幾個角度來進行優化:
對於請求鏈路,核心的方案常常包括使用快取,比如 DNS 快取、CDN 快取、HTTP 快取、後臺快取等等,前端的話還可以考慮使用Service Worker、PWA等技術。使用快取並非萬能藥,很多使用由於快取的存在,我們在功能更新修復的時候還需要考慮快取的情況。除此之外,還可以考慮使用HTTP/2、HTTP/3 等提升資源請求速度,以及對多個請求進行合併,減少通訊次數;對請求進行域名拆分,提升並行請求數量。
資料大小則主要考對請求資源進行合理的拆分(CSS、Javascript 指令碼、圖片/音訊/視訊等)和壓縮,減少請求資源的體積,比如使用Tree-shaking、程式碼分割、移除用不上的依賴項等。在請求資源返回後,瀏覽器會進行解析和載入,這個過程會影響頁面的可見時間,通過對首屏載入的優化,可有效地提升使用者體驗。
2、首屏載入優化: 首屏載入優化核心點在於兩部分:
◦將頁面內容儘快地展示給使用者,減少頁面白屏時間。
◦將使用者可操作的時間儘量提前,避免使用者無法操作的卡頓體驗。
我們的頁面也需要在使用者端進行展示,此時可充分利用使用者端的優勢:
◦配合使用者端進行資源預請求和預載入,比如使用預熱 Web 容器配合使用者端將資源和資料進行離線,可用於下一次頁面的快速渲染使用秒看技術,通過生成預覽圖片的方式提前將頁面內容提供給使用者除了首屏渲染以外,使用者在瀏覽器頁面過程中,也會觸發頁面的二次運算和渲染,此時需要進行渲染過程的優化
3、渲染過程優化:渲染過程的優化可以理解成首屏載入完成後,使用者的操作互動觸發的二次渲染。主要思路是減少使用者的操作等待時間,以及通過將頁面渲染影格率保持在 60FPS 左右,提升頁面互動和渲染的流暢度。包括但不限於以下方案:
◦使用資源預載入,提升空閒時間的資源利用率--preload
◦減少/合併 DOM 操作,減少瀏覽器渲染過程中的計算耗時
◦使用離屏渲染,在頁面不可見的地方提前進行渲染(比如 Canvas 離屏渲染)
◦通過合理使用瀏覽器 GPU 能力,提升瀏覽器渲染效率(比如使用 css transform 代替 Canvas 縮放繪製)
以上這些,是對常見的 Web 頁面渲染優化方案。對於運算邏輯複雜、計算量較大的業務邏輯,我們還需要進行計算/邏輯執行的提速。
什麼是重繪和迴流
迴流比重繪更加消耗效能,付出的代價更高。
◦recalculate style (style):結合DOM和CSSOM,確定各元素應用的CSS規則
◦layout:重新計算各元素位置來佈局頁面,也稱reflow
◦update layer tree (layer):更新渲染樹
◦paint:繪製各個圖層
◦composite layers (composite):把各個圖層合成為完整頁面
核心: 佈局會不會變! 迴流一定會導致重繪,重繪不一定導致迴流
1.重繪:簡單來說就是重新繪畫,當給一個元素更換顏色、更換背景,雖然不會影響頁面佈局,但是顏色或背景變了,就會重新渲染頁面,這就是重繪。
2.迴流: 當增加或刪除dom節點,或者給元素修改寬高時,會改變頁面佈局,那麼就會重新構造dom樹然後再次進行渲染,這就是迴流。
哪些會引起迴流呢?
總結
重繪不會引起dom結構和頁面佈局的變化,只是樣式的變化,有重繪不一定有迴流。
迴流則是會引起dom結構和頁面佈局的變化,有迴流就一定有重繪。
怎麼進行優化或減少?
1.計算/邏輯執行提速
通過將 Javscript 大任務進行拆解,結合非同步任務的管理,避免出現長時間計算導致頁面卡頓的情況
將耗時長且非關鍵邏輯的計算拆離,比如使用 Web Worker
通過使用執行效率更高的方式,減少計算耗時,比如使用 Webassembly
通過將計算過程提前,減少計算等待時長,比如使用 AOT 技術
通過使用更優的演演算法或是儲存結構,提升計算效率,比如 VSCode 使用紅黑樹優化文字緩衝區的計算
通過將計算結果快取的方式,減少運算次數
空間角度(資源)
在做效能優化的時候,其實很多情況下會依賴時間換空間、空間換時間等方式,只能根據自己專案的實際情況做出取捨,選擇相對合適的一種方案去進行優化。
資源佔用常見的優化方式包括:
1.合理使用快取,不濫用使用者的快取資源(比如瀏覽器快取、IndexDB),及時進行快取清理;
2.避免存在記憶體洩露,比如儘量避免全域性變數的使用、及時解除參照等
3.避免複雜/異常的遞迴呼叫,導致呼叫棧的溢位
4.通過使用資料結構享元的方式,減少物件的建立,從而減少記憶體佔用
注意:new Worker(xxx.js)裡的xxx.js必須和HTML檔案同源必須在http/https協定下存取HTML檔案,不能用檔案協定(類似file:///E:/wamp64/www/t.html 這種
因此我用mamp搭建的一個環境指向測試的資料夾;本地設定http://www.performance.com/進行測試;
主程序程式碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>web-worker小測試</title>
</head>
<body>
<!-- <script type="text/javascript" src="worker.js"></script> -->
<script>
// const work = new Worker('worker.js');
// work.postMessage('hello worker')
// work.onmessage = (e) => {
// console.log(`主程序收到了子程序發出的資訊:${e.data}`);
// // 主程序收到了子程序發出的資訊:你好,我是子程序!
// work.terminate();
// };
let cnt = 0;
for (let i = 0; i < 900000000; i += 1) {
cnt += 1;
}
console.log(cnt);
</script>
</body>
</html
worker.js
onmessage = (e) => {
console.log(`收到了主程序發出的資訊:${e.data}`);
let cnt = 0;
for (let i = 0; i < 900000000; i += 1) {
cnt += 1;
}
console.log(cnt);
//收到了主程序發出的資訊:hello worker
postMessage(`你好,我是子程序!${cnt}`);
}
在vue.js中的使用過程, 除了設定,其他同上;
1、npm install vue-worker
2、chainWebpack中進行設定:
config.module
.rule('worker')
.test(/\.worker\.js$/)
.use('worker-loader')
.loader('worker-loader')
.options({
inline: 'fallback',
});
config.module.rule('js').exclude.add(/\.worker\.js$/);
更新後,當前頁面程式碼段中,計算300ms+ 提升,效果一般,主要還是dom節點過多引起的;
Web Worker的限制
1.在 Worker 執行緒的執行環境中沒有 window 全域性物件,也無法存取 DOM 物件
2.Worker中只能獲取到部分瀏覽器提供的 API,如定時器、navigator、location、XMLHttpRequest等
3.由於可以獲取XMLHttpRequest 物件,可以在 Worker 執行緒中執行ajax請求
4.每個執行緒執行在完全獨立的環境中,需要通過postMessage、 message事件機制來實現的執行緒之間的通訊
計算的運算時長 - 通訊時長 > 50ms,推薦使用Web Worker
1.生產環境關閉productionSourceMap、css sourceMap
SourceMap就是當頁面出現某些錯誤,能夠定位到具體的某一行程式碼,SourceMap就是幫你建立這個對映關係的,方便程式碼偵錯;
關閉css sourceMap以後,js檔案從55M降低到12M, 檔案從650個減少至325個;
分析大檔案, 目前專案的情況: js總計12.1M (325個專案); css 總計11.1M (249個專案),
◦element-ui年1.85M--涉及改造的有點多,暫時不更新;
◦Ecahrts 2.55M --cdn引入
◦handsontable 3.34M---線上引入後,由於handsontable-vue還會安裝handsontable, 因此需要把handsontable-vue也使用線上的方式,cdh資源可在https://www.jsdelivr.com/package/npm/@handsontable/vue中找到;
◦vue-json-editor 1.24M--沒找到線上js資源,建議元件引入的時候,直接使用json-editor;
◦將資源進行cdn方式引入;使用CDN引入以後,js總計9.3M(326個專案), css總計10.9(249個專案)
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
// 正式環境不打包公共js
let externals = {};
// 儲存cdn的檔案
const cdn = {
css: [
'https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.css',
],
js: [],
};
// 正式環境才需要
// if (isProduction) {
externals = { // 排除打包的js
vue: 'Vue',
echarts: 'echarts',
vueHandsontable: 'vue-handsontable',
};
cdn.js = [
'https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js', // vuejs
'https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js',
'https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js',
'https://cdn.jsdelivr.net/npm/@handsontable/[email protected]/dist/vue-handsontable.min.js',
];
// }
**************
configureWebpack: {
// 常用的公共js 排除掉,不打包 而是在index新增cdn,
externals,
plugins: [
new BundleAnalyzerPlugin(), // 分析打包大小使用預設設定
],
},
*************
chainWebpack: (config) => {
// 注入cdn變數 (打包時會執行)
config.plugin('html').tap((args) => {
args[0].cdn = cdn; // 設定cdn給外掛
return args;
});
}
使用CDN引入以後,js總計9.3M(326個專案)=> 3.1M, css總計10.9(249個專案)--沒有發生變化
nginx的設定, 這篇文章很不錯https://www.cnblogs.com/wwjj4811/p/15847916.html, 這塊我沒線上測試;不過應該沒啥問題;
1、安裝 compression-webpack-plugin(vue2--npm install --save-dev [email protected])
2、const CompressionPlugin = require('compression-webpack-plugin');
3、chainWebpack中設定
if (isProduction) {
config.plugin('compressionPlugin').use(new CompressionPlugin({
test: /\.(js)$/, // 匹配檔名
threshold: 10240, // 對超過10k的資料壓縮
minRatio: 0.8,
deleteOriginalAssets: true, // 刪除原始檔
}));
}
4、nginx中增加設定後重啟
gzip on; #開啟gzip功能
gzip_types *; #壓縮原始檔型別,根據具體的存取資源型別設定
gzip_comp_level 6; #gzip壓縮級別
gzip_min_length 1024; #進行壓縮響應頁面的最小長度,content-length
gzip_buffers 4 16K; #快取空間大小
gzip_http_version 1.1; #指定壓縮響應所需要的最低HTTP請求版本
gzip_vary on; #往頭資訊中新增壓縮標識
gzip_disable "MSIE [1-6]\."; #對IE6以下的版本都不進行壓縮
gzip_proxied off; #nginx作為反向代理壓縮伺服器端返回資料的條件
tree shaking:通常用於描述移除 JavaScript 上下文中的未參照程式碼(dead-code)。它依賴於 ES2015 模組系統中的靜態結構特性,例如import和export,是 webpack 4 版本,擴充套件的這個檢測能力;
使用tree-shaking以後,js總計3.1M(326個專案)=> 2.9M(315個專案), css總計10.9(249個專案)--384K(15個專案)
作者:京東零售 蘇文靜
來源:京東雲開發者社群 轉載請註明來源