愛康體檢寶 PC(www.tijianbao.com/) 算是一個「老」項目,爲何說「老」呢,由於在前端技術突飛猛進,天天都有新知識、新概念,甚至新框架的今天,它仍是基於vue-cli 2.x、webpack 3.x構建,顯然有些老了。其次,在早期開始這個項目的時候,因爲倉促上線,也沒有過多的考慮性能及加載問題,目前網站上使用的圖片未通過裁切,全部的庫都打包到一個 vendor 裏,首屏加載時間太長,等等這些問題使網站的用戶體驗不是太好,基於各方面的緣由,決定對它進行一次優化,主要從如下幾個方面進行:javascript
下面分別展開來講php
https 主要帶來安全性方面的提高,並且 http/2 依賴於 https,只有使用 https 協議的站點能夠升級 http/2 協議。css
http/2 帶來了一系列的改動和優化,主要以下:html
這裏有一篇來自 google 的 HTTP/2 簡介 更爲全面和權威。前端
配置主要是在編譯 nginx 時加上 with-http_ssl_module 模塊和 with-http_v2_module模塊vue
./configure --with-http_v2_module --with-http_ssl_module
複製代碼
配置服務器 conf 文件java
server {
listen 443 ssl http2 default_server;
ssl_certificate server.crt;
ssl_certificate_key server.key;
...
}
複製代碼
而後重啓服務器,完成升級~webpack
緩存對於 web 應用程序相當重要,合理控制緩存能夠有效提高 web 性能,咱們以前有些域下未作明確的緩存管理,雖然瀏覽器有默認的緩存機制,可是因爲默認的機制未必能知足咱們的要求,並且各瀏覽器的默認機制不一樣,可能形成 web 程序的表現也不一樣,因此頗有必要對各資源的緩存進行精細控制。nginx
關於瀏覽器緩存,我寫過 一篇文章 作了詳細介紹,這裏只說具體的實施細節:web
具體 nginx 配置以下:
# 配置 gzip
gzip on;
gzip_min_length 0k;
gzip_comp_level 1;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
# 開啓 etag
etag on;
# 不緩存接口
location ~* \.(?:manifest|appcache|xml|json)$ {
add_header Cache-Control "no-cache";
}
# 設置 html 過時時間爲 80s
location ~* \.(?:html)$ {
add_header Cache-Control "max-age=80";
}
# 給靜態資源設置一個長期緩存
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
expires 1M;
access_log off;
add_header Cache-Control "public";
}
# CSS and Javascript
location ~* \.(?:css|js)$ {
expires 1y;
access_log off;
add_header Cache-Control "public";
}
複製代碼
由於 html 類型的頁面文件可能實時發版,不能設置較長時間的緩存,不然可能形成發版後不更新的現象。
靜態資源都設置成較長緩存時間,因爲發版後,若是靜態資源有更新,都會產生新的 hash 值,從而使老的資源過時。
api 請求因爲要實現獲請最新內容,因此不設置瀏覽器緩存,指望的結果是能經過 etag 優化傳輸,但在實踐中經過 nginx 設置 api 無效,具體過程還在探索中。
經過以上方式,對 web 程序的緩存進行了較精細的控制。
網站自己使用的圖片有兩個來源,分別是 又拍雲 和 咱們本身的 idc(經過阿里雲加速),在使用的過程當中都沒有裁切,致使總體加載的圖片資源比較大,作了以下優化:
又拍雲提供了動態裁切的功能,能夠特別方便的控制圖片資源。
例如對於某個圖片資源,咱們須要的是一個寬爲 200px 的圖片,之前咱們是直接引用這個資源:domain.url/images/001.jpg ,這是一個原始資源,多是一個很大的圖片,直接引用會形成浪費
經過自動裁切,咱們能夠引用指定寬度的圖片:domain.url/images/001.jpg!/fw/200 ,經過在 url 後加 !/fw/200,能夠引用寬度爲 200px 的圖片,最大限度上節省資源
對於支持 webp 的瀏覽器,還可讓其輸出 webp 版本:domain.url/images/001.jpg!/fw/200/format/webp ,經過關鍵字 /format/webp 指定輸出的資源格式爲 webp
總的來講,又拍雲提供方便靈活的資源控制方法,更多細節見官方文檔:help.upyun.com/knowledge-b…
來自 idc 的圖片相對較難處理,因爲沒有云存儲提供的功能,idc 只是單純的作爲文件存儲服務器,因此沒有辦法動態裁切。
不過天無絕人之路,能夠經過 nginx 的一個模塊來實現相似的功能,這個模塊就是:ngx_http_image_filter_module,經過這個模塊能夠對指定的資源按條件進行裁切,當 CDN 回源的時候給他裁切好的圖片就能夠了,部分實現了雲存儲的功能,具體實施以下:
1、編譯 nginx 時加上 --with-http_image_filter_module
2、配置 nginx
location ~* /images/(.+)$ {
set $width -; #圖片寬度默認值
set $height -; #圖片高度默認值
if ($arg_width != "") {
set $width $arg_width;
}
if ($arg_height != "") {
set $height $arg_height;
}
#image_filter_jpeg_quality 85;
image_filter resize $width $height; #設置圖片寬高
image_filter_buffer 10M; #設置Nginx讀取圖片的最大buffer。
image_filter_interlace on; #是否開啓圖片圖像隔行掃描
if ($arg_info = "yes") {
# image_filter size;
}
error_page 415 = 415.png; #圖片處理錯誤提示圖,例如縮放參數不是數字
}
複製代碼
經過以上配置,當咱們想訪問某個資源時可能經過:domain.url/images/002.jpg?width=200 獲得寬爲 200px 的圖片。
因爲大規模部署,運維須要作更詳細的測試,因此當此次優化上線時,這個功能尚未上線。當測試完成後,運維就會將這個功能部署到線上。
這個模塊的官方文檔是:nginx.org/en/docs/htt…
使用圖片懶加載 主要依賴 Vue-Lazyload 這個 npm 模塊,具體使用方法見:www.npmjs.com/package/vue…
這裏主要說一下其中的兩個功能 progressive 和 webp:
Vue.use(VueLazyload,{
observer: true,
attempt: 10,
filter: {
progressive (listener, options) {
const is_upyun_CDN = /upyunimages\./
if (is_upyun_CDN.test(listener.src)) {
listener.el.setAttribute('lazy-progressive', 'true')
listener.loading = listener.src.replace(/fw.+/, 'fw/10')
}
},
webp (listener, options) {
if (!options.supportWebp) return
const is_upyun_CDN = /upyunimages\./
if (is_upyun_CDN.test(listener.src)) {
listener.src += '/format/webp'
}
}
}
})
複製代碼
經過 filter 總體對全部懶加載資源進行過濾控制:
progressive:容許在加載大圖見,先加載一個小圖,會有一個很好的用戶體驗
webp: 對支持 webp 的瀏覽器,加載資源的 webp 版本,有效下降文件大小
經過這一系列操做,能夠更進一種下降沒必要要的資源加載量。
按說 webpack 3 用的好好的,爲何要升級到 4 呢,緣由仍是由於新版本給咱們帶來了諸多好片,並且目前已是穩定版本,主要有如下內容:
升級過程當中可能會遇到種種問題,好在有個 官方升級指南 能夠幫咱們覆蓋掉一部分,但這會指南過於簡明扼要,具體到項目中還會有不少坑,好在經過錯誤提示結合偉大的google,最終都能找到答案(若是你用 baidu ,頗有可能最終爬不上來 :< ...),也能夠結合網上其餘一些升級方面的文章,都會頗有幫助,我這裏就不細述了。
總在來講升級 wp 4 會花一些時間,但帶來的效率和性能提高絕對值得。
對於 web 單頁面應用而言,一個很是大的痛點就是在首次加載時加載的資源量過大,致使用戶在第一次訪問時出如今白屏時間較長,如何優化這個體驗是總體優化中的重中之重,因此放在最後來講。
前面已經說了,經過減小圖片大小、升級 webpack 四、優化公共資源包等等手段,都是爲了這個服務(固然了,也不全是:>),這些手段都是對資源進行操做。當對這全部的資源進行了操做,如何合理處理這些資源,就到了瀏覽器的渲染機制,如何經過優化渲染過程,提升首屏渲染速度,是咱們下一步要考慮的。
瀏覽器渲染頁面的過程,主要分爲五步(略過請求部分,只討論請求到資源後瀏覽器如何處理):
從上面可知,瀏覽器只要加載到 html 結構和 css,就能夠渲染出頁面。
針對上面獲得的結構,有3種可選方案來實現:
先揭曉答案,最終我選擇了第 3 種,至於爲何選擇,接下來挨個來看
什麼是服務端渲染?來自 官網 的解釋是:將組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後將這些靜態標記"激活"爲客戶端上徹底可交互的應用程序。
服務器端渲染(SSR)的優點主要在於:
我以前寫過一篇文章,詳細討論了 如何實現一個服務端渲染項目。
可是這樣一個看似美好的方案,一樣存在須要權衡的地方:
同時還有一個不得不考慮的問題是,因爲服務端渲染模糊了先後端的界限,須要更多的服務器方面的知識,在項目落地時要全面考慮運維、後期項目交接等等,最後放棄這個方案~
預渲染 能夠達到和 SSR 相似的目的,它在編譯階段,將指定的頁面編譯成 html,當有請求時直接將 html 內容發送給客戶端,可是他也存在問題:
但不管怎樣,這是一個不錯的方案,VUE 官方也推薦這個方案,在實踐的過程當中我也遇到了一些問題,同時也作了一些 記錄 ,可是綜合考慮仍是放棄了這個方案。
這是咱們最終選擇的方案,這個方案從原理到實現都相對簡單,它藉助 html-webpack-plugin 將一段指定的 html 和 css 插入到模板中,在 js 和 api 請求未返回以前,以最快的速度給用戶一個 loading 提示,告知用戶獲得了響應。
具體作法以下:
將 loading 效果的 html 拆分紅 loading.html
和 loading.css
,分別放在 /src/preLoad/loading.html 和 /src/preLoad/loading.css
在 webpack 的 config 文件裏讀取這兩個文件:
module.exports = {
loading: {
html: fs.readFileSync(path.join(__dirname, '../src/preLoad/loading.html')),
css: '<style>' + fs.readFileSync(path.join(__dirname, '../src/preLoad/loading.css')) + '</style>'
}
// ...
}
複製代碼
在 build 的配置文件裏引入:
module.exports = {
plugins: [
new HtmlWebpackPlugin({
loading: config.loading
// ...
})
// ...
]
// ...
}
複製代碼
在模板文件中插入變量:
<!DOCTYPE html>
<html>
<head>
<!-- ... -->
<%= htmlWebpackPlugin.options.loading.css %>
</head>
<body>
<div id="app">
<%= htmlWebpackPlugin.options.loading.html %>
</div>
</body>
</html>
複製代碼
經過這種方法,能夠將一個動態的 loading 效果插入到頁面中。
因爲 css 會阻塞渲染,因此當咱們看到這個 loading 以前,儘可能的少加載其餘的 css 和 js,採用的方案是不提取 css,因爲 vue-cli 默認的設置是提取,因此須要手動修改一下:
// vue-loader.conf.js
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: false // 不提取
}),
// ...
}
複製代碼
這樣 css 就會被編譯進 js 裏,經過 js 進行輸出,在 loading 效果出來以前,不會阻塞頁面。
最後還有一個問題須要解決,因爲程序依賴各類第三方包,這些包都會打包到 vendor.js 中,使這個文件特別大,甚至超過了 1M,須要對這塊進行優化,思路就是告訴 webpack 要打包不,不要將某些包打到 vendor.js裏,而後咱們手動在 html 裏引入這些文件。
因爲如今第三方 CDN 提供了穩定的資源訪問,並且藉助 http/2 的多種利用特性,使的這些第三方資源加載特別快,具體作法以下:
在 webpack 的 config 定義將要從第三方引入的資源:
在 webpack 的基礎配置文件裏定義那些包不須要打包到 vendor.js裏:
// webpack.base.conf.js
module.exports = {
externals: {
'vue': 'Vue',
'vuex': 'Vuex',
'iview': 'iview'
}
//...
}
複製代碼
在 webpack 的 config 文件裏批明第三方資源:
module.exports = {
loading: {
html: fs.readFileSync(path.join(__dirname, '../src/preLoad/loading.html')),
css: '<style>' + fs.readFileSync(path.join(__dirname, '../src/preLoad/loading.css')) + '</style>'
},
css: [
'https://cdn.jsdelivr.net/npm/iview@2.14.3/dist/styles/iview.css'
],
js: [
'https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js',
'https://cdn.jsdelivr.net/npm/vuex@3.0.1/dist/vuex.min.js',
'https://cdn.jsdelivr.net/npm/iview@2.14.3/dist/iview.min.js'
]
// ...
}
複製代碼
和 loading 相似,在 build 的配置文件裏引入:
module.exports = {
plugins: [
new HtmlWebpackPlugin({
loading: config.loading,
externals_js: config.js, // 引入 js
externals_css: config.css, // 引入 css
// ...
})
// ...
]
// ...
}
複製代碼
在模板文件中插入變量:
<!DOCTYPE html>
<html>
<head>
<!-- ... -->
<%= htmlWebpackPlugin.options.loading.css %>
<% for (var i in htmlWebpackPlugin.options.externals_css) { %>
<link href="<%= htmlWebpackPlugin.options.externals_css[i] %>" rel="stylesheet">
<% } %>
</head>
<body>
<div id="app">
<%= htmlWebpackPlugin.options.loading.html %>
</div>
<% for (var i in htmlWebpackPlugin.options.externals_js) { %>
<script src="<%= htmlWebpackPlugin.options.externals_js[i] %>"></script>
<% } %>
</body>
</html>
複製代碼
經過這種方法,將比較大的包從 vendor.js 裏剔除,經過第三方 CDN 引入。
經過這幾個方面的處理,來對比一下優化先後的數據:
項目 | 優化前 | 優化後 |
---|---|---|
總的資源加載量 | 6.7M | 939K |
總加載完成時間 | 19.05s | 5.11s |
首屏渲染時間 | 808ms | 391ms |
首次內容渲染 | 2.53s | 1.62s |
PageSpeed Insights 分數 | 13 分 | 83 分 |
因爲瀏覽器訪問及測試受限於網絡及服務的不穩定性,其結果是不精確的,但作爲參考值能夠看到他是有很大提高。