我們都知道對(duì)于Web應(yīng)用來(lái)說(shuō)性能很重要片酝。然而性能優(yōu)化相關(guān)的知識(shí)卻非常的龐大并且雜亂。對(duì)于性能優(yōu)化需要做些什么以及性能瓶頸是什么挖腰,通常我們并不清楚(不包括那些對(duì)性能優(yōu)化有豐富經(jīng)驗(yàn)的高手)雕沿。
事實(shí)上關(guān)于Web性能有很多可以優(yōu)化的點(diǎn),其中涉及到的知識(shí)大致可以劃分為幾類:度量標(biāo)準(zhǔn)猴仑、編碼優(yōu)化晦炊、靜態(tài)資源優(yōu)化、交付優(yōu)化宁脊、構(gòu)建優(yōu)化断国、性能監(jiān)控。
本文主要介紹性能優(yōu)化需要做的事以及需要考慮的問(wèn)題榆苞。目的在于給讀者腦海中生成一個(gè)宏觀的地圖稳衬。
不會(huì)介紹每個(gè)優(yōu)化項(xiàng)目具體如何操作。PS:后續(xù)會(huì)有系列文章針對(duì)不同優(yōu)化分類下的具體優(yōu)化操作進(jìn)行更詳細(xì)的介紹坐漏。
web前端全棧資料粉絲福利(面試題薄疚、視頻、資料筆記赊琳、進(jìn)階路線)
1. 度量標(biāo)準(zhǔn)與設(shè)定目標(biāo)
在進(jìn)行性能優(yōu)化之前街夭,我們需要為應(yīng)用選擇一個(gè)正確的度量標(biāo)準(zhǔn)(性能指標(biāo))以及設(shè)定一個(gè)合理的優(yōu)化目標(biāo)。
并不是所有指標(biāo)都同樣重要躏筏,這取決于你的應(yīng)用板丽。最后根據(jù)度量標(biāo)準(zhǔn)設(shè)定一個(gè)現(xiàn)實(shí)的目標(biāo)。
1.1 度量標(biāo)準(zhǔn)
下面是一些值得考慮的指標(biāo):
首次有效繪制(First Meaningful Paint,簡(jiǎn)稱FMP埃碱,當(dāng)主要內(nèi)容呈現(xiàn)在頁(yè)面上)
英雄渲染時(shí)間(Hero Rendering Times猖辫,度量用戶體驗(yàn)的新指標(biāo),當(dāng)用戶最關(guān)心的內(nèi)容渲染完成)
可交互時(shí)間(Time to Interactive砚殿,簡(jiǎn)稱TTI啃憎,指頁(yè)面布局已經(jīng)穩(wěn)定,關(guān)鍵的頁(yè)面字體是可見(jiàn)的似炎,并且主進(jìn)程可用于處理用戶輸入辛萍,基本上用戶可以點(diǎn)擊UI并與其交互)
輸入響應(yīng)(Input responsiveness,界面響應(yīng)用戶輸入所需的時(shí)間)
感知速度指數(shù)(Perceptual Speed Index羡藐,簡(jiǎn)稱PSI贩毕,測(cè)量頁(yè)面在加載過(guò)程中視覺(jué)上的變化速度,分?jǐn)?shù)越低越好)
自定義指標(biāo)传睹,由業(yè)務(wù)需求和用戶體驗(yàn)來(lái)決定耳幢。
FMP與英雄渲染時(shí)間非常相似岸晦,但它們不一樣的地方在于FMP不區(qū)分內(nèi)容是否有用欧啤,不區(qū)分渲染出的內(nèi)容是否是用戶關(guān)心的。
1.2 設(shè)定目標(biāo)
100毫秒的界面響應(yīng)時(shí)間與60FPS
速度指標(biāo)(Speed Index)小于1250ms
3G網(wǎng)絡(luò)環(huán)境下可交互時(shí)間小于5s
重要文件的大小預(yù)算小于170kb
以上四種指標(biāo)的設(shè)定都有據(jù)可循启上。詳細(xì)信息請(qǐng)查看RAIL性能模型邢隧。
2. 編碼優(yōu)化
編碼優(yōu)化涉及到應(yīng)用的運(yùn)行時(shí)性能,本小節(jié)介紹幾個(gè)可以提升程序運(yùn)行時(shí)性能的建議冈在。
2.1 數(shù)據(jù)讀取速度
事實(shí)上數(shù)據(jù)訪問(wèn)速度有快慢之分倒慧,下面列出幾個(gè)影響數(shù)據(jù)訪問(wèn)速度的因素:
字面量與局部變量的訪問(wèn)速度最快,數(shù)組元素和對(duì)象成員相對(duì)較慢
變量從局部作用域到全局作用域的搜索過(guò)程越長(zhǎng)速度越慢
對(duì)象嵌套的越深包券,讀取速度就越慢
對(duì)象在原型鏈中存在的位置越深纫谅,找到它的速度就越慢
推薦的做法是緩存對(duì)象成員值。將對(duì)象成員值緩存到局部變量中會(huì)加快訪問(wèn)速度
2.2 DOM
應(yīng)用在運(yùn)行時(shí)溅固,性能的瓶頸主要在于DOM操作的代價(jià)非常昂貴付秕,下面列出一些關(guān)于DOM操作相關(guān)提升性能的建議:
在JS中對(duì)DOM進(jìn)行訪問(wèn)的代價(jià)非常高。請(qǐng)盡可能減少訪問(wèn)DOM的次數(shù)(建議緩存DOM屬性和元素侍郭、把DOM集合的長(zhǎng)度緩存到變量中并在迭代中使用叹坦。讀變量比讀DOM的速度要快很多溜歪。)
重排與重繪的代價(jià)非常昂貴。如果操作需要進(jìn)行多次重排與重繪,建議先讓元素脫離文檔流齿椅,處理完畢后再讓元素回歸文檔流,這樣瀏覽器只會(huì)進(jìn)行兩次重排與重繪(脫離時(shí)和回歸時(shí))曼验。
善于使用事件委托
2.3 流程控制
下面列出一些流程控制相關(guān)的一些可以略微提升性能的細(xì)節(jié)卡骂,這些細(xì)節(jié)在大型開(kāi)源項(xiàng)目中大量運(yùn)用(例如Vue):
避免使用for...in(它能枚舉到原型,所以很慢)
在JS中倒序循環(huán)會(huì)略微提升性能
減少迭代的次數(shù)
基于循環(huán)的迭代比基于函數(shù)的迭代快8倍
用Map表代替大量的if-else和switch會(huì)提升性能
3. 靜態(tài)資源優(yōu)化
Web應(yīng)用的運(yùn)行離不開(kāi)靜態(tài)資源煮甥,所以對(duì)靜態(tài)資源的優(yōu)化至關(guān)重要毛好。
3.1 使用Brotli或Zopfli進(jìn)行純文本壓縮
在最高級(jí)別的壓縮下Brotli會(huì)非常慢(但較慢的壓縮最終會(huì)得到更高的壓縮率)以至于服務(wù)器在等待動(dòng)態(tài)資源壓縮的時(shí)間會(huì)抵消掉高壓縮率帶來(lái)的好處望艺,但它非常適合靜態(tài)文件壓縮,因?yàn)樗慕鈮核俣群芸臁?/p>
使用Zopfli壓縮可以比Zlib的最大壓縮提升3%至8%肌访。
3.2 圖片優(yōu)化
盡可能通過(guò)srcset找默,sizes和元素使用響應(yīng)式圖片。還可以通過(guò)元素使用WebP格式的圖像吼驶。
響應(yīng)式圖片可能大家未必聽(tīng)說(shuō)過(guò)惩激,但響應(yīng)式布局大家肯定都聽(tīng)說(shuō)過(guò)。響應(yīng)式圖片與響應(yīng)式布局類似蟹演,它可以在不同屏幕尺寸與分辨率的設(shè)備上都能良好工作(比如自動(dòng)切換圖片大小风钻、自動(dòng)裁切圖片等)。
當(dāng)然酒请,如果您不滿足這種尺度的優(yōu)化骡技,還可以對(duì)圖片進(jìn)行更深層次的優(yōu)化。例如:模糊圖片中不重要的部分以減小文件大小羞反、使用自動(dòng)播放與循環(huán)的HTML5視頻替換GIF圖布朦,因?yàn)橐曨l比GIF文件還小(好消息是未來(lái)可以通過(guò)img標(biāo)簽加載視頻)昼窗。
4. 交付優(yōu)化
交付優(yōu)化指的是對(duì)頁(yè)面加載資源以及用戶與網(wǎng)頁(yè)之間的交付過(guò)程進(jìn)行優(yōu)化是趴。
4.1 異步無(wú)阻塞加載JS
JS的加載與執(zhí)行會(huì)阻塞頁(yè)面渲染,可以將Script標(biāo)簽放到頁(yè)面的最底部澄惊。但是更好的做法是異步無(wú)阻塞加載JS唆途。有多種無(wú)阻塞加載JS的方法:defer、async掸驱、動(dòng)態(tài)創(chuàng)建script標(biāo)簽肛搬、使用XHR異步請(qǐng)求JS代碼并注入到頁(yè)面。
但更推薦的做法是使用defer或async毕贼。如果使用defer或async請(qǐng)將Script標(biāo)簽放到head標(biāo)簽中温赔,以便讓瀏覽器更早地發(fā)現(xiàn)資源并在后臺(tái)線程中解析并開(kāi)始加載JS。
4.2 使用Intersection Observer實(shí)現(xiàn)懶加載
懶加載是一個(gè)比較常用的性能優(yōu)化手段帅刀,下面列出了一些常用的做法:
可以通過(guò)Intersection Observer延遲加載圖片让腹、視頻、廣告腳本扣溺、或任何其他資源骇窍。
可以先加載低質(zhì)量或模糊的圖片,當(dāng)圖片加載完畢后再使用完整版圖片替換它锥余。
延遲加載所有體積較大的組件腹纳、字體、JS、視頻或Iframe是一個(gè)好主意
4.3 優(yōu)先加載關(guān)鍵的CSS
CSS資源的加載對(duì)瀏覽器渲染的影響很大嘲恍,默認(rèn)情況下瀏覽器只有在完成<head>標(biāo)簽中CSS的加載與解析之后才會(huì)渲染頁(yè)面足画。如果CSS文件過(guò)大,用戶就需要等待很長(zhǎng)的時(shí)間才能看到渲染結(jié)果佃牛。針對(duì)這種情況可以將首屏渲染必須用到的CSS提取出來(lái)內(nèi)嵌到<head>中淹辞,然后再將剩余部分的CSS用異步的方式加載》溃可以通過(guò)Critical做到這一點(diǎn)象缀。
4.4 資源提示(Resource Hints)
Resource Hints(資源提示)定義了HTML中的Link元素與dns-prefetch、preconnect爷速、prefetch與prerender之間的關(guān)系央星。它可以幫助瀏覽器決定應(yīng)該連接到哪些源,以及應(yīng)該獲取與預(yù)處理哪些資源來(lái)提升頁(yè)面性能惫东。
4.4.1 dns-prefetch
dns-prefetch可以指定一個(gè)用于獲取資源所需的源(origin)莉给,并提示瀏覽器應(yīng)該盡可能早的解析。
<link rel="dns-prefetch" >
4.4.2 preconnect
preconnect用于啟動(dòng)預(yù)鏈接廉沮,其中包含DNS查找颓遏,TCP握手,以及可選的TLS協(xié)議废封,允許瀏覽器減少潛在的建立連接的開(kāi)銷州泊。
4.4.3 prefetch
Prefetch用于標(biāo)識(shí)下一個(gè)導(dǎo)航可能需要的資源丧蘸。瀏覽器會(huì)獲取該資源漂洋,一旦將來(lái)請(qǐng)求該資源,瀏覽器可以提供更快的響應(yīng)力喷。
瀏覽器不會(huì)預(yù)處理刽漂、不會(huì)自動(dòng)執(zhí)行、不會(huì)將其應(yīng)用于當(dāng)前上下文弟孟。
as與crossorigin選項(xiàng)都是可選的贝咙。
4.4.4 prerender
prerender用于標(biāo)識(shí)下一個(gè)導(dǎo)航可能需要的資源。瀏覽器會(huì)獲取并執(zhí)行拂募,一旦將來(lái)請(qǐng)求該資源庭猩,瀏覽器可以提供更快的響應(yīng)。
<link rel="prerender" >
瀏覽器將預(yù)加載目標(biāo)頁(yè)面相關(guān)的資源并執(zhí)行來(lái)預(yù)處理HTML響應(yīng)陈症。
4.5 Preload
通過(guò)一個(gè)現(xiàn)有元素(例如:img蔼水,script,link)聲明資源會(huì)將獲取與執(zhí)行耦合在一起录肯。然而應(yīng)用可能只是想要先獲取資源趴腋,當(dāng)滿足某些條件時(shí)再執(zhí)行資源。
Preload提供了預(yù)獲取資源的能力,可以將獲取資源的行為從資源執(zhí)行中分離出來(lái)优炬。因此颁井,Preload可以構(gòu)建自定義的資源加載與執(zhí)行。
例如蠢护,應(yīng)用可以使用Preload進(jìn)行CSS資源的預(yù)加載雅宾、并且同時(shí)具備:高優(yōu)先級(jí)、不阻塞渲染等特性葵硕。然后應(yīng)用程序在合適的時(shí)間使用CSS資源:
var res = document.createElement("link");
res.rel = "preload";
res.as = "style";
res.href = "styles/other.css";
document.head.appendChild(res);
Link: ; rel=preload; as=style
4.6 快速響應(yīng)的用戶界面
PSI(Perceptual Speed Index秀又,感知速度指數(shù))是提升用戶體驗(yàn)的重要指標(biāo),讓用戶感覺(jué)到頁(yè)面的反饋比沒(méi)有反饋體驗(yàn)要好很多贬芥。
可以嘗試使用骨架屏或添加一些Loading過(guò)渡動(dòng)畫(huà)提示用戶體驗(yàn)吐辙。
輸入響應(yīng)(Input responsiveness)指標(biāo)同樣重要,甚至更重要蘸劈。試想昏苏,用戶點(diǎn)擊了網(wǎng)頁(yè)后缺毫無(wú)反應(yīng)會(huì)是什么心情。JS的單線程大家已經(jīng)不能再熟悉威沫,這意味著當(dāng)JS在運(yùn)行時(shí)用戶界面處于“鎖定”狀態(tài)贤惯,所以JS同步執(zhí)行的時(shí)間越長(zhǎng),用戶等待響應(yīng)的時(shí)間也就越長(zhǎng)棒掠。
據(jù)調(diào)查孵构,JS執(zhí)行100毫秒以上用戶就會(huì)明顯覺(jué)得網(wǎng)頁(yè)變卡了。所以要嚴(yán)格限制每個(gè)JS任務(wù)執(zhí)行時(shí)間不能超過(guò)100毫秒烟很。
解決方案是可以將一個(gè)大任務(wù)拆分成多個(gè)小任務(wù)分布在不同的Macrotask中執(zhí)行(通俗的說(shuō)是將大的JS任務(wù)拆分成多個(gè)小任務(wù)異步執(zhí)行)颈墅。或者使用WebWorkers雾袱,它可以在UI線程外執(zhí)行JS代碼運(yùn)算恤筛,不會(huì)阻塞UI線程,所以不會(huì)影響用戶體驗(yàn)芹橡。
應(yīng)用越復(fù)雜毒坛,主動(dòng)管理UI線程就越重要
5. 構(gòu)建優(yōu)化
現(xiàn)代前端應(yīng)用都需要有構(gòu)建的過(guò)程,項(xiàng)目在構(gòu)建過(guò)程中是否進(jìn)行了合理的優(yōu)化林说,會(huì)對(duì)Web應(yīng)用的性能有著巨大的影響煎殷。例如:影響構(gòu)建后文件的體積、代碼執(zhí)行效率腿箩、文件加載時(shí)間豪直、首次有效繪制指標(biāo)等。
5.1 使用預(yù)編譯
拿Vue舉例度秘,如果您使用單文件組件開(kāi)發(fā)項(xiàng)目顶伞,組件會(huì)在編譯階段將模板編譯為渲染函數(shù)饵撑。最終代碼被執(zhí)行時(shí)可以直接執(zhí)行渲染函數(shù)進(jìn)行渲染。而如果您沒(méi)有使用單文件組件預(yù)編譯代碼唆貌,而是在網(wǎng)頁(yè)中引入vue.min.js滑潘,那么應(yīng)用在運(yùn)行時(shí)需要先將模板編譯成渲染函數(shù),然后再執(zhí)行渲染函數(shù)進(jìn)行渲染锨咙。相比預(yù)編譯语卤,多了模板編譯的步驟,所以會(huì)浪費(fèi)很多性能酪刀。
5.2 使用 Tree-shaking粹舵、Scope hoisting、Code-splitting
Tree-shaking是一種在構(gòu)建過(guò)程中清除無(wú)用代碼的技術(shù)骂倘。使用Tree-shaking可以減少構(gòu)建后文件的體積眼滤。
目前Webpack與Rollup都支持Scope Hoisting。它們可以檢查import鏈历涝,并盡可能的將散亂的模塊放到一個(gè)函數(shù)中诅需,前提是不能造成代碼冗余。所以只有被引用了一次的模塊才會(huì)被合并荧库。使用Scope Hoisting可以讓代碼體積更小并且可以降低代碼在運(yùn)行時(shí)的內(nèi)存開(kāi)銷堰塌,同時(shí)它的運(yùn)行速度更快。前面2.1節(jié)介紹了變量從局部作用域到全局作用域的搜索過(guò)程越長(zhǎng)執(zhí)行速度越慢分衫,Scope Hoisting可以減少搜索時(shí)間场刑。
code-splitting是Webpack中最引人注目的特性之一。此特性能夠把代碼分離到不同的bundle中蚪战,然后可以按需加載或并行加載這些文件牵现。code-splitting可以用于獲取更小的bundle,以及控制資源加載優(yōu)先級(jí)屎勘,如果使用合理施籍,會(huì)極大影響加載時(shí)間居扒。
5.3 服務(wù)端渲染(SSR)
單頁(yè)應(yīng)用需要等JS加載完畢后在前端渲染頁(yè)面概漱,也就是說(shuō)在JS加載完畢并開(kāi)始執(zhí)行渲染操作前的這段時(shí)間里瀏覽器會(huì)產(chǎn)生白屏。
服務(wù)端渲染(Server Side Render喜喂,簡(jiǎn)稱SSR)的意義在于彌補(bǔ)主要內(nèi)容在前端渲染的成本瓤摧,減少白屏?xí)r間,提升首次有效繪制的速度玉吁≌彰郑可以使用服務(wù)端渲染來(lái)獲得更快的首次有效繪制。
比較推薦的做法是:使用服務(wù)端渲染靜態(tài)HTML來(lái)獲得更快的首次有效繪制进副,一旦JavaScript加載完畢再將頁(yè)面接管下來(lái)这揣。
5.4 使用import函數(shù)動(dòng)態(tài)導(dǎo)入模塊
使用import函數(shù)可以在運(yùn)行時(shí)動(dòng)態(tài)地加載ES2015模塊悔常,從而實(shí)現(xiàn)按需加載的需求。
這種優(yōu)化在單頁(yè)應(yīng)用中變得尤為重要给赞,在切換路由的時(shí)候動(dòng)態(tài)導(dǎo)入當(dāng)前路由所需的模塊机打,會(huì)避免加載冗余的模塊(試想如果在首次加載頁(yè)面時(shí)一次性把整個(gè)站點(diǎn)所需要的所有模塊都同時(shí)加載下來(lái)會(huì)加載多少非必須的JS,應(yīng)該盡可能的讓加載的JS更小片迅,只在首屏加載需要的JS)残邀。
使用靜態(tài)import導(dǎo)入初始依賴模塊。其他情況下使用動(dòng)態(tài)import按需加載依賴
5.5 使用HTTP緩存頭
正確設(shè)置expires柑蛇,cache-control和其他HTTP緩存頭芥挣。
推薦使用Cache-control: immutable避免重新驗(yàn)證。
6. 其他
其他一些值得考慮的優(yōu)化點(diǎn):
HTTP2
使用最高級(jí)的CDN(付費(fèi)的比免費(fèi)的強(qiáng)的多)
優(yōu)化字體
其他垂直領(lǐng)域的性能優(yōu)化
7. 性能監(jiān)控
最后耻台,你可能需要一個(gè)性能檢測(cè)工具來(lái)持續(xù)監(jiān)視網(wǎng)站的性能空免。
8. 總結(jié)
最后用一張圖來(lái)總結(jié)這篇文章所表達(dá)的內(nèi)容,感謝@anjia幫忙畫(huà)的這張圖盆耽。