前言
移動互聯(lián)網(wǎng)時代帜平,用戶對于網(wǎng)頁的打開速度要求越來越高。首屏作為直面用戶的第一屏裆甩,其重要性不言而喻。優(yōu)化用戶體驗(yàn)更是我們前端開發(fā)非常需要 focus 的東西之一嗤栓。
從用戶的角度而言,當(dāng)打開一個網(wǎng)頁抛腕,往往關(guān)心的是從輸入完網(wǎng)頁地址后到最后展現(xiàn)完整頁面這個過程需要的時間,這個時間越短,用戶體驗(yàn)越好摔敛。所以作為網(wǎng)頁的開發(fā)者,就從輸入url到頁面渲染呈現(xiàn)這個過程中去提升網(wǎng)頁的性能马昙。
所以輸入URL后發(fā)生了什么呢?在瀏覽器中輸入url會經(jīng)歷域名解析行楞、建立TCP連接、發(fā)送http請求子房、資源解析等步驟就轧。
http緩存優(yōu)化是網(wǎng)頁性能優(yōu)化的重要一環(huán)田度,這一部分我會在后續(xù)筆記中做一個詳細(xì)總結(jié)妒御,所以本文暫不多做詳細(xì)整理镇饺。本文主要從網(wǎng)頁渲染過程乎莉、網(wǎng)頁交互以及Vue應(yīng)用優(yōu)化三個角度對性能優(yōu)化做一個小結(jié)奸笤。
一、頁面加載及渲染過程優(yōu)化
瀏覽器渲染流程
首先談?wù)勀玫椒?wù)端資源后瀏覽器渲染的流程:
- 解析 HTML 文件监右,構(gòu)建 DOM 樹,同時瀏覽器主進(jìn)程負(fù)責(zé)下載 CSS 文件
- CSS 文件下載完成秸侣,解析 CSS 文件成樹形的數(shù)據(jù)結(jié)構(gòu),然后結(jié)合 DOM 樹合并成 RenderObject 樹
- 布局 RenderObject 樹 (Layout/reflow)味榛,負(fù)責(zé) RenderObject 樹中的元素的尺寸,位置等計算
- 繪制 RenderObject 樹 (paint)搏色,繪制頁面的像素信息
- 瀏覽器主進(jìn)程將默認(rèn)的圖層和復(fù)合圖層交給 GPU 進(jìn)程,GPU 進(jìn)程再將各個圖層合成(composite)频轿,最后顯示出頁面
CRP(關(guān)鍵渲染路徑Critical Rendering Path)優(yōu)化
關(guān)鍵渲染路徑是瀏覽器將 HTML、CSS航邢、JavaScript 轉(zhuǎn)換為在屏幕上呈現(xiàn)的像素內(nèi)容所經(jīng)歷的一系列步驟。也就是我們剛剛提到的的的瀏覽器渲染流程膳殷。
為盡快完成首次渲染,我們需要最大限度減小以下三種可變因素:
- 關(guān)鍵資源的數(shù)量: 可能阻止網(wǎng)頁首次渲染的資源赚窃。
- 關(guān)鍵路徑長度: 獲取所有關(guān)鍵資源所需的往返次數(shù)或總時間。
- 關(guān)鍵字節(jié): 實(shí)現(xiàn)網(wǎng)頁首次渲染所需的總字節(jié)數(shù)勒极,等同于所有關(guān)鍵資源傳送文件大小的總和。
優(yōu)化 DOM
- 刪除不必要的代碼和注釋包括空格辱匿,盡量做到最小化文件炫彩。
- 可以利用 GZIP 壓縮文件散休。
- 結(jié)合 HTTP 緩存文件媒楼。
優(yōu)化 CSSOM
首先戚丸,DOM 和 CSSOM 通常是并行構(gòu)建的,所以 CSS 加載不會阻塞 DOM 的解析限府。
然而,由于 Render Tree 是依賴于 DOM Tree 和 CSSOM Tree 的胁勺,
所以他必須等待到 CSSOM Tree 構(gòu)建完成,也就是 CSS 資源加載完成(或者 CSS 資源加載失敗)后署穗,才能開始渲染。因此案疲,CSS 加載會阻塞 Dom 的渲染。
由此可見褐啡,對于 CSSOM 縮小、壓縮以及緩存同樣重要备畦,我們可以從這方面考慮去優(yōu)化。
- 減少關(guān)鍵 CSS 元素數(shù)量
- 當(dāng)我們聲明樣式表時懂盐,請密切關(guān)注媒體查詢的類型,它們極大地影響了 CRP 的性能 莉恼。
優(yōu)化 JavaScript
當(dāng)瀏覽器遇到 script 標(biāo)記時,會阻止解析器繼續(xù)操作类垫,直到 CSSOM 構(gòu)建完畢琅坡,JavaScript 才會運(yùn)行并繼續(xù)完成 DOM 構(gòu)建過程。
- async: 當(dāng)我們在 script 標(biāo)記添加 async 屬性以后榆俺,瀏覽器遇到這個 script 標(biāo)記時會繼續(xù)解析 DOM坞淮,同時腳本也不會被 CSSOM 阻止,即不會阻止 CRP陪捷。
- defer: 與 async 的區(qū)別在于,腳本需要等到文檔解析后( DOMContentLoaded 事件前)執(zhí)>行市袖,而 async 允許腳本在文檔解析時位于后臺運(yùn)行(兩者下載的過程不會阻塞 DOM,但執(zhí)行會)苍碟。
- 當(dāng)我們的腳本不會修改 DOM 或 CSSOM 時,推薦使用 async 微峰。
- 預(yù)加載 —— preload & prefetch 。
- DNS 預(yù)解析 —— dns-prefetch 蜓肆。
小結(jié)
- 分析并用 關(guān)鍵資源數(shù) 關(guān)鍵字節(jié)數(shù) 關(guān)鍵路徑長度 來描述我們的 CRP 。
- 最小化關(guān)鍵資源數(shù): 消除它們(內(nèi)聯(lián))症概、推遲它們的下載(defer)或者使它們異步解析(async)等等 。
- 優(yōu)化關(guān)鍵字節(jié)數(shù)(縮小穴豫、壓縮)來減少下載時間 。
- 優(yōu)化加載剩余關(guān)鍵資源的順序: 讓關(guān)鍵資源(CSS)盡早下載以減少 CRP 長度 精肃。
瀏覽器重繪(Repaint)和回流(Reflow)
回流必將引起重繪,重繪不一定會引起回流司抱。
重繪(Repaint)
當(dāng)頁面中元素樣式的改變并不影響它在文檔流中的位置時(例如:color、background-color习柠、visibility 等),瀏覽器會將新樣式賦予給元素并重新繪制它资溃,這個過程稱為重繪。
回流(Reflow)
當(dāng) Render Tree 中部分或全部元素的尺寸溶锭、結(jié)構(gòu)、或某些屬性發(fā)生改變時趴捅,瀏覽器重新渲染部分或全部文檔的過程稱為回流垫毙。
會導(dǎo)致回流的操作:
* 頁面首次渲染
* 瀏覽器窗口大小發(fā)生改變
* 元素尺寸或位置發(fā)生改變元素內(nèi)容變化(文字?jǐn)?shù)量或圖片大小等等)
* 元素字體大小變化
* 添加或者刪除可見的 DOM 元素
* 激活 CSS 偽類(例如:hover)
* 查詢某些屬性或調(diào)用某些方法
* 一些常用且會導(dǎo)致回流的屬性和方法
clientWidth拱绑、clientHeight、clientTop猎拨、clientLeftoffsetWidth、offsetHeight迟几、offsetTop、offsetLeftscrollWidth类腮、scrollHeight、scrollTop蚜枢、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()厂抽、getComputedStyle()、
getBoundingClientRect()筷凤、scrollTo()
性能影響
回流比重繪的代價要更高。
有時即使僅僅回流一個單一的元素藐守,它的父元素以及任何跟隨它的元素也會產(chǎn)生回流。現(xiàn)代瀏覽器會對頻繁的回流或重繪操作進(jìn)行優(yōu)化:瀏覽器會維護(hù)一個隊(duì)列卢厂,把所有引起回流和重繪的操作放入隊(duì)列中,如果隊(duì)列中的任務(wù)數(shù)量或者時間間隔達(dá)到一個閾值的慎恒,瀏覽器就會將隊(duì)列清空,進(jìn)行一次批處理融柬,這樣可以把多次回流和重繪變成一次。
當(dāng)你訪問以下屬性或方法時粒氧,瀏覽器會立刻清空隊(duì)列:
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth门怪、offsetHeight骡澈、offsetTop掷空、offsetLeft
scrollWidth、scrollHeight坦弟、scrollTop、scrollLeft
width酿傍、height
getComputedStyle()
getBoundingClientRect()
因?yàn)殛?duì)列中可能會有影響到這些屬性或方法返回值的操作,即使你希望獲取的信息與隊(duì)列中操作引發(fā)的改變無關(guān)赤炒,瀏覽器也會強(qiáng)行清空隊(duì)列,確保你拿到的值是最精確的莺褒。
如何避免
css
避免使用 table 布局。
盡可能在 DOM 樹的最末端改變 class遵岩。
避免設(shè)置多層內(nèi)聯(lián)樣式。
將動畫效果應(yīng)用到 position 屬性為 absolute 或 fixed 的元素上尘执。
避免使用 CSS 表達(dá)式(例如:calc())。
Javascript
避免頻繁操作樣式正卧,最好一次性重寫 style 屬性,或者將樣式列表定義為 class 并一次性更改 class 屬性炉旷。
// 優(yōu)化前
const el = document.getElementById('test');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 優(yōu)化后,一次性修改樣式,這樣可以將三次重排減少到一次重排
const el = document.getElementById('test');
el.style.cssText += '; border-left: 1px ;border-right: 2px; padding: 5px;'
避免頻繁操作 DOM窘行,創(chuàng)建一個 documentFragment,在它上面應(yīng)用所有 DOM 操作罐盔,最后再把它添加到文檔中。
也可以先為元素設(shè)置 display: none,操作結(jié)束后再把它顯示出來六孵。因?yàn)樵?display 屬性為 none 的元素上進(jìn)行的 DOM 操作不會引發(fā)回流和重繪。
避免頻繁讀取會引發(fā)回流/重繪的屬性劫窒,如果確實(shí)需要多次使用,就用一個變量緩存起來主巍。
對具有復(fù)雜動畫的元素使用絕對定位,使它脫離文檔流孕索,否則會引起父元素及后續(xù)元素頻繁回流。
圖片懶加載
圖片懶加載在一些圖片密集型的網(wǎng)站中運(yùn)用比較多搞旭,通過圖片懶加載可以讓一些不可視的圖片不去加載,避免一次性加載過多的圖片導(dǎo)致請求阻塞(瀏覽器一般對同一域名下的并發(fā)請求的連接數(shù)有限制)选脊,這樣就可以提高網(wǎng)站的加載速度,提高用戶體驗(yàn)恳啥。
原理
將頁面中的img標(biāo)簽src指向一張小圖片或者src為空,然后定義data-src(這個屬性可以自定義命名钝的,我才用data-src)屬性指向真實(shí)的圖片。src指向一張默認(rèn)的圖片硝桩,否則當(dāng)src為空時也會向服務(wù)器發(fā)送一次請求⊥爰梗可以指向loading的地址。注意衙伶,圖片要指定寬高。
<img src="default.jpg" data-src="666.jpg" />
當(dāng)載入頁面時矢劲,先把可視區(qū)域內(nèi)的img標(biāo)簽的data-src屬性值負(fù)給src,然后監(jiān)聽滾動事件芬沉,把用戶即將看到的圖片加載躺同。這樣便實(shí)現(xiàn)了懶加載丸逸。
實(shí)例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
img {
display: block;
margin-bottom: 50px;
width: 400px;
height: 400px;
}
</style>
</head>
<body>
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<img src="Go.png" data-src="./lifecycle.jpeg" alt="">
<script>
let num = document.getElementsByTagName('img').length;
let img = document.getElementsByTagName("img");
let n = 0; //存儲圖片加載到的位置,避免每次都從第一張圖片開始遍歷
lazyload(); //頁面載入完畢加載可是區(qū)域內(nèi)的圖片
window.onscroll = lazyload;
function lazyload() { //監(jiān)聽頁面滾動事件
let seeHeight = document.documentElement.clientHeight; //可見區(qū)域高度
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; //滾動條距離頂部高度
for (let i = n; i < num; i++) {
if (img[i].offsetTop < seeHeight + scrollTop) {
if (img[i].getAttribute("src") == "Go.png") {
img[i].src = img[i].getAttribute("data-src");
}
n = i + 1;
}
}
}
</script>
</body>
</html>
事件委托
事件委托其實(shí)就是利用JS事件冒泡機(jī)制把原本需要綁定在子元素的響應(yīng)事件(click椭员、keydown……)委托給父元素,讓父元素?fù)?dān)當(dāng)事件監(jiān)聽的職務(wù)隘击。事件代理的原理是DOM元素的事件冒泡研铆。
優(yōu)點(diǎn):
- 大量減少內(nèi)存占用埋同,減少事件注冊棵红。
- 新增元素實(shí)現(xiàn)動態(tài)綁定事件
例如有一個列表需要綁定點(diǎn)擊事件,每一個列表項(xiàng)的點(diǎn)擊都需要返回不同的結(jié)果逆甜。
傳統(tǒng)寫法:
<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>
<script>
(function () {
var color_list = document.querySelectorAll('li')
console.log("color_list", color_list)
for (let item of color_list) {
item.onclick = showColor;
}
function showColor(e) {
alert(e.target.innerHTML)
console.log("showColor -> e.target", e.target.innerHTML)
}
})();
</script>
傳統(tǒng)方法會利用for循環(huán)遍歷列表為每一個列表元素綁定點(diǎn)擊事件,當(dāng)列表中元素數(shù)量非常龐大時交煞,需要綁定大量的點(diǎn)擊事件,這種方式就會產(chǎn)生性能問題素征。這種情況下利用事件委托就能很好的解決這個問題。
改用事件委托:
<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>
<script>
(function () {
var color_list = document.getElementByid('color-list');
color_list.addEventListener('click', showColor, true);
function showColor(e) {
var x = e.target;
if (x.nodeName.toLowerCase() === 'li') {
alert(x.innerHTML);
}
}
})();
</script>
二御毅、渲染完成后的頁面交互優(yōu)化:
防抖(debounce)/節(jié)流(throttle)
防抖(debounce)
輸入搜索時,可以用防抖debounce等優(yōu)化方式端蛆,減少http請求;
這里以滾動條事件舉例:防抖函數(shù) onscroll 結(jié)束時觸發(fā)一次今豆,延遲執(zhí)行
function debounce(func, wait) {
let timeout;
return function() {
let context = this; // 指向全局
let args = arguments;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
func.apply(context晚凿, args); // context.func(args)
}, wait);
};
}
// 使用
window.onscroll = debounce(function() {
console.log('debounce');
}歼秽, 1000);
節(jié)流(throttle)
節(jié)流函數(shù):只允許一個函數(shù)在N秒內(nèi)執(zhí)行一次。滾動條調(diào)用接口時,可以用節(jié)流throttle等優(yōu)化方式箩祥,減少http請求;
下面還是一個簡單的滾動條事件節(jié)流函數(shù):節(jié)流函數(shù) onscroll 時袍祖,每隔一段時間觸發(fā)一次,像水滴一樣
function throttle(fn蕉陋, delay) {
let prevTime = Date.now();
return function() {
let curTime = Date.now();
if (curTime - prevTime > delay) {
fn.apply(this, arguments);
prevTime = curTime;
}
};
}
// 使用
var throtteScroll = throttle(function() {
console.log('throtte');
}凳鬓, 1000);
window.onscroll = throtteScroll;
參考鏈接:https://zhuanlan.zhihu.com/p/113864878?from_voters_page=true