最近總結了一下自己在項目中使用到的性能優化手段,這里主要從兩個部分來詳解vue項目的性能優化:代碼層優化、webpack打包優化
一、代碼優化
1、v-if 和 v-show
v-if 是懶加載,當狀態為 true 時才會加載,並且為 false 時不會占用布局空間;
v-show 是無論狀態是 true 或者是 false,都會進行渲染,並且只是簡單地基於 CSS 的 display 屬性進行切換,並占據布局空間。對於在項目中,需要頻繁調用,不需要權限的顯示隱藏,可以選擇使用 v-show,可以減少系統的切換開銷。
在我來看要分兩個維度去思考問題:
第一個維度是權限問題,只要涉及到權限相關的展示無疑要用 v-if
,
第二個維度在沒有權限限制下根據用戶點擊的頻次選擇,頻繁切換的使用 v-show
,不頻繁切換的使用 v-if
,
這里要說的優化點在於減少頁面中 dom 總數,我比較傾向於使用 v-if
,因為減少了 dom 數量,加快首屏渲染,至於性能方面我感覺肉眼看不出來切換的渲染過程,也不會影響用戶的體驗。
2、不要在模板里面寫過多的表達式與判斷
v-if="isShow && isAdmin && (a || b)"
,這種表達式雖說可以識別,但是不是長久之計,當看着不舒服時,適當的寫到 methods 和 computed 里面封裝成一個方法,這樣的好處是方便我們在多處判斷相同的表達式,其他權限相同的元素再判斷展示的時候調用同一個方法即可。
3、v-for為item設置唯一key值
詳見之前我總結的這篇博客:圖解vue中 v-for 的 :key 的作用,虛擬dom Diff算法
v-for 在列表數據進行遍歷渲染時,需要為每一項item設置唯一key值,方便vue.js內部機制精准找到該條列表數據。當state更新時,新的狀態值和舊的狀態值對比,較快地定位到diff。
循環調用子組件時添加 key:key 可以唯一標識一個循環個體,可以使用例如 item.id
作為 key,假如數組數據是這樣的 ['a' , 'b', 'c', 'a']
,使用 :key="item"
顯然沒有意義,更好的辦法就是在循環的時候 (item, index) in arr
,然后 :key="index"
來確保 key 的唯一性
當 Vue.js 用v-for
正在更新已渲染過的元素列表時,它默認用“就地復用”策略。如果數據項的順序被改變,Vue 將不會移動 DOM 元素來匹配數據項的順序, 而是簡單復用此處每個元素,並且確保它在特定索引下顯示已被渲染過的每個元素。為了給 Vue 一個提示,以便它能跟蹤每個節點的身份,從而重用和重新排序現有元素,你需要為每項提供一個唯一 key 屬性。理想的 key 值是每項都有的且唯一的 id。這個特殊的屬性相當於 Vue 1.x 的 track-by ,但它的工作方式類似於一個屬性,所以你需要用 v-bind 來綁定動態值 。
4、v-for 遍歷避免同時使用 v-if
當 v-if 與 v-for 一起使用時,v-for 具有比 v-if 更高的優先級,這意味着 v-if 將分別重復運行於每個 v-for 循環中。所以,不推薦v-if和v-for同時使用,必要情況下可以替換成 computed 屬性。
5、computed 和 watch 區分使用場景
computed: 是計算屬性,依賴其它屬性值,並且 computed 的值有緩存,只有它依賴的屬性值發生改變,下一次獲取 computed 的值時才會重新計算 computed 的值;
watch: 更多的是「觀察」的作用,類似於某些數據的監聽回調 ,每當監聽的數據變化時都會執行回調進行后續操作;
運用場景:
1、當我們需要進行數值計算,並且依賴於其它數據時,應該使用 computed,因為可以利用 computed 的緩存特性,避免每次獲取值時,都要重新計算;
2、當我們需要在數據變化時執行異步或開銷較大的操作時,應該使用 watch。使用 watch 選項允許我們執行異步操作 ( 訪問一個 API ),限制我們執行該操作的頻率,並在我們得到最終結果前,設置中間狀態。這些都是計算屬性無法做到的。
詳見之前總結的這2篇博客:理解Vue的計算屬性、Vue偵聽器watch、vue中watch的用法總結以及報錯處理Error in callback for watcher "checkList"
6、長列表性能優化
詳見我之前總結的這篇博客:vue利用 object.freeze 提升列表渲染性能
Vue 會通過 Object.defineProperty 對數據進行劫持,來實現視圖響應數據的變化,然而有些時候我們的組件就是純粹的數據展示,不會有任何改變,我們就不需要 Vue 來劫持我們的數據,在大量數據展示的情況下,這能夠很明顯的減少組件初始化的時間,那如何禁止 Vue 劫持我們的數據呢?可以通過 Object.freeze 方法來凍結一個對象,一旦被凍結的對象就再也不能被修改了。
//Object.freeze 方法凍結對象
this.data = Object.freeze(res.data);
7、事件的銷毀
Vue 組件銷毀時,會自動清理它與其它實例的連接,解綁它的全部指令及事件監聽器,但是僅限於組件本身的事件。例如,當我們執行某個計時器的時候,頁面銷毀的時候我們肯定要把事件銷毀,銷毀計時器一般有兩種方法,我建議第二種方法。
方法一、在data函數中定義定時器名稱,然后在methods中使用定時器,最后在beforeDestroy()生命周期內清除定時器
data(){ return { timer: null // 定時器名稱
} }, methods: { this.timer = setInterval(()=>{ // 執行定時器操作
}, 500) }, beforeDestroy() { clearInterval(this.timer); this.timer = null; }
這個方法有兩點不好的地方:
1、它需要在這個組件實例中保存這個 timer,如果可以的話最好只有生命周期鈎子可以訪問到它。這並不算嚴重的問題,但是它可以被視為雜物。
2、我們的建立代碼獨立於我們的清理代碼,這使得我們比較難於程序化的清理我們建立的所有東西。
方法二:通過$once這個事件偵聽器器在定義完定時器之后的位置來清除定時器
const timer = setInterval(() =>{ // 執行定時器操作
}, 500); // 通過$once來監聽定時器,在beforeDestroy鈎子可以被清除。
this.$once('hook:beforeDestroy', () => { clearInterval(timer); })
詳見我之前總結的這篇博客:VUE @hook淺析(監聽子組件的生命周期鈎子)
8、細分vue組件,css樣式以及mixin使用
在項目開發過程之中,如果把所有的組件的布局寫在一個組件中,這樣該組件文件大小也會比較大,當數據變更時,由於組件代碼比較龐大,vue的數據驅動視圖更新比較慢,造成渲染比較慢,造成比較差的體驗效果。所以把組件細分,比如一個組件,可以把整個組件細分成頭部組件、左側菜單組件、內容區組件等。能復用的功能一定要封裝成公共組件,例如一些彈窗組件。這樣,不僅加載速度更快(js文件更小),而且還更好維護。細分vue組件、提取公共css樣式、使用mixin均是這個效果。
詳見我之前總結的這篇博客:VUE的mixin混入解析
9、對路由組件進行懶加載
這里的懶加載是指在訪問到對應的組件時才加載它,首屏的時候不加載。這里實現的方法很簡單,只要將以前直接import組件的方式改為:const Login = () => import('@/pages/Login’);即可。
Vue 是單頁面應用,可能會有很多的路由引入 ,這樣使用 webpcak 打包后的文件很大,當進入首頁時,加載的資源過多,頁面會出現白屏的情況,不利於用戶體驗。如果我們能把不同路由對應的組件分割成不同的代碼塊,然后當路由被訪問的時候才加載對應的組件,這樣就更加高效了。這樣會大大提高首屏顯示的速度,但是可能其他的頁面的速度就會降下來。
import Vue from 'vue' import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'
Vue.use(Router) export default new Router({ routes: [ // { // path: '/', // name: 'HelloWorld', // component: HelloWorld // }
{ path: '/', name: 'HelloWorld', component: () => import('@/components/HelloWorld.vue') } ] })
將注釋的內容改為下面這種引入即可。詳見之前寫的這篇博客:vue-router 懶加載優化
10、ui框架按需加載
比如之前寫的這篇博客:vue按需引入echarts
不多說,各ui框架均有介紹。
11、圖片資源懶加載
對於圖片過多的頁面,為了加速頁面加載速度,可以使用v-lazy之類的懶加載庫或者綁定鼠標的scroll事件,滾動到可視區域先再對數據進行加載顯示,減少系統加載的數據。這樣對於頁面加載性能上會有很大的提升,也提高了用戶體驗。
12、避免內存泄漏
關於內存泄漏,詳見我之前總結的這2篇博客:
二、webpack打包優化
1、優化 SourceMap
source-map:一種提供源代碼 到 構建后 代碼映射技術(如果構建后的代碼出錯了,通過映射可以追蹤源代碼的錯誤)
打開webpack.config.js
source-map :外部,錯誤代碼准確信息 和 源代碼的錯誤位置
devtool的全部值及介紹 source-map: 一種 提供源代碼到構建后代碼映射 技術 (如果構建后代碼出錯了, 通過映射可以追蹤源代碼錯誤) [inline-|hidden-|eval-] [nosources] [cheap-[module-]]source-map source-map:外部--->錯誤代碼准確信息, 源代碼的錯誤位置 inline-source-map:內嵌--->錯誤代碼准確信息 和源代碼的錯誤位置 hidden-source-map:外部--->錯誤代碼錯誤原因, 但沒有錯誤位置,不能追蹤源代碼錯誤(隱藏源代碼) eval-source-map:內嵌--->錯誤代碼准確信息, 源代碼的錯誤位置 nosources-source-map:外聯--->錯誤代碼准確信息,但是沒有任何源代碼信息(隱藏源代碼) cheap-source-map:外部--->錯誤代碼准確信息 和 源代碼的錯誤位置,只能精確行 cheap-module-source-map外部--->錯誤代碼准確信息, 源代碼的錯誤位置 內聯 和 外部的區別: 1. 外部生成了文件 , 內聯沒有文件, 2. 內聯構建速度快 這么多source-map如何選擇? 開發環境: 速度快,調試更友好 速度快( eval>inline>cheap>··· ) 組合: eval-cheap-source-map > eval-source-map 調試更友好 組合source-map > cheap-module-source-map > cheap-source-map 最終結果:eval-source-map(速度快)和 cheap-module-source-map(性能更好) (vuecli與react腳手架默認) 生產環境: 源代碼要不要隱藏?調試要不要更友好 內嵌會讓代碼體積變大,所以在生產環境下不用 內嵌 nosources-source-map 全部隱藏 hidden-source-map 只隱藏源代碼,會提示構建后代碼錯誤信息 最終結果: source-map 和 cheap-module-source-map
開發環境: eval-source-map 或者 cheap-module-source-map
生產環境: source-map 或者 cheap-module-source-map
2、使用cdn引入第三方插件
打包時,把vue、vuex、vue-router、axios等,換用國內的bootcdn直接引入到根目錄的index.html。在webpack設置中添加externals,忽略不需要打包的庫。
module.exports = { context: path.resolve(__dirname, '../'), entry: { app: './src/main.js' }, externals:{ 'vue':'Vue', 'vue-router':'VueRouter', 'vuex':'Vuex' },
在index.html中使用cdn引入
<script src="//cdn.bootcss.com/vue/2.2.5/vue.min.js"></script>
<script src="//cdn.bootcss.com/vue-router/2.3.0/vue-router.min.js"></script>
<script src="//cdn.bootcss.com/vuex/2.2.1/vuex.min.js"></script>
<script src="//cdn.bootcss.com/axios/0.15.3/axios.min.js"></script>
去掉原有的引用,否則還是會打包
//去掉import,如: //import Vue from 'vue' //import Router from 'vue-router' //去掉Vue.use(XXX),如: //Vue.use(Router)
3、開啟 gzip 壓縮
安裝 compression-webpack-plugin:cnpm i compression-webpack-plugin -D
在 vue.config.js中引入並修改 webpack配置:
const CompressionPlugin = require('compression-webpack-plugin') configureWebpack: (config) => { if (process.env.NODE_ENV === 'production') { // 為生產環境修改配置...
config.mode = 'production'
return { plugins: [new CompressionPlugin({ test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //對超過10k的數據進行壓縮
deleteOriginalAssets: false //是否刪除原文件
})] } } }
在服務器我們也要做相應的配置,如果發送請求的瀏覽器支持 gzip,就發送給它 gzip 格式的文件,我的服務器是用 express框架搭建的,只要安裝一下 compression就能使用
//注意,要放在所有其他中間件注冊之前
const compression = require('compression') app.use(compression())
更好的是直接通過nigix配置gzip壓縮,詳見之前我寫的這篇博客:nginx配置解決vue單頁面打包文件大,首次加載慢的問題
4、Webpack 對圖片進行壓縮
在 vue 項目中除了可以在 webpack.base.conf.js 中 url-loader 中設置 limit 大小來對圖片處理,對小於 limit 的圖片轉化為 base64 格式,其余的不做操作。所以對有些較大的圖片資源,在請求資源的時候,加載會很慢,我們可以用 image-webpack-loader來壓縮圖片:
首先,安裝 image-webpack-loader:npm install image-webpack-loader --save-dev
然后,在 webpack.base.conf.js 中進行配置
{ test: /\.(png|jpeg|gif|svg)(\?.*)?$/, use:[ { loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { loader: 'image-webpack-loader', options: { bypassOnDebug: true, } } ] }
5、減少 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 編譯出來的代碼的文件大小。
首先,安裝 babel-plugin-transform-runtime :npm install babel-plugin-transform-runtime --save-dev
然后,修改 .babelrc 配置文件為:
"plugins": [ "transform-runtime" ]
6、提取公共代碼
如果項目中沒有去將每個頁面的第三方庫和公共模塊提取出來,則項目會存在以下問題:
1、相同的資源被重復加載,浪費用戶的流量和服務器的成本。
1、每個頁面需要加載的資源太大,導致網頁首屏加載緩慢,影響用戶體驗。
所以我們需要將多個頁面的公共代碼抽離成單獨的文件,來優化以上問題 。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'] })
7、模板預編譯
當使用 DOM 內模板或 JavaScript 內的字符串模板時,模板會在運行時被編譯為渲染函數。通常情況下這個過程已經足夠快了,但對性能敏感的應用還是最好避免這種用法。
預編譯模板最簡單的方式就是使用單文件組件——相關的構建設置會自動把預編譯處理好,所以構建好的代碼已經包含了編譯出來的渲染函數而不是原始的模板字符串。
如果你使用 webpack,並且喜歡分離 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在構建過程中把模板文件轉換成為 JavaScript 渲染函數。
8、提取組件的 CSS
當使用單文件組件時,組件內的 CSS 會以 style 標簽的方式通過 JavaScript 動態注入。這有一些小小的運行時開銷,如果你使用服務端渲染,這會導致一段 “無樣式內容閃爍 (fouc) ” 。將所有組件的 CSS 提取到同一個文件可以避免這個問題,也會讓 CSS 更好地進行壓縮和緩存。
查閱這個構建工具各自的文檔來了解更多:webpack + vue-loader ( vue-cli 的 webpack 模板已經預先配置好)、Browserify + vueify、Rollup + rollup-plugin-vue。
詳見之前寫的這篇博客:NuxtJS處理因css在服務端渲染而增加源代碼量,從而影響到SEO的問題及VUE提取 CSS 到單個文件