vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)

优化结果:
js文件大小变化: 8.6 M-->336 KB
首页加载时间变化:52.82 s-->8.58 s(浏览器模拟3G网络状态下)

一、优化工作前准备
二、配置打包环境,使用webpack4 自带的分包功能
三、路由懒加载,按需引入
四、CDN替换依赖包引入
五、查看首屏文件加载,细节分析
六、开启gzip压缩
七、开启图片压缩
--------------------------------------------------------------------------
一、优化工作前准备
首先,添加打包文件分析插件,后续相关信息都需要依赖于此进行对比查看
1. 添加插件

npm i -D webpack-bundle-analyzer

2. 修改 vue.config.js(文末有项目完整vue.config.js文件)
// 引入js分析插件 let BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin // 环境区分主要为开发环境与其他环境(其他:生产,uat,测试等等) const isNotDevelopMentEnv = process.env.NODE_ENV !== 'development'chainWebpack (config) { config.plugins.delete('preload') // TODO: need test config.plugins.delete('prefetch') // TODO: need test // 配置启用打包文件分析 if (isNotDevelopMentEnv) { // js文件包分析 if (process.env.npm_config_report) { config .plugin('webpack-bundle-analyzer') .use(BundleAnalyzerPlugin) .end() } } }

3. 使用命令(我本地打包命令是npm run build,这里看各自项目对应命令,重点是在打包命令后面加上--report),打包完会自动打开浏览器显示项目中js文件情况,后续所做的优化效果,都将通过该视图进行查看对比
npm run build --report

4. 初始项目情况
1. js文件分析
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片

2. 首屏加载情况(本地node服务器运行,模拟3G网络,后续一致)
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片


二、配置打包环境,使用webpack4 自带的分包功能
1. 项目使用的是vue-element-admin,默认已经做了代码分包,在vue.config.js中可以看到配置。
chainWebpack (config) { config .when(isNotDevelopMentEnv, config => { config .plugin('ScriptExtHtmlWebpackPlugin') .after('html') .use('script-ext-html-webpack-plugin', [{ // `runtime` must same as runtimeChunk name. default is `runtime` inline: /runtime\..*\.js$/ }]) .end() config .optimization.splitChunks({ chunks: 'all', cacheGroups: { libs: { name: 'chunk-libs', test: /[\\/]node_modules[\\/]/, priority: 10, chunks: 'initial' // only package third parties that are initially dependent }, elementUI: { name: 'chunk-elementUI', // split elementUI into a single package priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm }, commons: { name: 'chunk-commons', test: resolve('src/components'), // can customize your rules minChunks: 3, //minimum common number priority: 5, reuseExistingChunk: true } } }) config.optimization.runtimeChunk('single') } ) } }

但是因为本地新建了许多环境文件,其中没有设置这些环境文件的NODE_ENV为production,导致分包功能丢失(原有分包功能是判断NODE_ENV为production才会执行)

2. 环境文件添加NODE_ENV,对应环境打包就会使用分包功能
# 在以下环境中添加该句:表示当以下环境打包时使用分包 # .env.test (公司测试环境) # .env.prd (公司生产环境) # .env.pet (性能测试环境) # .env.uat (用户测试环境)NODE_ENV=production

3. 项目情况
1. js文件分析
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片

2. 首屏加载情况
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片


三、路由懒加载,按需引入
1. 路由引入:使用 () => import('文件名')进行懒加载
{ path: '/default/iconTool/Business', component: () => import('@/views/default/iconTool/Business') }

2. 需要额外考虑:项目会根据接口返回的菜单数据动态生成路由,使用此方式会根据页面路由各自生成对应文件,但在开发时候会因为热更新而频繁加载导致变慢,所以需要区分开发与生产环境分开
2.1 开发环境使用require一次性引入页面
2.2 生产环境使用import懒加载
const notFoundComponent = () => import('@/views/default/404.vue')try { if (process.env.NODE_ENV === 'development') { // 开发环境一次性加载,避免多路由热更新缓慢 component = require(`@/views/${item.path}.vue`).default } else { // 打包环境,按照路由懒加载拆分页面 component = () => import(`@/views/${item.path}.vue`) // 当上面路由找不到时会使用下面模块,这里不写到时404页面会出不来 // 由于import是使用懒加载,只有在使用的时候才会加载该页面, // 因此import时会直接使用页面地址,即使页面地址不存在也不会报错, // 也无法使用404替换当真正使用的时候就会找不到页面而报错,因此需要 // 再这里多添加404,找不到时会可以使用404页面 component = notFoundComponent } } catch (e) { component = notFoundComponent }

3. 注意:
3.1 按道理是全局的路由(包括已有的静态的路由 && 动态添加的路由)引入全部使用条件判断去决定require还是import引入,将这里的逻辑封转成函数,然后引入路由的地方统一使用为函数;但在实际测试中部分页面路由使用此种方式引入会异常,所以折中处理,静态路由import引入,动态路由根据环境对应引入。项目可自行测试,也许你们的可以直接使用此方式
3.2 使用 () => import('filePath')时,要求filePath必须是显示字符串,不能使用变量,但这里可以使用【模版字符串 + 部分已明确路径】实现动态路径
component = () => import(`@/views/${filePath}.vue`)

3.3 这里根据环境对应引入会有一个问题,就是必须将所有动态路由都预先定义在路由文件中,不然就会出现本地用require正常,但是生产环境上面使用import就会报错找不到页面
3.4 根据环境引入还有个问题,就是匹配不到404页面逻辑了。猜想:使用import是懒加载,只有在用到页面才会去加载,假如配置了某个路径页面是不存在的,因为懒加载所以不会执行检测到错误,也就不会跑进用404页面替换当前路径的逻辑,所以看第2小点中代码注释说明,我是在import后面还加了一句设置404页面,这样页面地址存在的时候就会使用页面,不存在就会展示404页面,也不会报错

4. 项目情况
4.1 js文件分析
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片

4.2 首屏加载情况
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片


5. 路由懒加载分组
可查看 vue-router 懒加载分组webpackChunkName相关知识
{ path: '/login', component: () => import(/* webpackChunkName: "login" */ '@/views/default/Login') }

主要就是直接懒加载的话模块太多,请求数太多,可以考虑合并模块,减轻服务器压力,但在实际优化中发现速度没有多大变化,可能还没领悟到精髓,这里就不做过多描述,项目自行考虑是否采用此优化,贴两张图意思
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片

vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片


四、CDN替换依赖包引入
1. 项目情况
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片

2. 项目打包时会根据依赖关系自动打包压缩依赖文件,当依赖文件过大,会导致首屏加载变慢
3. 使用CDN: 简单来说就是可以使用CDN引入依赖库,减少依赖包体积,释放服务器压力, 它由距离最近的缓存服务器直接响应,提高加载速度
4. 进一步考虑:当我们在开发环境下,使用CDN引入会比我们直接引入依赖要慢,所以配置CDN需要只在生产环境
5. 配置步骤
5.1 修改vue.config.js
5.1.1 定义使用CDN的相关数据
// 环境区分主要为开发环境与其他环境(其他:生产,uat,测试等等) const isNotDevelopMentEnv = process.env.NODE_ENV !== 'development' const cdnData = https://www.it610.com/article/{ css: ['https://cdn.bootcdn.net/ajax/libs/element-ui/2.13.0/theme-chalk/index.css' ], js: [ 'https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js', 'https://cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.min.js', 'https://cdn.bootcdn.net/ajax/libs/vuex/3.1.0/vuex.min.js', 'https://cdn.bootcdn.net/ajax/libs/vue-router/3.0.6/vue-router.min.js', 'https://cdn.bootcdn.net/ajax/libs/element-ui/2.13.0/index.js', 'https://cdn.bootcdn.net/ajax/libs/jquery/1.12.1/jquery.min.js', 'https://cdn.bootcdn.net/ajax/libs/vee-validate/2.0.0-rc.21/vee-validate.min.js', 'https://cdn.bootcdn.net/ajax/libs/vee-validate/2.0.0-rc.21/locale/zh_CN.js' ], externals: { 'vue': 'Vue', 'vuex': 'Vuex', 'vue-router': 'VueRouter', 'element-ui': 'ELEMENT', 'vuex': 'Vuex', 'axios': 'axios', 'vee-validate': 'VeeValidate', 'jQuery':"jquery", 'jquery': 'window.$' } }

5.1.2 在configureWebpack中添加externals
configureWebpack: { externals: isNotDevelopMentEnv ? cdnData.externals : {} }

5.1.3 在chainWepack中添加如下
if (isNotDevelopMentEnv) { config.plugin('html') .tap(args => { args[0].cdn = cdnData return args }) }


5.2 修改 public/index.html(根据环境,开发环境不使用CDN,生产环境才开放)

5.3 注意: 采用CDN引入后,不需要删除原有依赖引入,因为在本地还是使用这些依赖进行调试的,打包后因为有CDN所以不会把这些依赖引入所以不用担心,import引入的不需要变更。例如main.js中使用import ElementUI from 'element-ui', 以上的代码已经实现在开发环境会设置不适用CDN,会使用依赖包文件;当发布到生产环境,因为我们已经在vue.config.js的externals中指代了element-ui,所以这个语句也是有效的可以直接使用CDN elementUI

6. 项目情况
6.1 js文件分析
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片

6.2 首屏加载情况
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片


五、查看首屏文件加载,细节分析
1. elementUI css资源发现重复引入,因为项目的使用主题化的时候已经引入样式文件,所以main.js中可以不需要引入elementUI样式文件了
// 必须使用主题化,ui组件的样式的颜色才会同步,统一UI颜色风格 $--color-primary: $primary; $--color-danger: $danger; /* 改变 icon 字体路径变量,必需 */ $--font-path: '~element-ui/lib/theme-chalk/fonts'; @import "~element-ui/packages/theme-chalk/src/index";

具体看各自项目,自行优化,我这只是提提这边的优化

2. 关于elementUI的按需加载优化:因为项目修改了elementUI的主题色,如上图代码
@import "~element-ui/packages/theme-chalk/src/index";

这一句在按需加载后会失效,不知该如何实现按需加载后后的主题修改,所以暂时放弃按需加载,还是CDN直接处理

3. 项目情况
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片


六、开启gzip压缩
1. 安装插件
npm i -D compression-webpack-plugin

2. 配置vue.config.js
const CompressionPlugin = require('compression-webpack-plugin') // 引入gzip压缩插件 chainWebpack (config) { // 配置启用打包文件分析 if (isNotDevelopMentEnv) { // gzip压缩 config.plugin('compression').use(CompressionPlugin, [ { algorithm: 'gzip', test: new RegExp('\\.(js|css)$'), threshold: 10240, //超过多少字节进行压缩 minRatio: 0.8 //至少压缩到原来体积的0.8,才会进行压缩 } ]) } }

3. 需要服务器配合开启gzip
3.1 服务器为nginx,修改nginx.conf文件
server { gzip on; gzip_buffers 4 16K; gzip_comp_level 5; gzip_min_length 100k; gzip_types text/plain application/x-javascript application/javascript application/json text/css application/xml text/javascript image/jpeg image/gif image/png; gzip_vary on; } // gzip on|off; 是否开启gzip // gzip_min_length 100k; 压缩的最小长度(再小就不要压缩了,意义不在) // gzip_buffers 4 16k; 缓冲(压缩在内存中缓冲几块? 每块多大?) // gzip_comp_level 5; 压缩级别(级别越高,压的越小,越浪费CPU计算资源) // gzip_types text/plain; 对哪些类型的文件用压缩 如txt,xml,html,css,js等 // gzip_vary on|off; 是否传输gzip压缩标志

3.2 服务器为tomcat,修改server.xml文件
// compression="on" 打开压缩功能 // compressableMimeType="text/html,text/xml" 压缩类型 // useSendfile="false" 设置该属性将会压缩所有文件,不限阙值,不然可能按照阙值部分压缩

注意:在网上查阅文章的时候,发现有文章把【useSendfile】这个单词写错了,一直没效果,注意单词不要写错

4. 项目情况
4.1 js文件分析(注意切换到Gzipped模式查看,这个就是到时使用gzip后的文件大小)
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片

4.2 首屏加载情况
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片


七、开启图片压缩(插件地址在国外,使用jenkins自动部署时可能会下载异常,自行考虑)
1. 安装插件
npm install -D image-webpack-loader

2. vue.config.js
chainWebpack (config) { if (isNotDevelopMentEnv) { // 开启图片压缩-使用异常 config.module.rule('images') .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) .use('image-webpack-loader') .loader('image-webpack-loader') .options({ bypassOnDebug: true }) } }

3. 图片变化
压缩前
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片

压缩后
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片


七、优化结果视图分析
vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)
文章图片


总结:在最后的打包分析看,还有很大优化空间,就是字体图标iconfont模块,初始element管理框架已经对图标做了处理,只不过当时年轻,不懂为什么他们要这么做就放弃了他们的图标方案,总感觉引一份iconfont.js比较方便,现在就需要优化处理了,等后面继续优化
参考文章:
https://blog.csdn.net/sixam/article/details/106058083?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-2
https://segmentfault.com/a/1190000021444697
https://blog.csdn.net/sinat_17775997/article/details/83023148
https://blog.csdn.net/Newbie___/article/details/104925587?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase
https://juejin.im/post/5bd02f98e51d457a944b634f
【vue|vue-cli3前端性能优化与首屏加载优化(2020-06-17)】
最后附上vue.config.js代码
'use strict' const path = require('path') const defaultSettings = require('./src/settings.js') const webpack = require('webpack') // 引入js分析插件 let BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin const CompressionPlugin = require('compression-webpack-plugin') // 引入gzip压缩插件function resolve(dir) { return path.join(__dirname, dir) }const name = defaultSettings.title || 'vue Admin Template' // page title// If your port is set to 80, // use administrator privileges to execute the command line. // For example, Mac: sudo npm run // You can change the port by the following methods: // port = 9528 npm run dev OR npm run dev --port = 9528 const port = process.env.port || process.env.npm_config_port || 8000 // dev port // 环境区分主要为开发环境与其他环境(其他:生产,uat,测试等等) const isNotDevelopMentEnv = process.env.NODE_ENV !== 'development' // 非正式环境:包括开发环境与测试环境 const isNotRegularEnv = process.env.VUE_APP_ENV === 'development' || process.env.VUE_APP_ENV === 'test' const cdnData = https://www.it610.com/article/{ css: ['https://cdn.bootcdn.net/ajax/libs/element-ui/2.13.0/theme-chalk/index.css' ], js: [ 'https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js', 'https://cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.min.js', 'https://cdn.bootcdn.net/ajax/libs/vuex/3.1.0/vuex.min.js', 'https://cdn.bootcdn.net/ajax/libs/vue-router/3.0.6/vue-router.min.js', 'https://cdn.bootcdn.net/ajax/libs/element-ui/2.13.0/index.js', 'https://cdn.bootcdn.net/ajax/libs/jquery/1.12.1/jquery.min.js', 'https://cdn.bootcdn.net/ajax/libs/vee-validate/2.0.0-rc.21/vee-validate.min.js', 'https://cdn.bootcdn.net/ajax/libs/vee-validate/2.0.0-rc.21/locale/zh_CN.js' ], externals: { 'vue': 'Vue', 'vuex': 'Vuex', 'vue-router': 'VueRouter', 'element-ui': 'ELEMENT', 'vuex': 'Vuex', 'axios': 'axios', 'vee-validate': 'VeeValidate', 'jQuery':"jquery", 'jquery': 'window.$' } } // All configuration item explanations can be find in https://cli.vuejs.org/config/ module.exports = { /** * You will need to set publicPath if you plan to deploy your site under a sub path, * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, * then publicPath should be set to "/bar/". * In most cases please use '/' !!! * Detail: https://cli.vuejs.org/config/#publicpath */ publicPath: '/', outputDir: 'ROOT', assetsDir: 'static', lintOnSave: !isNotDevelopMentEnv, // TODO 是否启动问题源码追踪 productionSourceMap: isNotRegularEnv, css: { loaderOptions: { sass: { data: `@import "@/styles/global.scss"; ` } } }, // 兼容ie浏览器 // entry: ['babel-polyfill', './app/js'],devServer: { port: port, open: false, compress: true, overlay: { warnings: false, errors: true }, // proxy: { //// change xxx-api/login => mock/login //// detail: https://cli.vuejs.org/config/#devserver-proxy //[process.env.VUE_APP_BASE_API]: { //target: `http://127.0.0.1:${port}/mock`, //changeOrigin: true, //pathRewrite: { //['^' + process.env.VUE_APP_BASE_API]: '' //} //} // }, // after: require('./mock/mock-server.js') }, configureWebpack: { // provide the app's title in webpack's name field, so that // it can be accessed in index.html to inject the correct title. name: name, resolve: { alias: { '@': resolve('src') } }, plugins: [ new webpack.ProvidePlugin({ $:"jquery", jQuery:"jquery", "windows.jQuery":"jquery" }), ], // 性能提醒 performance: { // 提醒方式 hints: "warning", // 文件大小峰值控制(单位:kb) maxAssetSize: 10000 }, externals: isNotDevelopMentEnv ? cdnData.externals : {} }, chainWebpack (config) { config.plugins.delete('preload') // TODO: need test config.plugins.delete('prefetch') // TODO: need test // 配置启用打包文件分析 if (isNotDevelopMentEnv) { // 开启图片压缩-使用异常,暂时注释 // config.module.rule('images') //.test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) //.use('image-webpack-loader') //.loader('image-webpack-loader') //.options({ bypassOnDebug: true }) // js文件包分析 if (process.env.npm_config_report) { config .plugin('webpack-bundle-analyzer') .use(BundleAnalyzerPlugin) .end() } // 配置html文件引入变量 config.plugin('html') .tap(args => { args[0].cdn = cdnData return args }) // gzip压缩 config.plugin('compression').use(CompressionPlugin, [ { algorithm: 'gzip', test: new RegExp('\\.(js|css)$'), threshold: 10240, //超过多少字节进行压缩 minRatio: 0.8 //至少压缩到原来体积的0.8,才会进行压缩 } ]) } // set svg-sprite-loader config.module .rule('svg') .exclude.add(resolve('src/icons')) .end() config.module .rule('icons') .test(/\.svg$/) .include.add(resolve('src/icons')) .end() .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) .end()// set preserveWhitespace config.module .rule('vue') .use('vue-loader') .loader('vue-loader') .tap(options => { options.compilerOptions.preserveWhitespace = true return options }) .end()config // https://webpack.js.org/configuration/devtool/#development .when(!isNotDevelopMentEnv, config => config.devtool('cheap-source-map') )config .when(isNotDevelopMentEnv, config => { config .plugin('ScriptExtHtmlWebpackPlugin') .after('html') .use('script-ext-html-webpack-plugin', [{ // `runtime` must same as runtimeChunk name. default is `runtime` inline: /runtime\..*\.js$/ }]) .end() config .optimization.splitChunks({ chunks: 'all', cacheGroups: { libs: { name: 'chunk-libs', test: /[\\/]node_modules[\\/]/, priority: 10, chunks: 'initial' // only package third parties that are initially dependent }, elementUI: { name: 'chunk-elementUI', // split elementUI into a single package priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm }, commons: { name: 'chunk-commons', test: resolve('src/components'), // can customize your rules minChunks: 3, //minimum common number priority: 5, reuseExistingChunk: true } } }) config.optimization.runtimeChunk('single') } ) } }

    推荐阅读