前端效能優化——首頁資源壓縮63%、白屏時間縮短86%
theme: juejin highlight: androidstudio
提升首屏的載入速度,是前端效能優化中最重要的環節,這裡筆者梳理出一些 常規且有效
的首屏優化建議
目標: 通過對比優化前後的效能變化,來驗證方案的有效性,瞭解並掌握其原理
1、路由懶載入
SPA 專案,一個路由對應一個頁面,如果不做處理,專案打包後,會把所有頁面打包成一個檔案,當用戶開啟首頁時,會一次性載入所有的資源,造成首頁載入很慢,降低使用者體驗
舉一個實際專案的打包詳情:
-
app.js 初始體積:
1175 KB
-
app.css 初始體積:
274 KB
將路由全部改成懶載入
// 通過webpackChunkName設定分割後代碼塊的名字
const Home = () => import(/* webpackChunkName: "home" */ "@/views/home/index.vue");
const MetricGroup = () => import(/* webpackChunkName: "metricGroup" */ "@/views/metricGroup/index.vue");
…………
const routes = [
{
path: "/",
name: "home",
component: Home
},
{
path: "/metricGroup",
name: "metricGroup",
component: MetricGroup
},
…………
]
重新打包後
-
app.js:
244 KB
、 home.js:35KB
-
app.css:
67 KB
、home.css:15KB
通過路由懶載入,該專案的首頁資源壓縮約 52%
路由懶載入的原理
懶載入前提的實現:ES6的動態地載入模組——import()
呼叫 import() 之處,被作為分離的模組起點,意思是,被請求的模組和它引用的所有子模組,會分離到一個單獨的 chunk 中
——摘自《webpack——模組方法》的import()小節
要實現懶載入,就得先將進行懶載入的子模組分離出來,打包成一個單獨的檔案
webpackChunkName 作用是 webpack 在打包的時候,對非同步引入的庫程式碼(lodash)進行程式碼分割時,設定程式碼塊的名字。webpack 會將任何一個非同步模組與相同的塊名稱組合到相同的非同步塊中
2、元件懶載入
除了路由的懶載入外,元件的懶載入在很多場景下也有重要的作用
舉個🌰:
home 頁面 和 about 頁面,都引入了 dialogInfo 彈框元件,該彈框不是一進入頁面就載入,而是需要使用者手動觸發後才展示出來
home 頁面示例: ```
```
專案打包後,發現 home.js 和 about.js 均包括了該彈框元件的程式碼(在 dist 檔案中搜索dialogInfo彈框元件)
當用戶開啟 home 頁時,會一次性載入該頁面所有的資源,我們期望的是使用者觸發按鈕後,再載入該彈框元件的資源
這種場景下,就很適合用懶載入的方式引入
彈框元件懶載入: ```
```
重新打包後,home.js 和 about.js 中沒有了彈框元件的程式碼,該元件被獨立打包成 dialogInfo.js,當用戶點選按鈕時,才會去載入 dialogInfo.js 和 dialogInfo.css
最終,使用元件路由懶後,該專案的首頁資源進一步減少約 11%
元件懶載入的使用場景
有時資源拆分的過細也不好,可能會造成瀏覽器 http 請求的增多
總結出三種適合元件懶載入的場景:
1)該頁面的 JS 檔案體積大,導致頁面開啟慢,可以通過元件懶載入進行資源拆分,利用瀏覽器並行下載資源,提升下載速度(比如首頁)
2)該元件不是一進入頁面就展示,需要一定條件下才觸發(比如彈框元件)
3)該元件複用性高,很多頁面都有引入,利用元件懶載入抽離出該元件,一方面可以很好利用快取,同時也可以減少頁面的 JS 檔案大小(比如表格元件、圖形元件等)
3、合理使用 Tree shaking
Tree shaking 的作用:消除無用的 JS 程式碼,減少程式碼體積
舉個🌰:
// util.js
export function targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
export function deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
專案中只使用了 targetType 方法,但未使用 deepClone 方法,專案打包後,deepClone 方法不會被打包到專案裡
tree-shaking 原理:
依賴於ES6的模組特性,ES6模組依賴關係是確定的,和執行時的狀態無關,可以進行可靠的靜態分析,這就是 tree-shaking 的基礎
靜態分析就是不需要執行程式碼,就可以從字面量上對程式碼進行分析。ES6之前的模組化,比如 CommonJS 是動態載入,只有執行後才知道引用的什麼模組,就不能通過靜態分析去做優化,正是基於這個基礎上,才使得 tree-shaking 成為可能
Tree shaking 並不是萬能的
並不是說所有無用的程式碼都可以被消除,還是上面的程式碼,換個寫法 tree-shaking 就失效了
``` // util.js export default { targetType(target) { return Object.prototype.toString.call(target).slice(8, -1).toLowerCase(); }, deepClone(target) { return JSON.parse(JSON.stringify(target)); } };
// 引入並使用 import util from '../util'; util.targetType(null) ```
同樣的,專案中只使用了 targetType 方法,未使用 deepClone 方法,專案打包後,deepClone 方法還是被打包到專案裡
在 dist 檔案中搜索 deepClone 方法:
究其原因,export default 匯出的是一個物件,無法通過靜態分析判斷出一個物件的哪些變數未被使用,所以 tree-shaking 只對使用 export 匯出的變數生效
這也是函數語言程式設計越來越火的原因,因為可以很好利用 tree-shaking 精簡專案的體積,也是 vue3 全面擁抱了函數語言程式設計的原因之一
4、骨架屏優化白屏時長
使用骨架屏,可以縮短白屏時間,提升使用者體驗。國內大多數的主流網站都使用了骨架屏,特別是手機端的專案
SPA 單頁應用,無論 vue 還是 react,最初的 html 都是空白的,需要通過載入 JS 將內容掛載到根節點上,這套機制的副作用:會造成長時間的白屏
常見的骨架屏外掛就是基於這種原理,在專案打包時將骨架屏的內容直接放到 html 檔案的根節點中
使用骨架屏外掛,打包後的 html 檔案(根節點內部為骨架屏):
同一專案,對比使用骨架屏前後的 FP 白屏時間:
- 無骨架屏:白屏時間
1063ms
- 有骨架屏:白屏時間
144ms
骨架屏確實是優化白屏的不二選擇,白屏時間縮短了 86%
骨架屏外掛
這裡以 vue-skeleton-webpack-plugin
外掛為例,該外掛的亮點是可以給不同的頁面設定不同的骨架屏,這點確實很酷
1)安裝
npm i vue-skeleton-webpack-plugin
2)vue.config.js 配置
// 骨架屏
const SkeletonWebpackPlugin = require("vue-skeleton-webpack-plugin");
module.exports = {
configureWebpack: {
plugins: [
new SkeletonWebpackPlugin({
// 例項化外掛物件
webpackConfig: {
entry: {
app: path.join(__dirname, './src/skeleton.js') // 引入骨架屏入口檔案
}
},
minimize: true, // SPA 下是否需要壓縮注入 HTML 的 JS 程式碼
quiet: true, // 在服務端渲染時是否需要輸出資訊到控制檯
router: {
mode: 'hash', // 路由模式
routes: [
// 不同頁面可以配置不同骨架屏
// 對應路徑所需要的骨架屏元件id,id的定義在入口檔案內
{ path: /^\/home(?:\/)?/i, skeletonId: 'homeSkeleton' },
{ path: /^\/detail(?:\/)?/i, skeletonId: 'detailSkeleton' }
]
}
})
]
}
}
3)新建 skeleton.js 入口檔案
``` // skeleton.js import Vue from "vue"; // 引入對應的骨架屏頁面 import homeSkeleton from "./views/homeSkeleton"; import detailSkeleton from "./views/detailSkeleton";
export default new Vue({
components: {
homeSkeleton,
detailSkeleton,
},
template: <div>
<homeSkeleton id="homeSkeleton" style="display:none;" />
<detailSkeleton id="detailSkeleton" style="display:none;" />
</div>
,
});
```
5、長列表虛擬滾動
首頁中不乏有需要渲染長列表的場景,當渲染條數過多時,所需要的渲染時間會很長,滾動時還會造成頁面卡頓,整體體驗非常不好
虛擬滾動——指的是隻渲染可視區域的列表項,非可見區域的不渲染,在滾動時動態更新可視區域,該方案在優化大量資料渲染時效果是很明顯的
虛擬滾動圖例:
虛擬滾動基本原理:
計算出 totalHeight 列表總高度,並在觸發時滾動事件時根據 scrollTop 值不斷更新 startIndex 以及 endIndex ,以此從列表資料 listData 中擷取對應元素
虛擬滾動效能對比:
-
在不使用虛擬滾動的情況下,渲染10萬個文字節點:
-
使用虛擬滾動的情況後:
使用虛擬滾動使效能提升了 78%
虛擬滾動外掛
虛擬滾動的外掛有很多,比如 vue-virtual-scroller、vue-virtual-scroll-list、react-tiny-virtual-list、react-virtualized 等
這裡簡單介紹 vue-virtual-scroller 的使用
``` // 安裝外掛 npm install vue-virtual-scroller
// main.js import VueVirtualScroller from 'vue-virtual-scroller' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
Vue.use(VueVirtualScroller)
// 使用 ```
該外掛主要有 RecycleScroller.vue、DynamicScroller.vue 這兩個元件,其中 RecycleScroller 需要 item 的高度為靜態的,也就是列表每個 item 的高度都是一致的,而 DynamicScroller 可以相容 item 的高度為動態的情況
6、Web Worker 優化長任務
由於瀏覽器 GUI 渲染執行緒與 JS 引擎執行緒是互斥的關係,當頁面中有很多長任務時,會造成頁面 UI 阻塞,出現介面卡頓、掉幀等情況
檢視頁面的長任務:
開啟控制檯,選擇 Performance 工具,點選 Start 按鈕,展開 Main 選項,會發現有很多紅色的三角,這些就屬於長任務(長任務:執行時間超過50ms的任務)
測試實驗:
如果直接把下面這段程式碼直接丟到主執行緒中,計算過程中頁面一直處於卡死狀態,無法操作
let sum = 0;
for (let i = 0; i < 200000; i++) {
for (let i = 0; i < 10000; i++) {
sum += Math.random()
}
}
使用 Web Worker 執行上述程式碼時,計算過程中頁面正常可操作、無卡頓
// worker.js
onmessage = function (e) {
// onmessage獲取傳入的初始值
let sum = e.data;
for (let i = 0; i < 200000; i++) {
for (let i = 0; i < 10000; i++) {
sum += Math.random()
}
}
// 將計算的結果傳遞出去
postMessage(sum);
}
Web Worker 具體的使用與案例,詳情見 一文徹底瞭解Web Worker,十萬、百萬條資料都是弟弟🔥
Web Worker 的通訊時長
並不是執行時間超過 50ms 的任務,就可以使用 Web Worker,還要先考慮通訊時長
的問題
假如一個運算執行時長為 100ms,但是通訊時長為 300ms, 用了 Web Worker可能會更慢
比如新建一個 web worker, 瀏覽器會載入對應的 worker.js 資源,下圖中的 Time 是這個資源的通訊時長(也叫載入時長)
當任務的運算時長 - 通訊時長 > 50ms,推薦使用Web Worker
7、requestAnimationFrame 製作動畫
requestAnimationFrame
是瀏覽器專門為動畫提供的 API,它的重新整理頻率與顯示器的頻率保持一致,使用該 api 可以解決用 setTimeout/setInterval 製作動畫卡頓的情況
下面的案例演示了兩者製作進度條的對比(執行按鈕可點選)
可以看到使用定時器製作的動畫,卡頓還是比較明顯的
setTimeout/setInterval、requestAnimationFrame 三者的區別:
1)引擎層面
setTimeout/setInterval 屬於 JS引擎
,requestAnimationFrame 屬於 GUI引擎
JS引擎與GUI引擎
是互斥的,也就是說 GUI 引擎在渲染時會阻塞 JS 引擎的計算
2)時間是否準確
requestAnimationFrame 重新整理頻率是固定且準確的,但 setTimeout/setInterval 是巨集任務,根據事件輪詢機制,其他任務會阻塞或延遲js任務的執行,會出現定時器不準的情況
3)效能層面
當頁面被隱藏或最小化時,setTimeout/setInterval 定時器仍會在後臺執行動畫任務,而使用 requestAnimationFrame 當頁面處於未啟用的狀態下,螢幕重新整理任務會被系統暫停
8、JS 的6種載入方式
1)正常模式
```
```
這種情況下 JS 會阻塞 dom 渲染,瀏覽器必須等待 index.js 載入和執行完成後才能去做其它事情
2)async 模式
```
```
async 模式下,它的載入是非同步的,JS 不會阻塞 DOM 的渲染,async 載入是無順序的,當它載入結束,JS 會立即執行
使用場景:若該 JS 資源與 DOM 元素沒有依賴關係,也不會產生其他資源所需要的資料時,可以使用async 模式,比如埋點統計
3)defer 模式
```
```
defer 模式下,JS 的載入也是非同步的,defer 資源會在 DOMContentLoaded
執行之前,並且 defer 是有順序的載入
如果有多個設定了 defer 的 script 標籤存在,則會按照引入的前後順序執行,即便是後面的 script 資源先返回
所以 defer 可以用來控制 JS 檔案的執行順序,比如 element-ui.js 和 vue.js,因為 element-ui.js 依賴於 vue,所以必須先引入 vue.js,再引入 element-ui.js
```
```
defer 使用場景:一般情況下都可以使用 defer,特別是需要控制資源載入順序時
4)module 模式
```
```
在主流的現代瀏覽器中,script 標籤的屬性可以加上 type="module"
,瀏覽器會對其內部的 import 引用發起 HTTP 請求,獲取模組內容。這時 script 的行為會像是 defer 一樣,在後臺下載,並且等待 DOM 解析
Vite 就是利用瀏覽器支援原生的 es module
模組,開發時跳過打包的過程,提升編譯效率
5) preload
<link rel="preload" as="script" href="index.js">
link 標籤的 preload 屬性:用於提前載入一些需要的依賴,這些資源會優先載入(如下圖紅框)
vue2 專案打包生成的 index.html 檔案,會自動給首頁所需要的資源,全部新增 preload,實現關鍵資源的提前載入
preload 特點:
1)preload 載入的資源是在瀏覽器渲染機制之前進行處理的,並且不會阻塞 onload 事件;
2)preload 載入的 JS 指令碼其載入和執行的過程是分離的,即 preload 會預載入相應的指令碼程式碼,待到需要時自行呼叫;
6)prefetch
<link rel="prefetch" as="script" href="index.js">
prefetch 是利用瀏覽器的空閒時間,載入頁面將來可能用到的資源的一種機制;通常可以用於載入其他頁面(非首頁)所需要的資源,以便加快後續頁面的開啟速度
prefetch 特點:
1)pretch 載入的資源可以獲取非當前頁面所需要的資源,並且將其放入快取至少5分鐘(無論資源是否可以快取)
2)當頁面跳轉時,未完成的 prefetch 請求不會被中斷
載入方式總結
async、defer 是 script 標籤的專屬屬性,對於網頁中的其他資源,可以通過 link 的 preload、prefetch 屬性來預載入
如今現代框架已經將 preload、prefetch 新增到打包流程中了,通過靈活的配置,去使用這些預載入功能,同時我們也可以審時度勢地向 script 標籤新增 async、defer 屬性去處理資源,這樣可以顯著提升效能
9、圖片的優化
平常大部分效能優化工作都集中在 JS 方面,但圖片也是頁面上非常重要的部分
特別是對於移動端來說,完全沒有必要去載入原圖,浪費頻寬。如何去壓縮圖片,讓圖片更快的展示出來,有很多優化工作可以做
淘寶首頁的圖片資源都很小:
圖片的動態裁剪
很多雲服務,比如阿里雲或七牛雲,都提供了圖片的動態裁剪功能,效果很棒,確實是錢沒有白花
只需在圖片的url地址上動態新增引數,就可以得到你所需要的尺寸大小,比如:http://7xkv1q.com1.z0.glb.clouddn.com/grape.jpg?imageView2/1/w/200/h/200
圖片瘦身前後對比:
- 原圖:
1.8M
- 裁剪後:
12.8KB
經過動態裁剪後的圖片,載入速度會有非常明顯的提升
圖片的懶載入
對於一些圖片量比較大的首頁,使用者開啟頁面後,只需要呈現出在螢幕可視區域內的圖片,當用戶滑動頁面時,再去加載出現在螢幕內的圖片,以優化圖片的載入效果
圖片懶載入實現原理:
由於瀏覽器會自動對頁面中的 img 標籤的 src 屬性發送請求並下載圖片,可以通過 html5 自定義屬性 data-xxx 先暫存 src 的值,然後在圖片出現在螢幕可視區域的時候,再將 data-xxx 的值重新賦值到 img 的 src 屬性即可
<img src="" alt="" data-src="./images/1.jpg">
<img src="" alt="" data-src="./images/2.jpg">
這裡以 vue-lazyload
外掛為例
``` // 安裝 npm install vue-lazyload
// main.js 註冊 import VueLazyload from 'vue-lazyload' Vue.use(VueLazyload) // 配置項 Vue.use(VueLazyload, { preLoad: 1.3, error: 'dist/error.png', // 圖片載入失敗時的佔位圖 loading: 'dist/loading.gif', // 圖片載入中時的佔位圖 attempt: 1 })
// 通過 v-lazy 指令使用
```
使用字型圖示
字型圖示是頁面使用小圖示的不二選擇,最常用的就是 iconfont
字型圖示的優點:
1)輕量級:一個圖示字型要比一系列的影象要小。一旦字型載入了,圖示就會馬上渲染出來,減少了 http 請求
2)靈活性:可以隨意的改變顏色、產生陰影、透明效果、旋轉等
3)相容性:幾乎支援所有的瀏覽器,請放心使用
圖片轉 base64 格式
將小圖片轉換為 base64 編碼字串,並寫入 HTML 或者 CSS 中,減少 http 請求
轉 base64 格式的優缺點:
1)它處理的往往是非常小的圖片,因為 Base64 編碼後,圖片大小會膨脹為原檔案的 4/3,如果對大圖也使用 Base64 編碼,後者的體積會明顯增加,即便減少了 http 請求,也無法彌補這龐大的體積帶來的效能開銷,得不償失
2)在傳輸非常小的圖片的時候,Base64 帶來的檔案體積膨脹、以及瀏覽器解析 Base64 的時間開銷,與它節省掉的 http 請求開銷相比,可以忽略不計,這時候才能真正體現出它在效能方面的優勢
專案可以使用 url-loader
將圖片轉 base64:
``` // 安裝 npm install url-loader --save-dev
// 配置 module.exports = { module: { rules: [{ test: /.(png|jpg|gif)$/i, use: [{ loader: 'url-loader', options: { // 小於 10kb 的圖片轉化為 base64 limit: 1024 * 10 } }] }] } }; ```
優化總結
本文主要介紹的是 程式碼層面 的效能優化,經過上面的一系列優化,首頁開啟速度有了明顯的提升,雖然都是一些常規方案,但其中可以深挖的知識點並不少
參考文章:
路由懶載入原理及使用
vue-skeleton-webpack-plugin 骨架屏外掛使用
前端效能優化-虛擬滾動
requestAnimationFrame製作動畫
淺談script標籤中的async和defer
Tree-Shaking效能優化實踐 - 原理篇
使用 Preload&Prefetch 優化前端頁面的資源載入