在遇到一個頁面性能問題時,我理解的優化閉環是:分析、策略、驗證和沉淀。
* 分析需要有分析數據,因此得有一個性能監控管理。
* 策略就是制訂針對性的優化方案,解決當前遇到的問題。
* 驗證的對象上述策略,判斷方案是否有效,同樣需要數據支撐。
* 沉淀就是將解決過程文檔化、通用化,能夠總結成一套實際方案、優化規則等。
  這其中非常關鍵的一步是需要采集到性能數據,并且得有個可視化后臺查看數據變化。
  在之前已經自制了一個[性能優化平臺](https://www.cnblogs.com/strick/p/14578711.html),采集前端性能參數的 SDK 叫[shin.js](https://github.com/pwstrick/shin-admin/blob/main/public/shin.js#L226)。
## 一、優化的三部分
  在文章開頭,我想先聊聊網頁優化的三部分:網絡,渲染和容器。
  第一部分的網絡就是提升傳輸速度,可優化的手段包括 gzipped壓縮、CDN、HTTP 緩存、HTTP 2.0協議、并發請求等。
  像 HTTP 緩存分為強緩存和協商緩存,請求首部和瀏覽器配合完成資源的緩存機制,下圖摘自《[前端程序員面試筆試寶典](https://book.douban.com/subject/30324146/)》。
:-: 
  第二部分的渲染就是 CRP 優化(關鍵渲染路徑),CRP 是指瀏覽器從接收資源到渲染像素的過程。
  優化的點包括資源數、字節數和加載時序。現代化的 webpack 構建工具就會對資源做前兩項的優化處理,包括壓縮文件、合并文件、優化包的引入等。
  加載時序就包括日常都會用的圖片懶加載和預加載、腳本的延遲(defer)、異步(async)和預加載([preload](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Link_types/preload))等。
  對于比較龐大的首頁,可以先將那些能阻塞網頁首次渲染的關鍵資源載入,其余資源都延遲載入,以此提升頁面打開速度。
  第三部分的容器(WebView)就是借用端的能力,讓 APP 配合優化網頁。
  例如[預請求](https://www.cnblogs.com/strick/p/14918217.html),將請求接口的時機前置到容器打開之時,下面是一張實現流程圖。
:-: 
  還有一種靜態資源緩存至客戶端本地,當時與公司客戶端討論此方案時,他們覺得每次攔截請求會損傷性能,后面就采用了折中的辦法。
  就是他們去主動請求特定地址的靜態資源,然后開放接口讓我可以去讀取本地資源,也就是說由 Web 來控制是否讀取緩存資源。
## 二、問題引出
  現在言歸正傳,回到本次的優化中來。
  為了提升頁面產出率,聯合 UI 設計構建了一套可配置的[通用活動模板](https://www.cnblogs.com/strick/p/15928830.html)。
  活動上線后,就查看了性能數據,情況很不理想,如下圖所示。

  FP(白屏)時間大部分都在 2 秒以上,取平均值更是在 3 秒左右。[Google的報告](https://support.google.com/webmasters/answer/9205520?hl=zh-Hans)指出:
* 如果網頁加載時間從 1 秒增加到 3 秒,跳出率就會提高**32%**。
* 如果網頁加載時間從 1 秒增加到 6 秒,跳出率就會上升**106%**。
## 三、數據排查
  在數據庫中,將指定的性能數據記錄導出到 Excel 中。
  翻了一條后發現,性能問題集中在 DOM 中。
~~~
{
"unloadEventTime": 0,
"loadEventTime": 1,
"interactiveTime": 1255,
"parseDomTime": 1075,
"initDomTreeTime": 721,
"readyStart": 5,
"redirectCount": 0,
"compression": 0,
"redirectTime": 0,
"appcacheTime": 0,
"lookupDomainTime": 0,
"connectSslTime": 0,
"connectTime": 0,
"requestTime": 119,
"requestDocumentTime": 119,
"responseDocumentTime": 0,
"TTFB": 534,
}
~~~
  JSON 中的?interactiveTime、parseDomTime 和 initDomTreeTime 消耗的時間都不短,計算規則如下所示。
~~~
/**
* 解析 DOM 樹結構的時間
* 期間要加載內嵌資源
* 反省下你的 DOM 樹嵌套是不是太多了
*/
api.parseDomTime = timing.domComplete - timing.domInteractive;
/**
* 請求完畢至DOM加載耗時
*/
api.initDomTreeTime = timing.domInteractive - timing.responseEnd;
/**
* 首次可交互時間
*/
api.interactiveTime = timing.domInteractive - timing.fetchStart;
~~~
  參考 W3C 第二版性能參數圖可知,慢的地方集中在 Processing 階段。
:-: 
## 四、Chrome DevTools
  打開 Chrome DevTools 中的 Performance 一欄,錄制后,可在火焰圖中看到長任務。
  點擊 Long task 鏈接,會跳轉到[使用 RAIL 模型衡量性能](https://web.dev/rail/)一文。

  在 PC 瀏覽器中打開肯定會比在手機中快,但即使如此,還是出現了性能瓶頸,說明這里是真的慢。
  藍底的 DCL 是?[DOMContentLoaded](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/DOMContentLoaded_event)事件,在 HTML 文檔被完全加載和解析后觸發,綠底的 FP 就是白屏時間。
  黃底的 Evaluate Script 表示加載 JavaScript 腳本,Compile Script 表示執行 JavaScript 腳本。
  再來看看網絡請求瀑布圖,下圖中的藍線就是 DCL,可以清晰的看到,藍線之前在加載的基本都是 JavaScript 腳本。
:-: 
  由此可知,加載的腳本有點多,并且有一個 chunk-vendors.js 腳本還比較大,下載時間有點長(依據藍色塊)。
## 五、代碼分析
  定位到了問題根源,那就直接查看基于 Vue 的代碼是怎么寫的了。
**1)HTML**
  下面是編譯后的頁面 HTML 結構,只列出了關鍵部分。
~~~html
<!DOCTYPE html>
<html lang=en>
<head>
<script src=https://res.wx.qq.com/open/js/jweixin-1.6.0.js></script>
<script src=//www.xxxx.com/flexible/flexible.js></script>
<script src=//www.xxxx.co/files/js/baidu.js></script>
<script src=//www.xxxx.co/files/js/shin.js></script>
<link href=//www.xxxx.me/game/css/operation37.cba04f10.css rel=preload as=style>
<link href=//www.xxxx.me/game/js/chunk-lodash.152ef24b.js rel=preload as=script>
<link href=//www.xxxx.me/game/js/chunk-lottie.23b9982e.js rel=preload as=script>
<link href=//www.xxxx.me/game/js/operation37.fa5f5378.js rel=preload as=script>
<link href=//www.xxxx.me/game/css/chunk-vendors.779f7d1d.css rel=stylesheet>
<link href=//www.xxxx.me/game/css/operation37.cba04f10.css rel=stylesheet>
</head>
<body>
<div id=app></div>
<script src=//www.xxxx.me/game/js/chunk-vendors.ca022e99.js></script>
<script src=//www.xxxx.me/game/js/operation37.fa5f5378.js></script>
</body>
</html>
~~~
  首先在 head 中,引入了大量的 JavaScript 腳本,flexible.js 其實在構建時可以內聯,不需要網絡訪問。
  然后?jweixin-1.6.0.js 和 baidu.js 這兩個腳本完全可以延遲加載,后者就是增加百度統計的腳本。
  接著就是 shin.js 需要做壓縮處理,可以減少 50% 以上的尺寸。
  在 link 元素中,使用了[preload](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Link_types/preload),表示可并行的預加載,并且不會執行,這是提升頁面性能的一種手段。
  雖然第三方的庫(chunk-vendors.ca022e99.js)和業務主邏輯(operation37.fa5f5378.js)兩個腳本聲明在 body 中。
  但是主結構就是個空的 div,因此在加載和運行時就會延長 DOM 的解析,影響白屏時間。
**2)vendors 優化**
  Vue 內置了一條命令,可以查看每個腳本的尺寸以及內部依賴包的尺寸。
  在下圖中,vendors.js 的原始尺寸是 3.76M,gzipped 壓縮后的尺寸是 442.02KB,比較大的包是 lottie、swiper、moment、lodash 等。
:-: 
  這類比較大的包可以再單獨剝離,不用全部聚合在 vendors.js 中。
  在 vue.config.js 中,配置 config.optimization.splitChunks(),如下所示,參數含義可[參考官網](https://webpack.docschina.org/plugins/split-chunks-plugin)。
~~~
config.optimization.splitChunks(
{
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
lottie: {
name: 'chunk-lottie',
test: /[\\/]node_modules[\\/]lottie-web[\\/]/,
chunks: 'all',
priority: 3,
reuseExistingChunk: true,
enforce: true
},
swiper: {
name: 'chunk-swiper',
test: /[\\/]node_modules[\\/]_swiper@3.4.2@swiper[\\/]/,
chunks: 'all',
priority: 3,
reuseExistingChunk: true,
enforce: true
},
lodash: {
name: 'chunk-lodash',
test: /[\\/]node_modules[\\/]lodash[\\/]/,
chunks: 'all',
priority: 3,
reuseExistingChunk: true,
enforce: true
}
}
}
)
~~~
  在經過一頓初步操作后,原始尺寸降到 2.4M,gzipped 壓縮后的尺寸是 308.64KB,比之前少了 100 多 KB。
:-: 
  現在在入口處需要單獨聲明依賴的包,否則不會自動引入。
~~~
pages: {
operation37: {
entry: 'src/pages/operation37/index.js',
template: 'src/pages/operation37/index.html',
filename: 'operation37.html',
title: '榜單配置頁面',
chunks: ['chunk-lottie', 'operation37', 'chunk-vendors']
},
}
~~~
  其實大部分的 H5 頁面都比較簡單,可能就使用了包的一個小功能,那完全可以自己用代碼實現,這樣就不必依賴那個大包了。
  后面就是在代碼邏輯層面的優化,核心就是減少腳本尺寸。優化后,再去觀察數據的變化。
**3)CDN加速**
  之前部分靜態資源采取了 CDN 加速,現在需要將 game 下面中的靜態資源全部走 CDN。
  在云端配置些參數,就能走 CDN。不過,第一次沒有配置好,沒有配置轉發路徑,造成了嚴重的線上問題。
  第二次就比較謹慎,在測試環境將之前碰到的問題都驗證后,才最終在線上配置。
  白屏時間占比變化:
* 1 秒內的占比從 77.3% 最高提升至 78.7%
* 1 - 2 秒占比從 15.6% 最高提升至 18.7%
* 2 - 3 秒占比從 4% 最低下降至 1.8%
* 3 - 4 秒占比從 1.1% 最低下降至 0.6%
* 4 秒以上的占比從 2.1% 最低下降至 1.4%
參考資料:
[長的 JavaScript 任務是否會延遲您的交互時間?](https://web.dev/i18n/zh/long-tasks-devtools/)
[狙殺頁面卡頓 —— Performance 工具指北](https://zhuanlan.zhihu.com/p/41017888)
[chrome performance看瀏覽器渲染過程](https://blog.csdn.net/It_rod/article/details/79661739)
[深入理解瀏覽器解析渲染 HTML](https://juejin.cn/post/6844904131346300942)
[Vue CLI 項目頁面打開時間優化:從16秒到2秒內](http://www.zuo11.com/blog/2020/11/vue_cli_slow.html)
[preload 讓加載和解析解耦](https://juejin.cn/post/6844903854690009102)
*****
> 原文出處:
[博客園-Web優化躬行記](https://www.cnblogs.com/strick/category/1795726.html)
[知乎專欄-Web優化躬行記](https://zhuanlan.zhihu.com/c_1260996761008627712)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020