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文件分析

2. 首屏加载情况(本地node服务器运行,模拟3G网络,后续一致

 

二、配置打包环境,使用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文件分析

2. 首屏加载情况

 

三、路由懒加载,按需引入

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文件分析

4.2 首屏加载情况

 

5. 路由懒加载分组

可查看 vue-router 懒加载分组   webpackChunkName相关知识

{
    path: '/login',
    component: () => import(/* webpackChunkName: "login" */ '@/views/default/Login')
}

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

 

四、CDN替换依赖包引入

1. 项目情况

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 = {
    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,生产环境才开放)

<html>
    <head>
        <!-- 样式文件优先加载 -->
        <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
            <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
        <% } %>
    </head>
    <body>
        <div id="app"></div>
        <!-- js加载 -->
        <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
            <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
        <% } %>
    </body>
</html>

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文件分析

6.2 首屏加载情况

 

五、查看首屏文件加载,细节分析

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. 项目情况

 

六、开启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文件

<Connector 
    port="8080" 
    protocol="HTTP/1.1"
    connectionTimeout="20000"

    compression="on"   
    compressableMimeType="text/html,text/xml,text/javascript,text/css,text/plain,application/javascript"
    useSendfile="false"/>
// compression="on" 打开压缩功能 
// compressableMimeType="text/html,text/xml" 压缩类型
// useSendfile="false" 设置该属性将会压缩所有文件,不限阙值,不然可能按照阙值部分压缩

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

 

4. 项目情况

4.1 js文件分析(注意切换到Gzipped模式查看,这个就是到时使用gzip后的文件大小)

4.2 首屏加载情况

 

七、开启图片压缩(插件地址在国外,使用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. 图片变化

压缩前

压缩后

 

七、优化结果视图分析

 

总结:在最后的打包分析看,还有很大优化空间,就是字体图标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.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 = {
    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')
                }
            )
    }
}
  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Z_pigeon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值