前言
現在確實有好多性能優化方面的文章,但好多都只是文字講解,馬上下班了,抽點時間總結總結。
前端性能優化的原則其實就是更好的用戶體驗,具體實現的目標大體有兩個:
- 合理使用內存或緩存,減少請求;
- 減少CPU或者GPU的計算,達到更快的展現。
前端在性能優化的方向大體有兩個:
- 減少頁面體積,提升網絡加載
- 優化頁面渲染
正文
一.減少頁面體積,提升網絡加載
1、靜態資源的壓縮合並(JS 代碼壓縮合並、CSS 代碼壓縮合並、雪碧圖)
- 壓縮是爲了減小文件體積,減輕網絡負載,達到更快的下載;
- 合併和雪碧圖都是爲了減少文件的請求次數,但不是合併的就一個比沒有合併時加載快,要看合併之後的體積,若文件合併後太大了也不太利於性能優化,所以在實際的項目中要做好權衡。
2、靜態資源緩存(資源名稱加 MD5 戳)
可以通過鏈接名稱控制緩存:通過前端構建工具爲打包的文件添加md5後綴,這樣當打包上線時請求的鏈接發生改變,這樣可以防止由於緩導致靜態資源更新失效;
3、 使用 CDN 讓資源加載更快
二.優化頁面渲染
1、CSS 放前面,JS 放後面
- 瀏覽器在渲染解析過程中,若遇到
<link href="...">
和<script src="...">
這種外鏈加載 CSS 和 JS 的標籤,瀏覽器會異步下載並解析執行。CSS放在頭部是爲了讓瀏覽器儘早解析執行Css文件,渲染出頁面的樣式,若放在底部會出現渲染卡頓的情況,影響性能和體驗。 - 而當渲染過程中遇到script標籤時就會執行JS代碼,從阻塞頁面渲染,因爲瀏覽器渲染和 JS 執行共用一個線程,而且這裏必須是單線程操作,多線程會產生渲染 DOM 衝突。所以要將JS放在底部,等到頁面渲染完成之後再去解析執行js,保證用戶體驗性。因爲瀏覽器渲染和 JS 執行共用一個線程,而且這裏必須是單線程操作,多線程會產生渲染 DOM 衝突。
- 另外,JS 執行如果涉及 DOM 操作,得等待 DOM 解析完成才行,JS 放在底部執行時,HTML 肯定都解析成了 DOM 結構。JS 如果放在 HTML 頂部,JS 執行的時候 HTML 還沒來得及轉換爲 DOM 結構,可能會報錯。
2、懶加載(圖片懶加載、下拉加載更多)
先將src賦值成一個通用的預覽圖,下拉時候再動態賦值成正式的圖片。如下,preview.png是預覽圖片,比較小,加載很快,而且很多圖片都共用這個preview.png,加載一次即可。待頁面下拉,圖片顯示出來時,再去替換src爲data-src的值。(data-開頭的屬性瀏覽器渲染的時候會忽略掉,提高渲染性能)
<img src="preview.png" data-src="realImg.png"/>
3、減少DOM 查詢,對 DOM 查詢做緩存
// 只查詢一個 DOM ,緩存在 pList 中了
var pList = document.getElementsByTagName('p')
for (var i = 0; i < pList.length; i++) {
}
// 每次循環,都會查詢 DOM ,耗費性能
for (var i = 0; i < document.getElementsByTagName('p').length; i++) {
}
4、減少DOM 操作,多個操作儘量合併在一起執行(DocumentFragment)
DOM 操作是非常耗費性能的,因此插入多個標籤時,先插入 Fragment 然後再統一插入 DOM。因爲Fragment文檔片段存在於內存中,並不在DOM樹中,所以將子元素插入到文檔片段時不會引起頁面迴流。
var listNode = document.getElementById('list')
// 要插入 10 個 li 標籤
var frag = document.createDocumentFragment();
var i, li;
for(i = 0; i < 10; i++) {
li = document.createElement("li");
li.innerHTML = "List item " + i;
frag.appendChild(li); //先放在 frag 中,最後一次性插入到 DOM 結構中。
}
listNode.appendChild(frag);
5、事件節流
在開發過程中會遇到頁面一些頻繁觸發的事件,比如mouseover、scroll、resize等事件。一秒可以執行很多次,這樣會造成嚴重的頁面性能問題,導致頁面c出現卡頓甚至瀏覽器崩潰。因此我們需要對事件進行節流,簡單的說就是控制其執行的次數。這裏就涉及到了常用到的js的節流和防抖功能實現。
1.防抖(debounce):在事件被觸發n秒後再執行回調,如果在這n秒內又被觸發,則重新計時。
function debounce(fn, delay) {
// 定時器,用來 setTimeout
var timer
// 返回一個函數,這個函數會在一個時間區間結束後的 delay 毫秒時執行 fn 函數
return function () {
// 保存函數調用時的上下文和參數,傳遞給 fn
var context = this
var args = arguments
// 每次這個返回的函數被調用,就清除定時器,以保證不執行 fn
timer && clearTimeout(timer)
// 當返回的函數被最後一次調用後(也就是用戶停止了某個連續的操作),
// 再過 delay 毫秒就執行 fn
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}
2、節流(throttle):規定一個單位時間,在這個單位時間內,只能有一次觸發事件的回調函數執行,如果在同一個單位時間內某事件被觸發多次,只有一次能生效。
function throttle(fn, threshhold) {
// 記錄上次執行的時間
var last
// 定時器
var timer
// 默認間隔爲 250ms
threshhold || (threshhold = 250)
// 返回的函數,每過 threshhold 毫秒就執行一次 fn 函數
return function () {
// 保存函數調用時的上下文和參數,傳遞給 fn
var context = this
var args = arguments
var now = +new Date()
// 如果距離上次執行 fn 函數的時間小於 threshhold,那麼就放棄
// 執行 fn,並重新計時
if (last && now < last + threshhold) {
timer && clearTimeout(timer)
// 保證在當前時間區間結束後,再執行一次 fn
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, threshhold)
// 在時間區間的最開始和到達指定間隔的時候執行一次 fn
} else {
last = now
fn.apply(context, args)
}
}
}
6、儘早執行操作(DOMContentLoaded)
window.addEventListener('load', function () {
// 頁面的全部資源加載完纔會執行,包括圖片、視頻等
})
document.addEventListener('DOMContentLoaded', function () {
// DOM 渲染完即可執行,此時圖片、視頻還可能沒有加載完
})
$(document).ready({function () {
// 同DOMContentLoaded
})
7、使用 預渲染 或者 SSR後端渲染,數據直接輸出到 HTML 中,減少瀏覽器使用 JS 模板渲染頁面 HTML 的時間 (如Vue SSR),同時也有利於網站的SEO。
最後
感謝各位的閱讀,如有問題麻煩及時指正出來。