8個方向做好前端性能優化

一、webpack方向

webpack優化其實可以歸爲HTTP層面的優化(網絡層面)。因爲HTTP這一層的優化兩大方向就是:減少請求次數縮短單次請求所花費的時間。而這兩個優化點的手段就是“資源的合併與壓縮”,正是我們每天用構建工具在做的事情,webpack無疑是最主流的。
webpack的優化主要是兩個主題:構建過程時間太長打包的文件體積太大
(1)構建過程時間太長這個問題,因爲不涉及產品體驗層面,所以不被重視很正常,打包慢的原因多是因爲重複打包,第三方庫(以node_modules爲代表),CommonsChunkPlugin每次構建時都會重新構建一次vendor,而平時一般從搭建工程開始這些第三方庫就基本不會動,不用每次打包時都從頭構建一遍,浪費時間。(補充:vendor.js文件是所有第三方庫打包合併的結果,比如vuecli項目最終會在dist下打包生成一個chunk-vendors.js)。推薦用DllPlugin工具,它會把第三方庫單獨打包到一個文件中,這個文件就是一個單純的依賴庫,這個依賴庫不會跟着你的業務代碼一起被重新打包,只有當依賴自身發生版本變化纔會重新打包。(其他的工具比如用happyPack將loader由單進程轉爲多進程加快速度就不多說了,補充下webpack是單線程的)
(2)打包的文件體積太大這個問題,有三個方面可以優化:按需加載、清除冗餘代碼、用Gzip壓縮。按需加載就不說了,清除冗餘代碼從webpack2開始就推出了tree-Shaking,它可以在編譯的過程中獲悉哪些模塊並沒有真正被使用,這些沒用的代碼,在最後打包的時候會被去除。比如一個js文件export出了2個函數,其中一個函數沒有在任何地方import使用過,打包時tree-shaking就會把它刪掉。到了webpack4就內置了uglifyjs-webpack-plugin對代碼做壓縮了,它會刪掉一些console語句、註釋等一些對開發者有用而對用戶無用的代碼。

二、Gzip方向

Gzip壓縮背後的原理是在一個文本文件中找出一些重複出現的字符串,臨時替換他們,來使整個文件變小,所以比較適合文本文件,不適合圖片,所以文件中代碼的重複率越高,壓縮率就越高,使用Gzip的收益就越大。通常Gzip壓縮率能達到60%~70%,尤其是上面講的第三方庫打包合併成vendor.js文件,體積就不小。Gzip壓縮可以是前端打包構建的時候做,也可以服務端實時地去壓縮,我做過的項目通常都是我前端去壓縮。壓縮/解壓就是犧牲服務器壓縮的時間和瀏覽器解壓的時間去換網絡傳輸的時間。我做過的項目一般會設置10KB這麼一個閥值,打包後大於10KB的js\css\html文件才需要壓縮。如果打包出來的都是1K、2K的文件,就沒必要壓縮了。
簡要說下Vue項目配置Gzip的方法:1、首先npm安裝compression-webpack-plugin庫,2、在vue.config.js裏配置:

const CompressionPlugin = require("compression-webpack-plugin")
module.exports = {
    publicPath: process.env.NODE_ENV === 'production'? '/canteen/': '/',
    productionSourceMap:false, //不生成.js.map文件,加速構建速度
    configureWebpack: config => { //webpack配置Gzip壓縮
        if(process.env.NODE_ENV === 'production'){
            return {
                plugins:[new CompressionPlugin({
                    test:/\.js$|\.html$|\.css$/,
                    threshold:10240, //超過10KB的才需要壓縮
                    deleteOriginalAssets:false //是否刪除原文件
                })]
            }
        }
},
}

服務端也需要配置,當支持Gzip的瀏覽器發起http請求時,會自動在請求頭帶上accept-encoding:gzip,服務端就返回壓縮後的文件,瀏覽器再解壓縮。如果請求頭沒有這個就返回原文件。
服務端配置Gzip。有動態靜態兩種。服務端動態gzip是常見的方案,即服務端判斷瀏覽器http請求頭中的Accept-Encoding是否有gzip,有的話就說明瀏覽器支持gzip,服務器就實時壓縮生成gzip返回給瀏覽器,否則就返回原文件。但是這種模式是比較消耗服務器CPU的,如果前端打包的時候就壓縮好,把原文件和gzip文件全丟到服務器上,服務器不幹壓縮的活,只區分瀏覽器是不是支持gzip,支持就返gzip文件,不支持就返原文件,那就能省去服務器動態壓縮的環節。注意:因爲Linux系統下nginx不能向磁盤寫文件,所以服務端只能實時生成。另外,如果你公司分配給你的服務器數量少,就不要用nginx動態壓縮了。
服務器nginx配置靜態gzip方法:
在nginx.config的目標應用location下配置:

gzip_static on;
gzip_http_version 1.1;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;
三、圖片優化方向

圖片的優化其實就是根據業務場景,在不同格式的圖片之間做選擇。當前廣泛使用的圖片格式有jpeg/jpg、png、svg、base64、webP,再加個雪碧圖方案。我們的目標依然還是資源壓縮減少HTTP請求
(1)jpeg/jpg:有損壓縮、體積小、加載快、不支持透明。這種格式最大特點就是有損壓縮,但是即使被稱爲有損壓縮,jpg仍然是一種高質量的壓縮方式,當它的質量問題不被我們肉眼察覺,就沒問題。JPG適合用於呈現色彩豐富的圖片,背景圖、輪播圖、Banner圖基本都會用jpg格式。淘寶京東首頁大圖都是jpg的。他也有缺陷,不支持透明是第一個、第二個是展示logo等線條感比較強的圖形時模糊感比較明顯。
(2)png:無損壓縮、質量高、體積大、支持透明。唯一的bug就是體積大,特點是支持透明。png-8和png-24,就是位數不一樣,8位的最多支持2的8次方(256)種顏色,24位的最多支持2的24次方(1600萬)種顏色,相應的體積也更大,一般場景用png-8足夠了。一般logo、顏色簡單、線條明顯的圖片用png。淘寶首頁不論logo大小,全都用的png。
(3)svg:文本文件、體積小、不失真、兼容性好。唯一bug是渲染成本比較高。svg文件的體積甚至比jpg更小,它最顯著的特點是可以無限放大而不失真,一張svg足以適配n種分辨率。svg是文本文件,可編程,可以寫到html裏和html一起下載不佔用HTTP請求。實際開發中,我們一般會把svg寫入獨立文件後再引入html:<img src="xxx.svg" alt=""> 如果設計師沒有給我們svg文件,我們也可以用阿里的iconfont在線矢量圖形庫去做。
(4)base64:文本文件、雪碧圖的補充方案。base64也一樣可以寫到html裏,不佔用HTTP請求。base64編碼後圖片體積會比原來的大1/3,如果你頁面圖標很小,又很少,又不想合成雪碧圖,就可以用base64。如果大圖也用base64轉碼,比起省掉的HTTP請求,體積膨脹反而會帶來性能開銷。所以base64適合小圖標。
(5)webP:年輕的全能型選手。硬傷是很新,兼容性差。它是谷歌專爲web開發的圖片格式,目的是加快圖片加載速度,所以只有chrome全程兼容。它支持有損壓縮、無損壓縮、支持透明、動圖等等,如果兼容性解決了,是最合適的方案。兼容的話可以交給服務端,服務器根據HTTP請求頭的accept字段來決定返回什麼格式的圖片。accept包含image/webp時就返回webp格式。

四、CDN方向

使用CDN目的是使http傳輸路徑變短。CDN (Content Delivery Network,即內容分發網絡)指的是一組分佈在各個地區的服務器。這些服務器存儲着數據的副本,服務器可以按照就近原則把距離客戶端最近的服務器上的資源返回。
瀏覽器緩存和本地存儲帶來的性能提升,是在“已經獲取到資源了再存起來,方便下一次的訪問”,只能加快二次請求的速度,而CDN可以縮短首次請求的時間。CDN服務器上一般只存靜態資源。它的核心是是緩存和回源,緩存就是部署時把靜態資源copy一份到CDN服務器上,回源就是CDN發現自己沒有這個資源(一般是靜態資源過期了),轉頭再向根服務器訪問資源。
另外,一般CDN服務器和根服務器的域名會用不一樣的,比如www.taobao.com它用的CDN服務器域名是g.alicdn.com。這樣做有一個額外的好處,原本訪問www.taobao.com帶上的Cookie,在訪問g.alicdn.com不會被帶上,節省了開銷,因爲Cookie 是緊跟域名的,同一個域名下的所有請求,都會攜帶 Cookie,換種說法就是訪問CDN獲取靜態資源時HTTP不會帶上Cookie,而獲取靜態資源時也壓根用不到Cookie。

五、服務端渲染方向

先說客戶端渲染。是把HTML,JS這些都下載下來,在瀏覽器裏跑一遍JS,根據JS的運行結果生成DOM。服務端渲染就是在服務端運行js,生成DOM,再把完整的html返回給瀏覽器。所以服務端也是實時渲染的,消耗服務器CPU,高併發是個瓶頸,所以如果不是服務器性能很強大,不會優先考慮服務端渲染,就算做,一般也只會給首屏頁面做服務端渲染。
另外,很多網站做服務端渲染的首要目的也並不是性能向的考慮,而是出於SEO優化的目的。客戶端渲染時,搜索引擎可不會去跑js,只會查找當前html靜態的關鍵字。所以,一般都是其他性能優化的招數都用盡了再考慮服務端渲染。有人說:“ssr這個東西好是好,可是流量上來的話,即便優化的再好也是非常喫硬件資源的。虛擬運維的話1核CPU的容器也就能支撐幾十甚至十幾的QPS”。

六、HTTP2方向

http1.1的並行加載資源數爲6到8個(準確的說是每個域最多隻能同時建立6個鏈接)
http2的優點在於一個域只建立一次TCP連接,使用多路複用,同時傳輸的資源數幾乎沒有上限,這樣就不用使用雪碧圖、合併JS/CSS等方法了。如果沒有正確配置,nginx會自動切換到HTTP/1.1,所以兼容性很好。
不需要開發,只需要服務端nginx配置:要求openssl版本高於1.0.2,然後修改nginx.config:listen 443 ssl 改爲listen 443 ssl http2,重啓nginx即可。

七、瀏覽器緩存(HTTP緩存)方向

HTTP緩存是指當下一次發HTTP請求資源前,判斷下是否應該從客戶端本地拿緩存。分爲強緩存協商緩存。如果命中強緩存,則客戶端不再詢問服務器而直接去客戶端本地讀取資源;如果命中協商緩存,就發HTTP詢問服務器是否應該讀本地的資源,服務器不會返回所請求的資源,而是告訴客戶端可以直接從客戶端本地加載這個資源,於是瀏覽器就又會從自己的緩存中去加載這個資源。當兩種模式都沒命中,瀏覽器直接從服務器加載資源。

所以,強緩存與協商緩存的共同點是:如果命中,都是從客戶端緩存中加載資源,而不是從服務器加載資源數據;區別是:強緩存不發請求到服務器,協商緩存會發請求到服務器。

1、強緩存怎麼做的:

先說早期的expire。早些年沒有協商緩存的概念,緩存指的也是強緩存,一直用expire。expire的值是一個時間戳——expires: Wed, 11 Sep 2019 16:12:18 GMT,是服務器響應HTTP時返回給瀏覽器的,意思就是“我返回給你的文件資源,你要緩存起來,在這個時間節點前你都去讀緩存,不要再問我要了”。所以expire有個bug,服務器返回的時間戳是服務器時間,而瀏覽器發HTTP前用來和expire比較的是本地時間,兩個時間可能不一致。

再說cache-control。cache-control就是expire的升級版了,強緩存和協商緩存都由它控制。如果和expire同時出現,cache-control優先級更高。cache-control有很多種取值:

private: 只有瀏覽器可以緩存,(默認值)
public:可以被任何緩存區緩存,包括瀏覽器和代理服務器
max-age=xxx: 緩存將在 xxx 秒後失效
s-maxage=xxx: 代理服務上的緩存將在 xxx 秒後失效
no-cache:需要使用協商緩存來驗證緩存數據
no-store: 所有內容都不會緩存,強制緩存和協商緩存都不會觸發

max-age的值就是一個時間段了(36000秒這樣),這是個相對時間,解決了expire的bug。max-age=xxx並不是獨立的屬性,如你設置了cache-control:max-age=1800,等價於cache-Control:private, max-age=1800。

s-maxage 不像 max-age 一樣爲大家所熟知。它用來表示代理服務器上的資源緩存期限,所以只對 public 緩存有效(如果寫了s-maxage,pubic可以省略不寫)。cache-control: max-age=36000,s-maxage=31536000,兩者同時出現時s-magage優先級更高。

2、協商緩存怎麼做的:

只要服務器響應的頭裏寫上cache-control:no-cache,就表示瀏覽器下一次請求相同文件時需要使用協商緩存。

先說Last-Modified / If-Modified-Since。和expire一樣last-modified是一個時間戳,瀏覽器第一次跟服務器請求資源文件時,服務器會在響應頭寫上last-modified表示文件最後修改的時間,瀏覽器要把這個文件和最後修改時間都緩存起來,下一次請求相同文件時,把這個時間寫到if-modified-since字段上,發給服務器,服務器根據瀏覽器傳過來if-modified-since和資源在服務器上的最後修改時間判斷資源是否有變化,如果沒有變化則返回304 Not Modified,但是不會返回資源內容;如果有變化,就正常返回資源文件。瀏覽器收到304後就從本地緩存加載資源。值得注意的是,如果服務器判斷資源文件沒變化時不會再往響應頭裏寫last-modified字段了。這種方式在絕大多數情況是足夠可靠的,但一般文件系統能感知到文件修改的最小粒度是s,所以一些極限情況下,文件內容修改了修改時間不一定變。所以產生了ETag方式。

再說ETag / If-None-Match。ETag的值是根據資源文件內容生成的唯一標識字符串,比如:ETag: W/"2a3b-1602480f459",只要文件內容變化唯一標識就不一樣。它的工作流程和“最後修改時間”基本一樣:第一次請求資源文件時,服務器計算好ETag值並寫在響應頭,瀏覽器收到後把資源文件和ETag值緩存起來,下一次請求相同文件時,把這個ETag寫到if-none-match字段上,發給服務器,服務器計算文件當前的ETag,比較瀏覽器傳來的if-none-match。我們看到,Etag的計算是實時的,會影響服務端的性能,而且文件越大,開銷越大。因此這個方案要謹慎使用,有的公司甚至直接不讓用ETag方法。

Etag可以是ETag: W/"2a3b-1602480f459",也可以是ETag:"2a3b-1602480f459",以W/開頭的表示weak tag(弱Etag,不帶W/的就是強Etag)。

最後總結下協商緩存。大多數情況下,可以關掉ETag而只用Last-Modified。ETag是特殊情況下Last-Modified的補充,而不是替代。兩者都存在的情況下ETag優先級比Last-Modified高。不管哪個方法,“最後修改時間”和“唯一標識”都由瀏覽器來存,服務器是不可能花費空間來保存緩存狀態的。

3、瀏覽器把資源文件緩存在什麼地方?

可以看到,瀏覽器會從3個地方讀緩存:

from memory cache:從內存讀緩存,最快也最短命,頁面關閉,文件就沒了。

from ServiceWorker:從瀏覽器的service worker讀取緩存,service worker是一個瀏覽器中的進程而不是瀏覽器內核下的線程,因此它在被註冊安裝之後,能夠被在多個頁面中使用,也不會因爲頁面的關閉而被銷燬。所以瀏覽器關了文件才銷燬。

from disk cache:從瀏覽器客戶端本地磁盤讀緩存,瀏覽器關閉,磁盤文件還在。

至於緩存到哪裏這個事不是標準規範,純屬瀏覽器個人行爲。各家瀏覽器的策略不盡相同,比如chrome會把體積大的js、css放到磁盤,小的放內存;而火狐瀏覽器就只會存在內存裏,沒有from disk cache,不會放到磁盤。

八、從瀏覽器渲染頁面機制的方向

我們一般說的瀏覽器內核可以分成2部分:JS引擎渲染引擎。JS引擎就是專門解釋js代碼的;渲染引擎又包含了:HTML解釋器、CSS解釋器、佈局、網絡、存儲、圖形、音視頻、圖片解碼器等等零部件。整體來看瀏覽器的一次渲染過程就是:調用瀏覽器各個零部件把網頁資源文件從服務器上下載下來,轉換爲圖像的過程。渲染過程對我們前端開發來說是個黑盒,但瞭解了其中的機制可以幫助我們避免一些問題。
再詳細點的渲染過程是:
(1)HTML文件下載完以後,HTML解析器就開始工作了,它自頂向下地解析HTML元素,最終生成DOM樹;
(2)當HTML解析到link標籤或者style標籤之後,CSS解析器就開始工作了,開始構建CSSOM樹;
(3)DOM樹和CSSOM樹都構建完成後,兩者結合生成了Render Tree——渲染樹,其實就是帶有樣式規則的DOM樹;
(4)然後佈局引擎開始工作,遞歸地爲渲染樹的每個元素計算它的尺寸、位置,給出它們出現在屏幕上的精確座標;
(5)最後圖形引擎(和GPU有關)開始工作,遍歷渲染樹,把每個節點繪製出來。

可以看到從創建渲染樹開始就是徹底的黑盒了,我們能優化的地方不多。另外可以看到,剛纔我說的這個流程裏是沒有JS參與的,因爲JS的本質是修改DOM,沒有它渲染照樣是完整的,這裏先不考慮,後面再說。這個流程裏很重要的一點是HTML解析和CSS解析是並行的,DOM樹和CSSOM樹都生成完畢了才能進行下一步——構建渲染樹。所以如果DOM樹已經生成了,但CSSOM樹還沒生成,我們就說CSS阻塞了,這個時候頁面是空白的,即便已經有DOM樹了,只要CSSOM不ok,渲染就沒法進行下去(目的是避免HTML文本裸奔在用戶眼前)。所以,業內的一個共識是:把CSS放在HTML儘量靠前,儘早解析到link或style標籤,讓CSS解析器工作起來。
ok,我們一方面要讓CSS解析器儘早工作起來,還要讓它工作快起來。我們能做的就是減少CSS選擇器複雜度。提升的方案主要是:
(1)後代選擇器的開銷是最高的,深度不要超過3層,儘可能使用id和類來關聯;
(2)少用標籤選擇器,用類選擇器替代;
(3)那些可以繼承的屬性,避免重複匹配重複定義;
(4)避免使用通配符,只對需要用到的元素進行選擇。

另外非常重要一點的是,css選擇器是從右到左解析的,#myList li{} 像這種寫法,看上去是先找到了id爲myList的元素,縮短了時間,但它實際是先找到所有li標籤,再找到哪個是包含在myList下的。所以這種情況直接用類選擇器最合適了。
最後來說有JS參與進來的情況。JS的問題是阻塞。JS引擎雖然獨立於渲染引擎(也就是不同線程),按理說,他們也可以並行工作,但是JS代碼裏充滿了DOM的操作、CSS的操作,所以爲了保證JS執行的時候有確定的DOM和CSS,JS執行的時候DOM和CSSOM的構建都會暫停。也就是當HTML解析到script標籤時,瀏覽器會暫停渲染過程,將控制權交給JS引擎,JS引擎執行script標籤內的代碼(外部的JS還要先下載再執行),執行完了再把控制權交給渲染引擎,繼續DOM和CSSOM的構建。所以這種情況你把script標籤寫在html最後也無濟於事,還是會阻塞。瀏覽器之所以會一股腦的暫停DOM樹構建,是因爲瀏覽器不知道這塊js代碼有沒有操作DOM, 但是我們開發是知道的,如果js代碼沒有操作DOM和CSS,那就完全可以並行執行,所以HTML也提供了async和defer屬性,來避免沒必要的阻塞。
默認模式(阻塞下載,下載完立刻執行)
async模式(異步下載,下載完立刻執行):<script async src="xxx.js"></script> HTML解析器解析到這兒時會異步的下載xxx.js文件,但是當下載結束時,js會立刻執行,這個時候渲染引擎也會被暫停。
defer模式(異步下載,下載完延遲執行):<script defer src="xxx.js"></script> 和async一樣,xxx.js也是異步下載,但是它會等到DOM樹和CSSOM樹都構建完成後再執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章