先從一道題目說(shuō)起
從輸入 URL 到頁(yè)面加載完成,發(fā)生了什么?
- 站在性能優(yōu)化的角度;我們可以分為5個(gè)過(guò)程;
- DNS 解析
- TCP 連接
- HTTP 請(qǐng)求拋出
- 服務(wù)端處理請(qǐng)求,HTTP 響應(yīng)返回
- 瀏覽器拿到響應(yīng)數(shù)據(jù),解析響應(yīng)內(nèi)容,把解析的結(jié)果展示給用戶
-
我們從這五個(gè)過(guò)程個(gè)個(gè)擊破;
dns解析花時(shí)間,tcp連接慢;這些需要我們的服務(wù)端解決;
那么我們的前端工程師在HTTP請(qǐng)求或者瀏覽器端能做一些什么優(yōu)化呢?
http方面前端可以減少請(qǐng)求次數(shù),壓縮體積;
瀏覽器前端可以做的事情比較多了,例如資源加載優(yōu)化、服務(wù)端渲染、瀏覽器緩存機(jī)制的利用、DOM 樹的構(gòu)建、網(wǎng)頁(yè)排版和渲染過(guò)程、回流與重繪的考量、DOM 操作的合理規(guī)避
1669f5358f63c0f8 (1).png
網(wǎng)絡(luò)層面(http請(qǐng)求優(yōu)化and減少網(wǎng)絡(luò)請(qǐng)求)
webpack打包體積優(yōu)化
- webpack-bundle-analyzer 是一款包可視化工具,可以找出體積大的模塊;
- 刪除冗余代碼 webpack3可以使用UglifyJsPlugin ;webpack4已經(jīng)自帶了,只需要配置下;
- 按需加載 vue項(xiàng)目可以用require.ensure來(lái)實(shí)現(xiàn)
- gzip 本來(lái)是服務(wù)端的工作,webpack也有g(shù)zip可以幫助服務(wù)端減輕壓力
圖片
- JPEG/JPG:有損壓縮、體積小、加載快、不支持透明;龐大的圖片用jpg
- PNG-8 與 PNG-24:無(wú)損壓縮、質(zhì)量高、體積大、支持透明 像logo類等比較突出的最好用png
- SVG (字體圖標(biāo)):文本文件、體積小、不失真、兼容性好
- Base64 :文本文件、依賴編碼、小圖標(biāo)解決方案,Base64 是作為雪碧圖的補(bǔ)充而存在的;Base64 編碼后,圖片大小會(huì)膨脹為原文件的 4/3;在傳輸非常小的圖片的時(shí)候,Base64 帶來(lái)的文件體積膨脹、以及瀏覽器解析 Base64 的時(shí)間開銷,與它節(jié)省掉的 HTTP 請(qǐng)求開銷相比,可以忽略不計(jì)
- CSS Sprites(精靈圖/雪碧圖):小圖標(biāo)解決方案
- WebP :與 PNG 相比,WebP 無(wú)損圖像的尺寸縮小了 26%。限制WebP發(fā)展的是瀏覽器兼容問(wèn)題;
瀏覽器緩存
- Memory Cache 內(nèi)存緩存是快的,也是“短命”的。
- Service Worker Cache 幫我們實(shí)現(xiàn)離線緩存、消息推送和網(wǎng)絡(luò)代理等功能,但必需以https 協(xié)議為前提
- Push Cache HTTP2存在,Push Cache 是緩存的最后一道防線,會(huì)話階段的緩存;
- HTTP Cache (主要、最具有代表性的)
HTTP緩存分為強(qiáng)緩存和協(xié)商緩存
強(qiáng)緩存:Expires 和 Cache-Control (http1.1新增)兩個(gè)字段來(lái)控制
expires 能做的事情,Cache-Control 都能做;expires 完成不了的事情,Cache-Control 也能做。因此,Cache-Control 可以視作是 expires 的完全替代方案。Cache-Control 相對(duì)于 expires 更加準(zhǔn)確,它的優(yōu)先級(jí)也更高。當(dāng) Cache-Control 與 expires 同時(shí)出現(xiàn)時(shí),我們以 Cache-Control 為準(zhǔn)。
public 與 private 是針對(duì)資源是否能夠被代理服務(wù)緩存而存在的一組對(duì)立概念。
no-cache 繞開了瀏覽器:我們?yōu)橘Y源設(shè)置了 no-cache 后,每一次發(fā)起請(qǐng)求都不會(huì)再去詢問(wèn)瀏覽器的緩存情況,而是直接向服務(wù)端去確認(rèn)該資源是否過(guò)期;no-store 比較絕情,顧名思義就是不使用任何緩存策略。在 no-cache 的基礎(chǔ)上,它連服務(wù)端的緩存確認(rèn)也繞開了,只允許你直接向服務(wù)端發(fā)送請(qǐng)求、并下載完整的響應(yīng)。
協(xié)商緩存:協(xié)商緩存機(jī)制下,瀏覽器需要向服務(wù)器去詢問(wèn)緩存的相關(guān)信息,進(jìn)而判斷是重新發(fā)起請(qǐng)求、下載完整的響應(yīng),還是從本地獲取緩存的資源。資源會(huì)被重定向到瀏覽器緩存,這種情況下網(wǎng)絡(luò)請(qǐng)求對(duì)應(yīng)的狀態(tài)碼是 304
165f701820fafcf8 (1).png
當(dāng)我們的資源內(nèi)容不可復(fù)用時(shí),直接為 Cache-Control 設(shè)置 no-store,拒絕一切形式的緩存;否則考慮是否每次都需要向服務(wù)器進(jìn)行緩存有效確認(rèn),如果需要,那么設(shè) Cache-Control 的值為 no-cache;否則考慮該資源是否可以被代理服務(wù)器緩存,根據(jù)其結(jié)果決定是設(shè)置為 private 還是 public;然后考慮該資源的過(guò)期時(shí)間,設(shè)置對(duì)應(yīng)的 max-age 和 s-maxage 值;最后,配置協(xié)商緩存需要用到的 Etag、Last-Modified 等參數(shù)。
本地存儲(chǔ)
- cookie 只能存儲(chǔ)4KB ,緊跟域名的
- Web Storage Local Storage和Session Storage 這兩個(gè)對(duì)前端來(lái)說(shuō)很熟悉了;
- IndexDB 運(yùn)行在瀏覽器上的非關(guān)系型數(shù)據(jù)庫(kù);IndexDB 是沒(méi)有存儲(chǔ)上限的(一般來(lái)說(shuō)不會(huì)小于 250M)
cdn
- 內(nèi)容分發(fā)網(wǎng)絡(luò)
- 緩存和回源。
緩存”就是說(shuō)我們把資源 copy 一份到 CDN 服務(wù)器上這個(gè)過(guò)程,“回源”就是說(shuō) CDN 發(fā)現(xiàn)自己沒(méi)有這個(gè)資源(一般是緩存的數(shù)據(jù)過(guò)期了),轉(zhuǎn)頭向根服務(wù)器(指業(yè)務(wù)服務(wù)器)或者它的上層服務(wù)器去要這個(gè)資源的過(guò)程。
- CDN 往往被用來(lái)存放靜態(tài)資源
所謂“靜態(tài)資源”,就是像 JS、CSS、圖片等不需要業(yè)務(wù)服務(wù)器進(jìn)行計(jì)算即得的資源。而“動(dòng)態(tài)資源”,顧名思義是需要后端實(shí)時(shí)動(dòng)態(tài)生成的資源,較為常見的就是 JSP、ASP 或者依賴服務(wù)端渲染得到的 HTML 頁(yè)面。
- 性能優(yōu)化方面的應(yīng)用
同一個(gè)域名下的請(qǐng)求會(huì)不分青紅皂白地?cái)y帶 Cookie,而靜態(tài)資源往往并不需要 Cookie 攜帶什么認(rèn)證信息。把靜態(tài)資源和主頁(yè)面置于不同的域名下,完美地避免了不必要的 Cookie 的出現(xiàn)!
服務(wù)端渲染(SSR)
- 客戶端渲染:需要把js文件跑完,生成對(duì)應(yīng)的dom樹;
- 服務(wù)端渲染:直接拿到服務(wù)端放回的html就可以呈現(xiàn)在用戶面前
- 質(zhì)上是本該瀏覽器做的事情,分擔(dān)給服務(wù)器去做。這樣當(dāng)資源抵達(dá)瀏覽器時(shí),它呈現(xiàn)的速度就快了。
CSSOM,JS的優(yōu)化
瀏覽器背后的運(yùn)行機(jī)制


CSS 選擇符是從右到左進(jìn)行匹配的
避免使用通配符,只對(duì)需要用到的元素進(jìn)行選擇。
關(guān)注可以通過(guò)繼承實(shí)現(xiàn)的屬性,避免重復(fù)匹配重復(fù)定義。
少用標(biāo)簽選擇器。如果可以,用類選擇器替代
不要畫蛇添足,id 和 class 選擇器不應(yīng)該被多余的標(biāo)簽選擇器拖后腿。
減少嵌套。后代選擇器的開銷是最高的,因此我們應(yīng)該盡量將選擇器的深度降到最低
JS的加載方式
正常模式 <script src="index.js"></script>
async 模式 <script async src="index.js"></script>
defer 模式 <script defer src="index.js"></script>
defer 模式下,JS 的加載是異步的,執(zhí)行是被推遲的。等整個(gè)文檔解析完成、DOMContentLoaded 事件即將被觸發(fā)時(shí),被標(biāo)記了 defer 的 JS 文件才會(huì)開始依次執(zhí)行。
從應(yīng)用的角度來(lái)說(shuō),一般當(dāng)我們的腳本與 DOM 元素和其它腳本之間的依賴關(guān)系不強(qiáng)時(shí),我們會(huì)選用 async;當(dāng)腳本依賴于 DOM 元素和其它腳本的執(zhí)行結(jié)果時(shí),我們會(huì)選用 defer。
DOM的優(yōu)化
- 回流和重繪
回流:當(dāng)我們對(duì) DOM 的修改引發(fā)了 DOM 幾何尺寸的變化(比如修改元素的寬、高或隱藏元素等)時(shí),瀏覽器需要重新計(jì)算元素的幾何屬性(其他元素的幾何屬性和位置也會(huì)因此受到影響),然后再將計(jì)算的結(jié)果繪制出來(lái)。這個(gè)過(guò)程就是回流(也叫重排)。
重繪:當(dāng)我們對(duì) DOM 的修改導(dǎo)致了樣式的變化、卻并未影響其幾何屬性(比如修改了顏色或背景色)時(shí),瀏覽器不需重新計(jì)算元素的幾何屬性、直接為該元素繪制新的樣式(跳過(guò)了上圖所示的回流環(huán)節(jié))。這個(gè)過(guò)程叫做重繪。
由此我們可以看出,重繪不一定導(dǎo)致回流,回流一定會(huì)導(dǎo)致重繪。我們對(duì)dom的優(yōu)化主要在于減少DOM操作;
- 回流的“導(dǎo)火索”
改變 DOM 元素的幾何屬性
改變 DOM 樹的結(jié)構(gòu)
獲取一些特定屬性的值:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight...
- 規(guī)避回流與重繪
js先用變量保存好要計(jì)算的值,最終再設(shè)置dom
避免逐條改變樣式,使用類名去合并樣式
將 DOM “離線”,先設(shè)置display:none;中間操作,后面再設(shè)置display:block;
- 瀏覽器Flush 隊(duì)列
- DOM Fragment 需要了解一下
本質(zhì)上是作為脫離了真實(shí) DOM 樹的容器出現(xiàn),用于緩存批量化的 DOM 操作
Event Loop 與異步更新策略
- macro(洪任務(wù)): setTimeout、setInterval、 setImmediate、script(整體代碼)、 I/O 操作、UI 渲染等。
- micro-task(微任務(wù)): process.nextTick、Promise、MutationObserver
當(dāng)我們需要在異步任務(wù)中實(shí)現(xiàn) DOM 修改時(shí),把它包裝成 micro 任務(wù)是相對(duì)明智的選擇。
- Vue狀態(tài)更新手法:nextTick
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 檢查上一個(gè)異步任務(wù)隊(duì)列(即名為callbacks的任務(wù)數(shù)組)是否派發(fā)和執(zhí)行完畢了。pending此處相當(dāng)于一個(gè)鎖
if (!pending) {
// 若上一個(gè)異步任務(wù)隊(duì)列已經(jīng)執(zhí)行完畢,則將pending設(shè)定為true(把鎖鎖上)
pending = true
// 是否要求一定要派發(fā)為macro任務(wù)
if (useMacroTask) {
macroTimerFunc()
} else {
// 如果不說(shuō)明一定要macro 你們就全都是micro
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
實(shí)際上也是運(yùn)用了promise
lazy-load(懶加載)
在懶加載的實(shí)現(xiàn)中,有兩個(gè)關(guān)鍵的數(shù)值:一個(gè)是當(dāng)前可視區(qū)域的高度,另一個(gè)是元素距離可視區(qū)域頂部的高度。
事件的節(jié)流(throttle)與防抖(debounce)
像scroll,resize,keyup等事件頻繁觸發(fā)會(huì)引發(fā)頁(yè)面的抖動(dòng)甚至卡頓
節(jié)流”與“防抖”是以閉包的形式來(lái)實(shí)現(xiàn)的;
它們通過(guò)對(duì)事件對(duì)應(yīng)的回調(diào)函數(shù)進(jìn)行包裹、以自由變量的形式緩存時(shí)間信息,最后用 setTimeout 來(lái)控制事件的觸發(fā)頻率。
- 節(jié)流和防抖結(jié)合體
// fn是我們需要包裝的事件回調(diào), delay是時(shí)間間隔的閾值
function throttle(fn, delay) {
// last為上一次觸發(fā)回調(diào)的時(shí)間, timer是定時(shí)器
let last = 0, timer = null
// 將throttle處理結(jié)果當(dāng)作函數(shù)返回
return function () {
// 保留調(diào)用時(shí)的this上下文
let context = this
// 保留調(diào)用時(shí)傳入的參數(shù)
let args = arguments
// 記錄本次觸發(fā)回調(diào)的時(shí)間
let now = +new Date()
// 判斷上次觸發(fā)的時(shí)間和本次觸發(fā)的時(shí)間差是否小于時(shí)間間隔的閾值
if (now - last < delay) {
// 如果時(shí)間間隔小于我們?cè)O(shè)定的時(shí)間間隔閾值,則為本次觸發(fā)操作設(shè)立一個(gè)新的定時(shí)器
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, delay)
} else {
// 如果時(shí)間間隔超出了我們?cè)O(shè)定的時(shí)間間隔閾值,那就不等了,無(wú)論如何要反饋給用戶一次響應(yīng)
last = now
fn.apply(context, args)
}
}
}
// 用新的throttle包裝scroll的回調(diào)
document.addEventListener('scroll', throttle(() => console.log('觸發(fā)了滾動(dòng)事件'), 1000))...

