先從一道題目說起
從輸入 URL 到頁面加載完成剧罩,發(fā)生了什么座泳?
- 站在性能優(yōu)化的角度幕与;我們可以分為5個過程啦鸣;
- DNS 解析
- TCP 連接
- HTTP 請求拋出
- 服務(wù)端處理請求,HTTP 響應(yīng)返回
- 瀏覽器拿到響應(yīng)數(shù)據(jù)诫给,解析響應(yīng)內(nèi)容啦扬,把解析的結(jié)果展示給用戶
-
我們從這五個過程個個擊破;
dns解析花時間扑毡,tcp連接慢;這些需要我們的服務(wù)端解決勋又;
那么我們的前端工程師在HTTP請求或者瀏覽器端能做一些什么優(yōu)化呢泉褐?
http方面前端可以減少請求次數(shù),壓縮體積膜赃;
瀏覽器前端可以做的事情比較多了,例如資源加載優(yōu)化端铛、服務(wù)端渲染疲眷、瀏覽器緩存機制的利用、DOM 樹的構(gòu)建狂丝、網(wǎng)頁排版和渲染過程、回流與重繪的考量倍试、DOM 操作的合理規(guī)避
1669f5358f63c0f8 (1).png
網(wǎng)絡(luò)層面(http請求優(yōu)化and減少網(wǎng)絡(luò)請求)
webpack打包體積優(yōu)化
- webpack-bundle-analyzer 是一款包可視化工具蛋哭,可以找出體積大的模塊;
- 刪除冗余代碼 webpack3可以使用UglifyJsPlugin 躁愿;webpack4已經(jīng)自帶了,只需要配置下彤钟;
- 按需加載 vue項目可以用require.ensure來實現(xiàn)
- gzip 本來是服務(wù)端的工作,webpack也有g(shù)zip可以幫助服務(wù)端減輕壓力
圖片
- JPEG/JPG:有損壓縮吠勘、體積小性芬、加載快、不支持透明辫樱;龐大的圖片用jpg
- PNG-8 與 PNG-24:無損壓縮俊庇、質(zhì)量高、體積大辉饱、支持透明 像logo類等比較突出的最好用png
- SVG (字體圖標):文本文件、體積小缔逛、不失真、兼容性好
- Base64 :文本文件褐奴、依賴編碼于毙、小圖標解決方案,Base64 是作為雪碧圖的補充而存在的;Base64 編碼后,圖片大小會膨脹為原文件的 4/3;在傳輸非常小的圖片的時候脖旱,Base64 帶來的文件體積膨脹、以及瀏覽器解析 Base64 的時間開銷萌庆,與它節(jié)省掉的 HTTP 請求開銷相比甘耿,可以忽略不計
- CSS Sprites(精靈圖/雪碧圖):小圖標解決方案
- WebP :與 PNG 相比,WebP 無損圖像的尺寸縮小了 26%佳恬。限制WebP發(fā)展的是瀏覽器兼容問題于游;
瀏覽器緩存
- Memory Cache 內(nèi)存緩存是快的垫言,也是“短命”的。
- Service Worker Cache 幫我們實現(xiàn)離線緩存蚌成、消息推送和網(wǎng)絡(luò)代理等功能,但必需以https 協(xié)議為前提
- Push Cache HTTP2存在担忧,Push Cache 是緩存的最后一道防線坯癣,會話階段的緩存;
- HTTP Cache (主要示罗、最具有代表性的)
HTTP緩存分為強緩存和協(xié)商緩存
強緩存:Expires 和 Cache-Control (http1.1新增)兩個字段來控制
expires 能做的事情,Cache-Control 都能做轧房;expires 完成不了的事情绍绘,Cache-Control 也能做。因此脯倒,Cache-Control 可以視作是 expires 的完全替代方案。Cache-Control 相對于 expires 更加準確剪撬,它的優(yōu)先級也更高悠反。當 Cache-Control 與 expires 同時出現(xiàn)時,我們以 Cache-Control 為準斋否。
public 與 private 是針對資源是否能夠被代理服務(wù)緩存而存在的一組對立概念。
no-cache 繞開了瀏覽器:我們?yōu)橘Y源設(shè)置了 no-cache 后疫诽,每一次發(fā)起請求都不會再去詢問瀏覽器的緩存情況,而是直接向服務(wù)端去確認該資源是否過期奇徒;no-store 比較絕情,顧名思義就是不使用任何緩存策略罢低。在 no-cache 的基礎(chǔ)上,它連服務(wù)端的緩存確認也繞開了网持,只允許你直接向服務(wù)端發(fā)送請求长踊、并下載完整的響應(yīng)。
協(xié)商緩存:協(xié)商緩存機制下日杈,瀏覽器需要向服務(wù)器去詢問緩存的相關(guān)信息,進而判斷是重新發(fā)起請求、下載完整的響應(yīng)酿炸,還是從本地獲取緩存的資源。資源會被重定向到瀏覽器緩存填硕,這種情況下網(wǎng)絡(luò)請求對應(yīng)的狀態(tài)碼是 304
165f701820fafcf8 (1).png
當我們的資源內(nèi)容不可復(fù)用時扁眯,直接為 Cache-Control 設(shè)置 no-store,拒絕一切形式的緩存姻檀;否則考慮是否每次都需要向服務(wù)器進行緩存有效確認,如果需要胶台,那么設(shè) Cache-Control 的值為 no-cache;否則考慮該資源是否可以被代理服務(wù)器緩存诈唬,根據(jù)其結(jié)果決定是設(shè)置為 private 還是 public缩麸;然后考慮該資源的過期時間,設(shè)置對應(yīng)的 max-age 和 s-maxage 值;最后济竹,配置協(xié)商緩存需要用到的 Etag霎槐、Last-Modified 等參數(shù)。
本地存儲
- cookie 只能存儲4KB 丘跌,緊跟域名的
- Web Storage Local Storage和Session Storage 這兩個對前端來說很熟悉了;
- IndexDB 運行在瀏覽器上的非關(guān)系型數(shù)據(jù)庫耸棒;IndexDB 是沒有存儲上限的(一般來說不會小于 250M)
cdn
- 內(nèi)容分發(fā)網(wǎng)絡(luò)
- 緩存和回源报辱。
緩存”就是說我們把資源 copy 一份到 CDN 服務(wù)器上這個過程,“回源”就是說 CDN 發(fā)現(xiàn)自己沒有這個資源(一般是緩存的數(shù)據(jù)過期了)碍现,轉(zhuǎn)頭向根服務(wù)器(指業(yè)務(wù)服務(wù)器)或者它的上層服務(wù)器去要這個資源的過程。
- CDN 往往被用來存放靜態(tài)資源
所謂“靜態(tài)資源”爽篷,就是像 JS、CSS逐工、圖片等不需要業(yè)務(wù)服務(wù)器進行計算即得的資源漂辐。而“動態(tài)資源”,顧名思義是需要后端實時動態(tài)生成的資源髓涯,較為常見的就是 JSP、ASP 或者依賴服務(wù)端渲染得到的 HTML 頁面瘤泪。
- 性能優(yōu)化方面的應(yīng)用
同一個域名下的請求會不分青紅皂白地攜帶 Cookie育八,而靜態(tài)資源往往并不需要 Cookie 攜帶什么認證信息。把靜態(tài)資源和主頁面置于不同的域名下髓棋,完美地避免了不必要的 Cookie 的出現(xiàn)惶洲!
服務(wù)端渲染(SSR)
- 客戶端渲染:需要把js文件跑完膳犹,生成對應(yīng)的dom樹;
- 服務(wù)端渲染:直接拿到服務(wù)端放回的html就可以呈現(xiàn)在用戶面前
- 質(zhì)上是本該瀏覽器做的事情铐料,分擔(dān)給服務(wù)器去做。這樣當資源抵達瀏覽器時钠惩,它呈現(xiàn)的速度就快了族阅。
CSSOM,JS的優(yōu)化
瀏覽器背后的運行機制
CSS 選擇符是從右到左進行匹配的
避免使用通配符愧沟,只對需要用到的元素進行選擇鲤遥。
關(guān)注可以通過繼承實現(xiàn)的屬性,避免重復(fù)匹配重復(fù)定義渴频。
少用標簽選擇器。如果可以,用類選擇器替代
不要畫蛇添足咕村,id 和 class 選擇器不應(yīng)該被多余的標簽選擇器拖后腿。
減少嵌套懈涛。后代選擇器的開銷是最高的,因此我們應(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í)行是被推遲的指郁。等整個文檔解析完成拷呆、DOMContentLoaded 事件即將被觸發(fā)時疫粥,被標記了 defer 的 JS 文件才會開始依次執(zhí)行腰懂。
從應(yīng)用的角度來說,一般當我們的腳本與 DOM 元素和其它腳本之間的依賴關(guān)系不強時绣溜,我們會選用 async;當腳本依賴于 DOM 元素和其它腳本的執(zhí)行結(jié)果時底哗,我們會選用 defer。
DOM的優(yōu)化
- 回流和重繪
回流:當我們對 DOM 的修改引發(fā)了 DOM 幾何尺寸的變化(比如修改元素的寬艘虎、高或隱藏元素等)時咒吐,瀏覽器需要重新計算元素的幾何屬性(其他元素的幾何屬性和位置也會因此受到影響),然后再將計算的結(jié)果繪制出來恬叹。這個過程就是回流(也叫重排)。
重繪:當我們對 DOM 的修改導(dǎo)致了樣式的變化绽昼、卻并未影響其幾何屬性(比如修改了顏色或背景色)時,瀏覽器不需重新計算元素的幾何屬性目溉、直接為該元素繪制新的樣式(跳過了上圖所示的回流環(huán)節(jié))。這個過程叫做重繪缭付。
由此我們可以看出循未,重繪不一定導(dǎo)致回流,回流一定會導(dǎo)致重繪的妖。我們對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先用變量保存好要計算的值,最終再設(shè)置dom
避免逐條改變樣式颜及,使用類名去合并樣式
將 DOM “離線”,先設(shè)置display:none;中間操作蹂楣,后面再設(shè)置display:block;
- 瀏覽器Flush 隊列
- DOM Fragment 需要了解一下
本質(zhì)上是作為脫離了真實 DOM 樹的容器出現(xiàn),用于緩存批量化的 DOM 操作
Event Loop 與異步更新策略
- macro(洪任務(wù)): setTimeout痊土、setInterval、 setImmediate犯祠、script(整體代碼)、 I/O 操作雷则、UI 渲染等肪笋。
- micro-task(微任務(wù)): process.nextTick度迂、Promise、MutationObserver
當我們需要在異步任務(wù)中實現(xiàn) DOM 修改時惭墓,把它包裝成 micro 任務(wù)是相對明智的選擇坛梁。
- 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)
}
})
// 檢查上一個異步任務(wù)隊列(即名為callbacks的任務(wù)數(shù)組)是否派發(fā)和執(zhí)行完畢了划咐。pending此處相當于一個鎖
if (!pending) {
// 若上一個異步任務(wù)隊列已經(jīng)執(zhí)行完畢,則將pending設(shè)定為true(把鎖鎖上)
pending = true
// 是否要求一定要派發(fā)為macro任務(wù)
if (useMacroTask) {
macroTimerFunc()
} else {
// 如果不說明一定要macro 你們就全都是micro
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
實際上也是運用了promise
lazy-load(懶加載)
在懶加載的實現(xiàn)中褐缠,有兩個關(guān)鍵的數(shù)值:一個是當前可視區(qū)域的高度,另一個是元素距離可視區(qū)域頂部的高度队魏。
事件的節(jié)流(throttle)與防抖(debounce)
像scroll,resize官帘,keyup等事件頻繁觸發(fā)會引發(fā)頁面的抖動甚至卡頓
節(jié)流”與“防抖”是以閉包的形式來實現(xiàn)的;
它們通過對事件對應(yīng)的回調(diào)函數(shù)進行包裹刽虹、以自由變量的形式緩存時間信息呢诬,最后用 setTimeout 來控制事件的觸發(fā)頻率。
- 節(jié)流和防抖結(jié)合體
// fn是我們需要包裝的事件回調(diào), delay是時間間隔的閾值
function throttle(fn, delay) {
// last為上一次觸發(fā)回調(diào)的時間, timer是定時器
let last = 0, timer = null
// 將throttle處理結(jié)果當作函數(shù)返回
return function () {
// 保留調(diào)用時的this上下文
let context = this
// 保留調(diào)用時傳入的參數(shù)
let args = arguments
// 記錄本次觸發(fā)回調(diào)的時間
let now = +new Date()
// 判斷上次觸發(fā)的時間和本次觸發(fā)的時間差是否小于時間間隔的閾值
if (now - last < delay) {
// 如果時間間隔小于我們設(shè)定的時間間隔閾值膛虫,則為本次觸發(fā)操作設(shè)立一個新的定時器
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, delay)
} else {
// 如果時間間隔超出了我們設(shè)定的時間間隔閾值,那就不等了稍刀,無論如何要反饋給用戶一次響應(yīng)
last = now
fn.apply(context, args)
}
}
}
// 用新的throttle包裝scroll的回調(diào)
document.addEventListener('scroll', throttle(() => console.log('觸發(fā)了滾動事件'), 1000))...