前言
Vue 框架通過資料雙向繫結和虛擬 DOM 技術,幫我們處理了前端開發中最髒最累的 DOM 操作部分, 我們不再需要去考慮如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 專案中仍然存在專案首屏優化、Webpack 編譯配置優化等問題,所以我們仍然需要去關注 Vue 專案效能方面的優化,使專案具有更高效的效能、更好的使用者體驗。本文是作者通過實際專案的優化實踐進行總結而來,希望讀者讀完本文,有一定的啟發思考,從而對自己的專案進行優化起到幫助。本文內容分為以下三部分組成:
- Vue 程式碼層面的優化;
- webpack 配置層面的優化;
- 基礎的 Web 技術層面的優化。
辛苦整理良久,還望手動點贊鼓勵~
一、程式碼層面的優化
1.1、v-if 和 v-show 區分使用場景
v-if 是 真正 的條件渲染,因為它會確保在切換過程中條件塊內的事件監聽器和子元件適當地被銷燬和重建;也是惰性的:如果在初始渲染時條件為假,則什麼也不做——直到條件第一次變為真時,才會開始渲染條件塊。
v-show 就簡單得多, 不管初始條件是什麼,元素總是會被渲染,並且只是簡單地基於 CSS 的 display 屬性進行切換。
所以,v-if 適用於在執行時很少改變條件,不需要頻繁切換條件的場景;v-show 則適用於需要非常頻繁切換條件的場景。
1.2、computed 和 watch 區分使用場景
computed: 是計算屬性,依賴其它屬性值,並且 computed 的值有快取,只有它依賴的屬性值發生改變,下一次獲取 computed 的值時才會重新計算 computed 的值;
watch: 更多的是「觀察」的作用,類似於某些資料的監聽回撥 ,每當監聽的資料變化時都會執行回撥進行後續操作;
運用場景:
-
當我們需要進行數值計算,並且依賴於其它資料時,應該使用 computed,因為可以利用 computed 的快取特性,避免每次獲取值時,都要重新計算;
-
當我們需要在資料變化時執行非同步或開銷較大的操作時,應該使用 watch,使用 watch 選項允許我們執行非同步操作 ( 訪問一個 API ),限制我們執行該操作的頻率,並在我們得到最終結果前,設定中間狀態。這些都是計算屬性無法做到的。
1.3、v-for 遍歷必須為 item 新增 key,且避免同時使用 v-if
(1)v-for 遍歷必須為 item 新增 key
在列表資料進行遍歷渲染時,需要為每一項 item 設定唯一 key 值,方便 Vue.js 內部機制精準找到該條列表資料。當 state 更新時,新的狀態值和舊的狀態值對比,較快地定位到 diff 。
(2)v-for 遍歷避免同時使用 v-if
v-for 比 v-if 優先順序高,如果每一次都需要遍歷整個陣列,將會影響速度,尤其是當之需要渲染很小一部分的時候,必要情況下應該替換成 computed 屬性。
推薦
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}
複製程式碼
不推薦:
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id">
{{ user.name }}
</li>
</ul>
複製程式碼
1.4、長列表效能優化
Vue 會通過 Object.defineProperty 對資料進行劫持,來實現檢視響應資料的變化,然而有些時候我們的元件就是純粹的資料展示,不會有任何改變,我們就不需要 Vue 來劫持我們的資料,在大量資料展示的情況下,這能夠很明顯的減少元件初始化的時間,那如何禁止 Vue 劫持我們的資料呢?可以通過 Object.freeze 方法來凍結一個物件,一旦被凍結的物件就再也不能被修改了。
export default {
data: () => ({
users: {}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};
複製程式碼
1.5、事件的銷燬
Vue 元件銷燬時,會自動清理它與其它例項的連線,解綁它的全部指令及事件監聽器,但是僅限於元件本身的事件。 如果在 js 內使用 addEventListene 等方式是不會自動銷燬的,我們需要在元件銷燬時手動移除這些事件的監聽,以免造成記憶體洩露,如:
created() {
addEventListener('click', this.click, false)
},
beforeDestroy() {
removeEventListener('click', this.click, false)
}
複製程式碼
1.6、圖片資源懶載入
對於圖片過多的頁面,為了加速頁面載入速度,所以很多時候我們需要將頁面內未出現在可視區域內的圖片先不做載入, 等到滾動到可視區域後再去載入。這樣對於頁面載入效能上會有很大的提升,也提高了使用者體驗。我們在專案中使用 Vue 的 vue-lazyload 外掛:
(1)安裝外掛
npm install vue-lazyload --save-dev
複製程式碼
(2)在入口檔案 man.js 中引入並使用
import VueLazyload from 'vue-lazyload'
複製程式碼
然後再 vue 中直接使用
Vue.use(VueLazyload)
複製程式碼
或者新增自定義選項
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png',
loading: 'dist/loading.gif',
attempt: 1
})
複製程式碼
(3)在 vue 檔案中將 img 標籤的 src 屬性直接改為 v-lazy ,從而將圖片顯示方式更改為懶載入顯示:
<img v-lazy="/static/img/1.png">
複製程式碼
以上為 vue-lazyload 外掛的簡單使用,如果要看外掛的更多引數選項,可以檢視 vue-lazyload 的 github 地址。
1.7、路由懶載入
Vue 是單頁面應用,可能會有很多的路由引入 ,這樣使用 webpcak 打包後的檔案很大,當進入首頁時,載入的資源過多,頁面會出現白屏的情況,不利於使用者體驗。如果我們能把不同路由對應的元件分割成不同的程式碼塊,然後當路由被訪問的時候才載入對應的元件,這樣就更加高效了。這樣會大大提高首屏顯示的速度,但是可能其他的頁面的速度就會降下來。
路由懶載入:
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
複製程式碼
1.8、第三方外掛的按需引入
我們在專案中經常會需要引入第三方外掛,如果我們直接引入整個外掛,會導致專案的體積太大,我們可以藉助 babel-plugin-component
,然後可以只引入需要的元件,以達到減小專案體積的目的。以下為專案中引入 element-ui 元件庫為例:
(1)首先,安裝
babel-plugin-component
:
npm install babel-plugin-component -D
複製程式碼
(2)然後,將 .babelrc 修改為:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
複製程式碼
(3)在 main.js 中引入部分元件:
import Vue from 'vue';
import { Button, Select } from 'element-ui';
Vue.use(Button)
Vue.use(Select)
複製程式碼
1.9、優化無限列表效能
如果你的應用存在非常長或者無限滾動的列表,那麼需要採用 視窗化 的技術來優化效能,只需要渲染少部分割槽域的內容,減少重新渲染元件和建立 dom 節點的時間。 你可以參考以下開源專案 vue-virtual-scroll-list 和 vue-virtual-scroller 來優化這種無限列表的場景的。
1.10、服務端渲染 SSR or 預渲染
服務端渲染是指 Vue 在客戶端將標籤渲染成的整個 html 片段的工作在服務端完成,服務端形成的 html 片段直接返回給客戶端這個過程就叫做服務端渲染。
(1)服務端渲染的優點:
-
更好的 SEO: 因為 SPA 頁面的內容是通過 Ajax 獲取,而搜尋引擎爬取工具並不會等待 Ajax 非同步完成後再抓取頁面內容,所以在 SPA 中是抓取不到頁面通過 Ajax 獲取到的內容;而 SSR 是直接由服務端返回已經渲染好的頁面(資料已經包含在頁面中),所以搜尋引擎爬取工具可以抓取渲染好的頁面;
-
更快的內容到達時間(首屏載入更快): SPA 會等待所有 Vue 編譯後的 js 檔案都下載完成後,才開始進行頁面的渲染,檔案下載等需要一定的時間等,所以首屏渲染需要一定的時間;SSR 直接由服務端渲染好頁面直接返回顯示,無需等待下載 js 檔案及再去渲染等,所以 SSR 有更快的內容到達時間;
(2)服務端渲染的缺點:
-
更多的開發條件限制: 例如服務端渲染只支援 beforCreate 和 created 兩個鉤子函式,這會導致一些外部擴充套件庫需要特殊處理,才能在服務端渲染應用程式中執行;並且與可以部署在任何靜態檔案伺服器上的完全靜態單頁面應用程式 SPA 不同,服務端渲染應用程式,需要處於 Node.js server 執行環境;
-
更多的伺服器負載:在 Node.js 中渲染完整的應用程式,顯然會比僅僅提供靜態檔案的 server 更加大量佔用CPU 資源,因此如果你預料在高流量環境下使用,請準備相應的伺服器負載,並明智地採用快取策略。
如果你的專案的 SEO 和 首屏渲染是評價專案的關鍵指標,那麼你的專案就需要服務端渲染來幫助你實現最佳的初始載入效能和 SEO,具體的 Vue SSR 如何實現,可以參考作者的另一篇文章《Vue SSR 踩坑之旅》。如果你的 Vue 專案只需改善少數營銷頁面(例如 /, /about, /contact
等)的 SEO,那麼你可能需要預渲染,在構建時 (build time) 簡單地生成針對特定路由的靜態 HTML 檔案。優點是設定預渲染更簡單,並可以將你的前端作為一個完全靜態的站點,具體你可以使用 prerender-spa-plugin 就可以輕鬆地新增預渲染 。
二、Webpack 層面的優化
2.1、Webpack 對圖片進行壓縮
在 vue 專案中除了可以在 webpack.base.conf.js
中 url-loader 中設定 limit 大小來對圖片處理,對小於 limit 的圖片轉化為 base64 格式,其餘的不做操作。所以對有些較大的圖片資源,在請求資源的時候,載入會很慢,我們可以用 image-webpack-loader
來壓縮圖片:
(1)首先,安裝 image-webpack-loader :
npm install image-webpack-loader --save-dev
複製程式碼
(2)然後,在 webpack.base.conf.js 中進行配置:
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
複製程式碼
2.2、減少 ES6 轉為 ES5 的冗餘程式碼
Babel 外掛會在將 ES6 程式碼轉換成 ES5 程式碼時會注入一些輔助函式,例如下面的 ES6 程式碼:
class HelloWebpack extends Component{...}
複製程式碼
這段程式碼再被轉換成能正常執行的 ES5 程式碼時需要以下兩個輔助函式:
babel-runtime/helpers/createClass // 用於實現 class 語法
babel-runtime/helpers/inherits // 用於實現 extends 語法
複製程式碼
在預設情況下, Babel 會在每個輸出檔案中內嵌這些依賴的輔助函式程式碼,如果多個原始碼檔案都依賴這些輔助函式,那麼這些輔助函式的程式碼將會出現很多次,造成程式碼冗餘。為了不讓這些輔助函式的程式碼重複出現,可以在依賴它們時通過 require('babel-runtime/helpers/createClass')
的方式匯入,這樣就能做到只讓它們出現一次。babel-plugin-transform-runtime
外掛就是用來實現這個作用的,將相關輔助函式進行替換成匯入語句,從而減小 babel 編譯出來的程式碼的檔案大小。
(1)首先,安裝 babel-plugin-transform-runtime
:
npm install babel-plugin-transform-runtime --save-dev
複製程式碼
(2)然後,修改 .babelrc 配置檔案為:
"plugins": [
"transform-runtime"
]
複製程式碼
如果要看外掛的更多詳細內容,可以檢視babel-plugin-transform-runtime 的 詳細介紹。
2.3、提取公共程式碼
如果專案中沒有去將每個頁面的第三方庫和公共模組提取出來,則專案會存在以下問題:
- 相同的資源被重複載入,浪費使用者的流量和伺服器的成本。
- 每個頁面需要載入的資源太大,導致網頁首屏載入緩慢,影響使用者體驗。
所以我們需要將多個頁面的公共程式碼抽離成單獨的檔案,來優化以上問題 。Webpack 內建了專門用於提取多個Chunk 中的公共部分的外掛 CommonsChunkPlugin,我們在專案中 CommonsChunkPlugin 的配置如下:
// 所有在 package.json 裡面依賴的包,都會被打包進 vendor.js 這個檔案中。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module, count) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
);
}
}),
// 抽取出程式碼模組的對映關係
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
複製程式碼
如果要看外掛的更多詳細內容,可以檢視 CommonsChunkPlugin 的 詳細介紹。
2.4、模板預編譯
當使用 DOM 內模板或 JavaScript 內的字串模板時,模板會在執行時被編譯為渲染函式。通常情況下這個過程已經足夠快了,但對效能敏感的應用還是最好避免這種用法。
預編譯模板最簡單的方式就是使用單檔案元件——相關的構建設定會自動把預編譯處理好,所以構建好的程式碼已經包含了編譯出來的渲染函式而不是原始的模板字串。
如果你使用 webpack,並且喜歡分離 JavaScript 和模板檔案,你可以使用 vue-template-loader,它也可以在構建過程中把模板檔案轉換成為 JavaScript 渲染函式。
2.5、提取元件的 CSS
當使用單檔案元件時,元件內的 CSS 會以 style 標籤的方式通過 JavaScript 動態注入。這有一些小小的執行時開銷,如果你使用服務端渲染,這會導致一段 “無樣式內容閃爍 (fouc) ” 。將所有元件的 CSS 提取到同一個檔案可以避免這個問題,也會讓 CSS 更好地進行壓縮和快取。
查閱這個構建工具各自的文件來了解更多:
- webpack + vue-loader ( vue-cli 的 webpack 模板已經預先配置好)
- Browserify + vueify - Rollup + rollup-plugin-vue
2.6、優化 SourceMap
我們在專案進行打包後,會將開發中的多個檔案程式碼打包到一個檔案中,並且經過壓縮、去掉多餘的空格、babel編譯化後,最終將編譯得到的程式碼會用於線上環境,那麼這樣處理後的程式碼和原始碼會有很大的差別,當有 bug的時候,我們只能定位到壓縮處理後的程式碼位置,無法定位到開發環境中的程式碼,對於開發來說不好調式定位問題,因此 sourceMap 出現了,它就是為了解決不好調式程式碼問題的。
SourceMap 的可選值如下(+ 號越多,代表速度越快,- 號越多,代表速度越慢, o 代表中等速度 )
開發環境推薦: cheap-module-eval-source-map
生產環境推薦: cheap-module-source-map
原因如下:
-
cheap: 原始碼中的列資訊是沒有任何作用,因此我們打包後的檔案不希望包含列相關資訊,只有行資訊能建立打包前後的依賴關係。因此不管是開發環境或生產環境,我們都希望新增 cheap 的基本型別來忽略打包前後的列資訊;
-
module :不管是開發環境還是正式環境,我們都希望能定位到bug的原始碼具體的位置,比如說某個 Vue 檔案報錯了,我們希望能定位到具體的 Vue 檔案,因此我們也需要 module 配置;
-
soure-map :source-map 會為每一個打包後的模組生成獨立的 soucemap 檔案 ,因此我們需要增加source-map 屬性;
-
eval-source-map:eval 打包程式碼的速度非常快,因為它不生成 map 檔案,但是可以對 eval 組合使用 eval-source-map 使用會將 map 檔案以 DataURL 的形式存在打包後的 js 檔案中。在正式環境中不要使用 eval-source-map, 因為它會增加檔案的大小,但是在開發環境中,可以試用下,因為他們打包的速度很快。
2.7、構建結果輸出分析
Webpack 輸出的程式碼可讀性非常差而且檔案非常大,讓我們非常頭疼。為了更簡單、直觀地分析輸出結果,社群中出現了許多視覺化分析工具。這些工具以圖形的方式將結果更直觀地展示出來,讓我們快速瞭解問題所在。接下來講解我們在 Vue 專案中用到的分析工具:webpack-bundle-analyzer
。
我們在專案中 webpack.prod.conf.js
進行配置:
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
複製程式碼
執行 $ npm run build --report
後生成分析報告如下:
2.8、Vue 專案的編譯優化
如果你的 Vue 專案使用 Webpack 編譯,需要你喝一杯咖啡的時間,那麼也許你需要對專案的 Webpack 配置進行優化,提高 Webpack 的構建效率。具體如何進行 Vue 專案的 Webpack 構建優化,可以參考作者的另一篇文章《 Vue 專案 Webpack 優化實踐》
三、基礎的 Web 技術優化
3.1、開啟 gzip 壓縮
gzip 是 GNUzip 的縮寫,最早用於 UNIX 系統的檔案壓縮。HTTP 協議上的 gzip 編碼是一種用來改進 web 應用程式效能的技術,web 伺服器和客戶端(瀏覽器)必須共同支援 gzip。目前主流的瀏覽器,Chrome,firefox,IE等都支援該協議。常見的伺服器如 Apache,Nginx,IIS 同樣支援,gzip 壓縮效率非常高,通常可以達到 70% 的壓縮率,也就是說,如果你的網頁有 30K,壓縮之後就變成了 9K 左右
以下我們以服務端使用我們熟悉的 express 為例,開啟 gzip 非常簡單,相關步驟如下:
- 安裝:
npm install compression --save
複製程式碼
- 新增程式碼邏輯:
var compression = require('compression');
var app = express();
app.use(compression())
複製程式碼
- 重啟服務,觀察網路皮膚裡面的 response header,如果看到如下紅圈裡的欄位則表明 gzip 開啟成功 :
3.2、瀏覽器快取
為了提高使用者載入頁面的速度,對靜態資源進行快取是非常必要的,根據是否需要重新向伺服器發起請求來分類,將 HTTP 快取規則分為兩大類(強制快取,對比快取)。
3.3、CDN 的使用
瀏覽器從伺服器上下載 CSS、js 和圖片等檔案時都要和伺服器連線,而大部分伺服器的頻寬有限,如果超過限制,網頁就半天反應不過來。而 CDN 可以通過不同的域名來載入檔案,從而使下載檔案的併發連線數大大增加,且CDN 具有更好的可用性,更低的網路延遲和丟包率 。
3.4、使用 Chrome Performance 查詢效能瓶頸
Chrome 的 Performance 皮膚可以錄製一段時間內的 js 執行細節及時間。使用 Chrome 開發者工具分析頁面效能的步驟如下。
- 開啟 Chrome 開發者工具,切換到 Performance 皮膚
- 點選 Record 開始錄製
- 重新整理頁面或展開某個節點
- 點選 Stop 停止錄製
更多關於 Performance 的內容可以點選這裡檢視。
總結
本文通過以下三部分組成:Vue 程式碼層面的優化、webpack 配置層面的優化、基礎的 Web 技術層面的優化;來介紹怎麼去優化 Vue 專案的效能。 希望對讀完本文的你有幫助、有啟發,如果有不足之處,歡迎批評指正交流!
作者:我是你的超級英雄