網站效能優化是必須的技能,而且需要長期積累,以下是我自己總結的一些效能優化的策略,主要分為幾個方面:
- 網路請求優化
- 頁面渲染優化
- JS阻塞效能與記憶體洩漏
- 負載均衡
1 網路請求優化
1.1 瀏覽器快取
瀏覽器在向伺服器發起請求前,會先查詢本地是否有相同的檔案,如果有,就會直接拉取本地快取,這和我們在後臺部署的Redis、Memcache類似,都是起到了中間緩衝的作用,我們先看看瀏覽器處理快取的策略:
瀏覽器預設的快取是放在記憶體內的,記憶體裡的快取會因為程式的結束或者說瀏覽器的關閉而被清除,而存在硬碟裡的快取才能夠被長期保留下去。很多時候,我們在network皮膚中各請求的size項裡,會看到兩種不同的狀態:from memory cache 和 from disk cache,前者指快取來自記憶體,後者指快取來自硬碟。而控制快取存放位置的,不是別人,就是我們在伺服器上設定的Etag欄位。在瀏覽器接收到伺服器響應後,會檢測響應頭部(Header),如果有Etag欄位,那麼瀏覽器就會將本次快取寫入硬碟中。
以Nginx為例,設定Etag
etag on; //開啟etag驗證
expires 14d; //設定快取過期時間為14天
複製程式碼
開啟我們的網站,在chrome devtools的network皮膚中觀察我們的請求資源,如果在響應頭部看見Etag和Expires欄位,就說明我們的快取配置成功了。
在我們配置快取時一定要切記,瀏覽器在處理使用者請求時,如果命中強快取,瀏覽器會直接拉取本地快取,不會與伺服器發生任何通訊,也就是說,如果我們在伺服器端更新了檔案,並不會被瀏覽器得知,就無法替換失效的快取。所以我們在構建階段,需要為我們的靜態資源新增md5 hash字尾,避免資源更新而引起的前後端檔案無法同步的問題。
1.2 資源打包壓縮
我們之前所作的瀏覽器快取工作,只有在使用者第二次訪問我們的頁面才能起到效果,如果要在使用者首次開啟頁面就實現優良的效能,必須對資源進行優化。我們常將網路效能優化措施歸結為三大方面:減少請求數、減小請求資源體積、提升網路傳輸速率。現在,讓我們逐個擊破:
以Webpack為例
- 壓縮JS
new webpack.optimize.UglifyJsPlugin()
複製程式碼
- 壓縮Html
new HtmlWebpackPlugin({
template: __dirname + '/views/index.html', // new 一個這個外掛的例項,並傳入相關的引數
filename: '../index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
chunksSortMode: 'dependency'
})
複製程式碼
我們在使用html-webpack-plugin 自動化注入JS、CSS打包HTML檔案時,很少會為其新增配置項,這裡我給出樣例,大家直接複製就行。
PS:這裡有一個技巧,在我們書寫HTML元素的src 或 href 屬性時,可以省略協議部分,這樣也能簡單起到節省資源的目的。
- 壓縮CSS
在使用webpack的過程中,我們通常會以模組的形式引入css檔案(webpack的思想不就是萬物皆模組嘛),但是在上線的時候,我們還需要將這些css提取出來,並且壓縮,這些看似複雜的過程只需要簡單的幾行配置就行
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module: {
rules: [..., {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: {
loader: 'css-loader',
options: {
minimize: true
}
}
})
}]
}
複製程式碼
- 使用webpack3的新特性:ModuleConcatenationPlugin
new webpack.optimize.ModuleConcatenationPlugin()
複製程式碼
- 把prod環境的shouldUseSourceMap設定為false,去掉build生成的map檔案
devtool: shouldUseSourceMap ? 'source-map' : false,
複製程式碼
最後,我們還應該在伺服器上開啟Gzip傳輸壓縮,它能將我們的文字類檔案體積壓縮至原先的四分之一,效果立竿見影,還是切換到我們的nginx配置文件,新增如下兩項配置專案:
gzip on;
gzip_types text/plain application/javascriptapplication/x-javascripttext/css application/xml text/javascriptapplication/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
複製程式碼
如果你在網站請求的響應頭裡看到這樣的欄位,那麼就說明我們們的Gzip壓縮配置成功啦:
【!!!特別注意!!!】不要對圖片檔案進行Gzip壓縮!不要對圖片檔案進行Gzip壓縮!不要對圖片檔案進行Gzip壓縮!我只會告訴你效果適得其反,至於具體原因,還得考慮伺服器壓縮過程中的CPU佔用還有壓縮率等指標,對圖片進行壓縮不但會佔用後臺大量資源,壓縮效果其實並不可觀,可以說是“弊大於利”,所以請在gzip_types 把圖片的相關項去掉。針對圖片的相關處理,我們接下來會更加具體地介紹。
1.3 圖片資源優化
- 不要在HTML裡縮放影像
- 使用雪碧圖(CSS Sprite)
- 使用字型圖示(iconfont)
1.4 使用CDN
使用CDN存放靜態資源,避免頻寬爆炸以及加快資源下載.
2 頁面渲染效能優化
2.1 減少重繪和迴流
- CSS屬性讀寫分離:瀏覽器每次對元素樣式進行讀操作時,都必須進行一次重新渲染(迴流 + 重繪),所以我們在使用JS對元素樣式進行讀寫操作時,最好將兩者分離開,先讀後寫,避免出現兩者交叉使用的情況。最最最客觀的解決方案,就是不用JS去操作元素樣式,這也是我最推薦的。
- 通過切換class或者使用元素的style.csstext屬性去批量操作元素樣式。
- DOM元素離線更新:當對DOM進行相關操作時,例、appendChild等都可以使用Document Fragment物件進行離線操作,帶元素“組裝”完成後再一次插入頁面,或者使用display:none 對元素隱藏,在元素“消失”後進行相關操作。
- 將沒用的元素設為不可見:visibility: hidden,這樣可以減小重繪的壓力,必要的時候再將元素顯示。
- 壓縮DOM的深度,一個渲染層內不要有過深的子元素,少用DOM完成頁面樣式,多使用偽元素或者box-shadow取代。
- 圖片在渲染前指定大小:因為img元素是內聯元素,所以在載入圖片後會改變寬高,嚴重的情況會導致整個頁面重排,所以最好在渲染前就指定其大小,或者讓其脫離文件流。
- 對頁面中可能發生大量重排重繪的元素單獨觸發渲染層,使用GPU分擔CPU壓力。(這項策略需要慎用,得著重考量以犧牲GPU佔用率為代價能否換來可期的效能優化,畢竟頁面中存在太多的渲染層對於GPU而言也是一種不必要的壓力,通常情況下,我們會對動畫元素採取硬體加速。)
2.2 減少頁面重新渲染以及Dom巢狀
- 以React為例,如果會引起頁面State變化的,最好在shouldComponentUpdate進行處理,避免每次props或者state更新的時候都重新渲染,如果頁面主要用來顯示的話,可以使用PureComponent代替Component,注意PureComponent對於引用型別的變化,不會重新渲染。巧用Fragment代替Component,減少Dom巢狀。
3 JS阻塞效能與記憶體洩漏
3.1 巧用JS的防抖與節流
函式防抖:將幾次操作合併為一此操作進行。原理是維護一個計時器,規定在delay時間後觸發函式,但是在delay時間內再次觸發的話,就會取消之前的計時器而重新設定。這樣一來,只有最後一次操作能被觸發。
function debounce(fn, wait) {
var timeout = null;
return function() {
if(timeout !== null)
clearTimeout(timeout);
timeout = setTimeout(fn, wait);
}
}
複製程式碼
函式節流:使得一定時間內只觸發一次函式。原理是通過判斷是否到達一定時間來觸發函式。
var throttle = function(func, delay) {
var prev = Date.now();
return function() {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
複製程式碼
區別: 函式節流不管事件觸發有多頻繁,都會保證在規定時間內一定會執行一次真正的事件處理函式,而函式防抖只是在最後一次事件後才觸發一次函式。 比如在頁面的無限載入場景下,我們需要使用者在滾動頁面時,每隔一段時間發一次 Ajax 請求,而不是在使用者停下滾動頁面操作時才去請求資料。這樣的場景,就適合用節流技術來實現。
3.2 記憶體洩漏
- 閉包記憶體洩漏Pattern
- 在某個頁面(SPA)WillUnMount的時候,記得關閉一些資源,例如WebSocket的斷開,eChart物件置空等。
4 負載均衡
- 使用PM2管理多程式
- Nginx做反向代理
- Docker管理多個容器