你知道的前端優(yōu)化手段
點擊上方 前端瓶子君,關(guān)注公眾號
回復(fù)算法,加入前端編程面試算法每日一題群

來源:HuberTRoy
https://juejin.cn/post/6966857691381645325
會優(yōu)化,我就不是不優(yōu),就是看著慢,哎~,就是玩兒~。
前言
性能優(yōu)化是一個項目發(fā)展到一定時期之后繞不開的話題,也是每個工程師心中永遠(yuǎn)在撩撥的刺。
總結(jié)一下常用的前端性能優(yōu)化的方法,希望對大家有些幫助~。
性能可能帶來的影響(販賣焦慮警告??)
試想當(dāng)你做的酷炫特效因為慢了0.1秒就少被一個人看到時的落寞(ㄒoㄒ),
試想當(dāng)你引以為傲的細(xì)節(jié)交互因為慢了0.2秒就被競爭對手的平庸互動拉走用戶的氣憤(╯>д<)╯?˙3˙?,
試想當(dāng)你精心打造的漂亮頁面因為慢了0.3秒就被搜索引擎無情的排在后面的無奈∑(O_O;)。
所以,是時候重拳出擊了,重鑄性能的榮光,我輩義不容辭( ̄▽ ̄)/。
調(diào)試工具
Network面板
Network面板記錄了與服務(wù)器交互的具體細(xì)節(jié)。
在這里我們可以看到發(fā)起的請求數(shù)量,傳輸體積以及解壓縮后的體積,同時還可以知道哪些資源是命中了強緩存,哪些資源命中的協(xié)商緩存。
查看某一個請求的瀑布流可以讓我們清晰的看到一個資源從服務(wù)器到達(dá)我們的電腦所花的時間。
如上圖,排隊用了1.65ms,DNS查詢用了21.47ms,initial connection(進(jìn)行TCP握手的時間)用了56.25ms,SSL握手的時間用了37.87ms,然后又用了100多ms第一個字節(jié)到達(dá)我們的電腦(TTFB - 上面的查詢/建立),接收整個文檔花了17ms。
這時候我們基于上面的信息就可以粗略的得到,如果能在請求資源之前如果已經(jīng)得到DNS地址(預(yù)查詢)可以省去21ms,已經(jīng)進(jìn)行過握手可以省去100ms(預(yù)連接),如果干脆請求也不請求可以省去200ms(緩存)繼而針對這些點做對應(yīng)的策略。
Network面板可以讓我們初步評估網(wǎng)站性能,對網(wǎng)站整體的體積,網(wǎng)絡(luò)的影響帶來一個整體的認(rèn)知,同時提供一些輔助功能,如禁用緩存,block某些資源。
lighthouse面板
lighthouse是對網(wǎng)站整體的評估,通過幾個不同的指標(biāo)給網(wǎng)站進(jìn)行打分。
First Contentful Paint 首屏渲染時間,Chrome會取第一個渲染出來的元素作為時間參考。
Time to Interactive 可交互時間,從能看到能摸的時間點。
Speed Index 速度指數(shù),頁面的填充速度。
Total Blocking Time 從能看到能摸之間超過50ms的任務(wù)總和。
Largest Contentful Paint 頁面中最大的那塊渲染的時間點。
Cumulative Layout Shift 元素移動所累積的時間點,比如有一個absolute的元素突然從左邊移到了右邊。
同時針對網(wǎng)站的信息,lighthouse還會給出一些完善建議:
這些建議可以幫助我們在接下來的優(yōu)化中提供一個大致的方向。
performance面板
performance面板會給我們提供一個具體的執(zhí)行過程,從HTML文檔下載,解析HTML,到解析CSS,計算樣式,執(zhí)行JS。
火焰圖
從火焰圖我們可以找到長任務(wù),分析長任務(wù),或者找到某些無關(guān)緊要的任務(wù)把他們拆分,延后,優(yōu)化使他們達(dá)到一個理想狀態(tài)。
performance monitor面板
performance monitor讓我們監(jiān)控內(nèi)存和CPU的占用,它給出的是整體的占用數(shù)據(jù),可以用來觀察某一段代碼某一個特效會不會造成性能影響。
webpack-bundle-analyze

如果你用到了webpack打包,可以用它來分析打包后的文件,做成具體策略。
從輸入一個URL談起
這是一個URL為了見到你穿越無數(shù)路由器的感人故事。
DNS查詢
與服務(wù)器交互首先要進(jìn)行DNS查詢,得到服務(wù)器的IP地址,瀏覽器會首先查詢自己的緩存,之后會查詢本地HOSTS,如果仍然沒找到會發(fā)起向DNS服務(wù)器查詢的請求。
在這里我們可以做的優(yōu)化不多,DNS是我們相對不可控的一個條件,但我們?nèi)匀豢梢宰龅囊粋€優(yōu)化策略是預(yù)查詢。
進(jìn)行DNS預(yù)查詢
在文檔頂部我們可以將我們即將要請求的地址的DNS預(yù)先查詢,通過插入一個link標(biāo)簽
<link rel="dns-prefetch" >
來告知瀏覽器我們將要從這個地址(通常會是存放靜態(tài)資源的CDN的地址,)拉取數(shù)據(jù)了,你先查詢一下,當(dāng)用到的時候就可以直接拿到對應(yīng)的IP。
dns-prefetch
建立HTTP(TCP)連接
得到服務(wù)器IP之后,首先進(jìn)行三次握手,之后會進(jìn)行SSL握手(HTTPS),SSL握手時會向服務(wù)器端確認(rèn)HTTP的版本。
針對這方面的優(yōu)化,前端可做的事情不多,主要是服務(wù)器端的事情,不過仍然要了解一下前端可以看得到的策略。
keep-alive
由于TCP的可靠性,每條獨立的TCP連接都會進(jìn)行一次三次握手,從上面的Network的分析中可以得到握手往往會消耗大部分時間,真正的數(shù)據(jù)傳輸反而會少一些(當(dāng)然取決于內(nèi)容多少)。HTTP1.0和HTTP1.1為了解決這個問題在header中加入了Connection: Keep-Alive,keep-alive的連接會保持一段時間不斷開,后續(xù)的請求都會復(fù)用這一條TCP,不過由于管道化的原因也會發(fā)生隊頭阻塞的問題。
HTTP1.1默認(rèn)開啟Keep-Alive,HTTP1.0可能現(xiàn)在不多見了,如果你還在用,可以升級一下版本,或者帶上這個header。
connection keep-alive
HTTP2
HTTP2相對于HTTP1.1的一個主要升級是多路復(fù)用,多路復(fù)用通過更小的二進(jìn)制幀構(gòu)成多條數(shù)據(jù)流,交錯的請求和響應(yīng)可以并行傳輸而不被阻塞,這樣就解決了HTTP1.1時復(fù)用會產(chǎn)生的隊頭阻塞的問題,同時HTTP2有首部壓縮的功能,如果兩個請求首部(headers)相同,那么會省去這一部分,只傳輸不同的首部字段,進(jìn)一步減少請求的體積。
Nginx開啟HTTP2的方式特別容易,只需要加一句http2既可開啟:
server {
listen 443 ssl http2; # 加一句 http2.
server_name domain.com;
}
復(fù)制代碼
成本低廉,效果巨大。
HTTP2
緩存
緩存通過復(fù)用之前的獲取過的資源,可以顯著提高網(wǎng)站和應(yīng)用程序的性能,合理的緩存不僅可以節(jié)省巨大的流量也會讓用戶二次進(jìn)入時身心愉悅,如果一個資源完全走了本地緩存,那么就可以節(jié)省下整個與服務(wù)器交互的時間,如果整個網(wǎng)站的內(nèi)容都被緩存在本地,那即使離線也可以繼續(xù)訪問(很酷,但還沒有完全很酷)。
HTTP緩存主要分為兩種,一種是強緩存,另一種是協(xié)商緩存,都通過Headers控制。
整體流程如下:
強緩存
強緩存根據(jù)請求頭的Expires和Cache-Control判斷是否命中強緩存,命中強緩存的資源直接從本地加載,不會發(fā)起任何網(wǎng)絡(luò)請求。
Cache-Control的值有很多:
Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: only-if-cached
復(fù)制代碼
常用的有max-age,no-cache和no-store。
max-age 是資源從響應(yīng)開始計時的最大新鮮時間,一般響應(yīng)中還會出現(xiàn)age標(biāo)明這個資源當(dāng)前的新鮮程度。
no-cache 會讓瀏覽器緩存這個文件到本地但是不用,Network中disable-cache勾中的話就會在請求時帶上這個haader,會在下一次新鮮度驗證通過后使用這個緩存。
no-store 會完全放棄緩存這個文件。
服務(wù)器響應(yīng)時的Cache-Control略有不同,其中有兩個需要注意下:
-
public, public 表明這個請求可以被任何對象緩存,代理/CDN等中間商。 -
private,private 表明這個請求只能被終端緩存,不允許代理或者CDN等中間商緩存。
Expires是一個具體的日期,到了那個日期就會讓這個緩存失活,優(yōu)先級較低,存在max-age的情況下會被忽略,和本地時間綁定,修改本地時間可以繞過。
另外,如果你的服務(wù)器的返回內(nèi)容中不存在Expires,Cache-Control: max-age,或 Cache-Control:s-maxage但是存在Last-Modified時,那么瀏覽器默認(rèn)會采用一個啟發(fā)式的算法,即啟發(fā)式緩存。通常會取響應(yīng)頭的Date_value \- Last-Modified_value值的10%作為緩存時間,之后瀏覽器仍然會按強緩存來對待這個資源一段時間,如果你不想要緩存的話務(wù)必確保有no-cache或no-store在響應(yīng)頭中。
協(xié)商緩存
協(xié)商緩存一般會在強緩存新鮮度過期后發(fā)起,向服務(wù)器確認(rèn)是否需要更新本地的緩存文件,如果不需要更新,服務(wù)器會返回304否則會重新返回整個文件。
服務(wù)器響應(yīng)中會攜帶ETag和Last-Modified,Last-Modified 表示本地文件最后修改日期,瀏覽器會在request header加上If-Modified-Since(上次返回的Last-Modified的值),詢問服務(wù)器在該日期后資源是否有更新,有更新的話就會將新的資源發(fā)送回來。
但是如果在本地打開緩存文件,就會造成Last-Modified被修改,所以在HTTP / 1.1 出現(xiàn)了ETag。
Etag就像一個指紋,資源變化都會導(dǎo)致ETag變化,跟最后修改時間沒有關(guān)系,ETag可以保證每一個資源是唯一的
If-None-Match的header會將上次返回的ETag發(fā)送給服務(wù)器,詢問該資源的ETag是否有更新,有變動就會發(fā)送新的資源回來
ETag(If-None-Match)的優(yōu)先級高于Last-Modified(If-Modified-Since),優(yōu)先使用ETag進(jìn)行確認(rèn)。
協(xié)商緩存比強緩存稍慢,因為還是會發(fā)送請求到服務(wù)器進(jìn)行確認(rèn)。
CDN
CDN會把源站的資源緩存到CDN服務(wù)器,當(dāng)用戶訪問的時候就會從最近的CDN服務(wù)器拿取資源而不是從源站拿取,這樣做的好處是分散了壓力,同時也會提升返回訪問速度和穩(wěn)定性。
壓縮
合理的壓縮資源可以有效減少傳輸體積,減少傳輸體積的結(jié)果就是用戶更快的拿到資源開始解析。
壓縮在各個階段都會出現(xiàn),比如上面提到的HTTP2的首部壓縮,進(jìn)行到這一步的壓縮是指對整個資源文件進(jìn)行的壓縮。
瀏覽器在發(fā)起請求時會在headers中攜帶accept-encoding: gzip, deflate, br,告知服務(wù)器客戶端可以接受的壓縮算法,之后響應(yīng)資源會在響應(yīng)頭中攜帶content-encoding: gzip告知本文件的壓縮算法。
GZIP壓縮
GZIP是非常常用的壓縮算法,現(xiàn)代客戶端都會支持,你可以在上傳文件時就上傳一份壓縮后的文件,也可以讓Nginx動態(tài)壓縮。
進(jìn)行頁面渲染
關(guān)鍵渲染路徑
關(guān)鍵渲染路徑是瀏覽器將HTML/CSS/JS轉(zhuǎn)換為屏幕上看到的像素內(nèi)容所經(jīng)過的一系列步驟。
瀏覽器得到HTML后會開始解析DOM樹,CSS資源的下載不會阻塞解析DOM,但是也要注意,如果CSS未下載解析完成是會阻塞最終渲染的。
從Performance面板中可以清晰的看到瀏覽器如何解析HTML的:
得到HTML后首先會解析HTML,然后解析樣式,計算樣式,繪制圖層等等操作,JS腳本運行,之后可能會重復(fù)這一步驟。
在這里前端可以做的事情多了起來,接下來自頂向下說起。
渲染頁面
預(yù)加載/預(yù)連接內(nèi)容
和前面說的DNS預(yù)查詢一樣,可以將即將要用到的資源或者即將要握手的地址提前告知瀏覽器讓瀏覽器利用還在解析HTML計算樣式的時間去提前準(zhǔn)備好。
preload
使用link的preload屬性預(yù)加載一個資源。
<link rel="preload" href="style.css" as="style">
復(fù)制代碼
as屬性可以指定預(yù)加載的類型,除了style還支持很多類型,常用的一般是style和script,css和js。
其他的類型可以查看文檔:
preload
prefetch
prefetch和preload差不多,prefetch是一個低優(yōu)先級的獲取,通常用在這個資源可能會在用戶接下來訪問的頁面中出現(xiàn)的時候。
當(dāng)然對當(dāng)前頁面的要用preload,不要用prefetch,可以用到的一個場景是在用戶鼠標(biāo)移入a標(biāo)簽時進(jìn)行一個prefetch。
prefetch
preconnect
preconnect和dns-prefetch做的事情類似,提前進(jìn)行TCP,SSL握手,省去這一部分時間,基于HTTP1.1(keep-alive)和HTTP2(多路復(fù)用)的特性,都會在同一個TCP鏈接內(nèi)完成接下來的傳輸任務(wù)。
script加標(biāo)記
當(dāng)瀏覽器解析至script標(biāo)簽時,瀏覽器的主線程就會等待script,或者運行script,然后繼續(xù)開始構(gòu)建,在以前,如果你把script標(biāo)簽放到了文檔的最上面,那么在等待下載和運行的這段時間內(nèi)頁面就會處于白屏和無法操作的狀態(tài),并且不是并行的下載,瀏覽器會逐個下載并運行,這是一個相當(dāng)糟糕的體驗。所以都會選擇將script放在文檔底部,盡可能推后腳本的執(zhí)行時機,不過并不完全可控。
時至今日,我們可以給script標(biāo)簽增加標(biāo)記,使其異步(延遲)運行,把可控權(quán)交給開發(fā)者。
async標(biāo)記
<script src="main.js" async>
async標(biāo)記告訴瀏覽器在等待js下載期間可以去干其他事,當(dāng)js下載完成后會立即(盡快)執(zhí)行,多條js可以并行下載。
async的好處是讓多條js不會互相等待,下載期間瀏覽器會去干其他事(繼續(xù)解析HTML等),異步下載,異步執(zhí)行。
defer標(biāo)記
<script src="main.js" defer></script>
與async一樣,defer標(biāo)記告訴瀏覽器在等待js下載期間可以去干其他事,多條js可以并行下載,不過當(dāng)js下載完成之后不會立即執(zhí)行,而是會等待解析完整個HTML之后在開始執(zhí)行,而且多條defer標(biāo)記的js會按照順序執(zhí)行,
<script src="main.js" defer></script>
<script src="main2.js" defer></script>
復(fù)制代碼
即使main2.js先于main.js下載完成也會等待main.js執(zhí)行完后再執(zhí)行。
到底該用哪個標(biāo)記
兩個標(biāo)記都是為了讓script標(biāo)簽實現(xiàn)異步下載,主要的區(qū)別在于async無法保證順序且下載完就會執(zhí)行而defer則會等待整個HTML解析之后才會開始執(zhí)行,并且按照插入的順序執(zhí)行。
如果兩個script之間沒有依賴關(guān)系并且可以盡快執(zhí)行的更加適合使用async,反之如果兩個script之間有依賴關(guān)系,或者希望優(yōu)先解析HTML,則defer更加適合。
視窗外的內(nèi)容懶加載
懶加載也是一個經(jīng)常被提及的技術(shù),視窗外的內(nèi)容是不會被用戶立即看到的,這時加載過多的內(nèi)容反而拖慢了網(wǎng)站整體的渲染,我們就可以用懶加載推遲這部分內(nèi)容的加載來達(dá)到加速可訪問和可交互性的目的,等用戶即將到達(dá)視窗內(nèi)的時候再開始加載這部分內(nèi)容,通常懶加載會與loading和骨架屏等技術(shù)搭配使用。
減少無意義的回流
回流與重繪是一個老生常談的問題,當(dāng)瀏覽器大小改變/滾動,DOM增刪,元素尺寸或者位置發(fā)生改變時都會發(fā)生回流,回流意味著瀏覽器要重新計算當(dāng)前頁面的與之相關(guān)的所有元素,重新進(jìn)行整體的布局。
這是一個非常消耗性能的事情,有些情況下回流無法避免,有些情況下則可以省略無意義的回流,比如用Js將20個li更改到同樣的尺寸時避免將每個li都即時更改,應(yīng)該用class一次性更改。
圖片視頻選擇合理的尺寸
分辨率越高的圖片顯示出來越消耗性能,當(dāng)然帶來的好處是更加的清晰,但很多情況下清晰并不是一個特別重要的標(biāo)準(zhǔn),我們可以犧牲一部分清晰度來讓圖片視頻體積更小,通常PC使用1倍圖,移動端使用2倍圖就夠了,原圖可以結(jié)合懶加載等待空閑或者主動觸發(fā)時在加載,像是微信QQ等聊天時發(fā)的表情包一樣,都是點開才會加載原圖。
這往往是一個容易被忽略(可能因為感覺沒必要)提升又很大的事情,如果你的網(wǎng)站圖片很多強烈建議著手優(yōu)化。
選擇一個支持動態(tài)剪裁的云服務(wù)即可享受這份美好~。
寫代碼時可以做的事
上面從代碼寫完的角度談起,接下來從寫代碼的角度談起。
首先是打包。
Tree-shaking
Tree-shaking指的是消除沒被引用的模塊代碼,減少代碼體積大小,以提高頁面的性能,最初由rollup提出。
webpack2加入對Tree-shaking的支持,webpack4中Tree-shaking默認(rèn)開啟,Tree-shaking基于ESModule靜態(tài)編譯而成,所以如果想要生效,在寫代碼的時候注意不要用CommonJS的模塊,同時也要注意不要讓babel給編譯成CommonJS的形式。
Tree-shaking連帶的有一個sideEffects的概念,因為Js的特性使得完全靜態(tài)分析是一個很難的事情,很多代碼往往會帶有副作用,比如一下代碼:
class Handler {
handleEvent() {
console.log('You called me.')
}
}
window.addEventListener('visibilitychange', new Handler())
復(fù)制代碼
在上面的代碼中不存在任何顯式的調(diào)用handleEvent,但當(dāng)visibilitychange發(fā)生時Js會去調(diào)用handleEvent,這個類就屬于有副作用的一種,它是不能被抖掉的代碼(實際上webpack也不會對類有啥想法)。
如果你確定某個文件是這種含有副作用的文件,可以在package.json中添加sideEffects: ['class.js']讓webpack強行打包進(jìn)去。
對于一些第三方庫來說為了兼容性考慮通常入口文件都是CommonJS的形式,這時想要成功抖掉不需要的部分通常有兩種方式。
以出鏡率極高的lodash為例。
lodash默認(rèn)是CommonJS的形式,使用常規(guī)的方法import { cloneDeep } from 'lodash'導(dǎo)入后,webpack會把整個lodash打包進(jìn)來,這對于只用到了一個函數(shù)的我們的來說顯然不可接受,此時可以改寫為:
import cloneDeep from 'lodash/cloneDeep'
復(fù)制代碼
或者如果提供了ESModule的版本也可以直接使用:
import { cloneDeep } from 'lodash-es
復(fù)制代碼
前者是精準(zhǔn)導(dǎo)入不依賴re-exports,后者則是一個正經(jīng)的Tree-shaking。
壓縮
生產(chǎn)環(huán)境的代碼不是給人看的,所以不需要考慮可讀性(降低可讀性還能提高被破解的成本o(≧口≦)o),盡可能少的字符是最優(yōu)選項,webpack4+無需配置默認(rèn)會壓縮代碼,如果你想親自試試,Js可選UglifyJS,CSS可選mini-css-extract-plugin。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
optimization: {
minimizer: [
new CssMinimizerPlugin(),
],
},
};
復(fù)制代碼
使用動態(tài)import()代替靜態(tài)import做條件渲染的懶加載
又是你~,懶加載。
如果你是Vue選手,最先接觸到的import()可能是vue-router文檔中關(guān)于路由懶加載的部分,其實具體到組件內(nèi)部,也可以用同樣的方式將一些基于判斷條件的子組件/第三方庫通過import()的方式導(dǎo)入,這樣webpack在打包時會單獨將它列為一個塊,當(dāng)符合判斷條件時才會嘗試去加載這個文件。
<template>
<div>
<sub-component v-if="status" />
</div>
</tamplate>
<script>
export default {
components: {
"sub-component": () => import('./sub-component') // 感謝imluch 指正~
},
data() {
return {
status: false
}
},
mounted() {
setTimeout(() => {
this.status = true
}, 10000)
}
}
</script>
復(fù)制代碼
SSR
利用服務(wù)器端優(yōu)先渲染出某一部分重要的內(nèi)容,讓其他內(nèi)容懶加載,這樣到達(dá)瀏覽器端時一部分HTML已經(jīng)存在,頁面上就可以呈現(xiàn)出一定的內(nèi)容,這里注意服務(wù)器端渲染出來的HTML部分最好不要超過14kb,TCP慢開始的規(guī)則讓第一個TCP包的大小是14kb,這是與網(wǎng)站交互會接受到的第一個包。
更多優(yōu)化手段
上面是目前所知的優(yōu)化手段~,更多的優(yōu)化待發(fā)掘中,路過的大哥們、小姐姐們還請給個贊??,有問題的也可留言交流~??。
