老项目vue2+webpack3项目升级教程

去年接手了公司一个项目,前一段时间,收到产品反馈说线上打开菜单非常的慢,点了几个列表页,网站就卡死了,再点击就没有反应了。用的 vue2.6.10 + webpack3 + element-ui2.15.7 项目很大,业务代码中使用了很多体积比较大的库,还有封装了大量的组件。

本地构建花费了 5 分钟,打包完 dist 文件有 16M 这么大,首屏加载大概有 7-8s,打开项目线上地址,我进去一看点了几个页面,果然出现产品和用户反馈的一样,卡顿明显,没几分钟,整个网站卡住了,刷新也还是卡。

那就开始整吧。

如何排查问题

  • 使用 chrome 的开发者工具(打开网站,按 F12)

    如何使用可看官网文档,这里不赘述了

    https://developer.chrome.com/docs/devtools/evaluate-performance/

    • Performance

    • Lighthouse

    • Network

      勾选 Preverse Log 保留日志,勾选 Disable Cache 屏蔽浏览器的接口缓存机制,No throtting 选择器 slow3G 可以对当前网络状态进行检测,查看接口的响应体积和顺序

  • npm run preview – –report 来分析 webpack 打包之后的各个静态资源的大小。你可以发现占用空间最多的是第三方依赖,前提是安装了包 webpack-bundle-analyzer

接口慢

因为这部分需要后端同事协助,所以在我发现存在一部分接口没有分页,前端数据量很大,记录下接口地址,然后开会和后端负责人讨论改造工作。(工作需要及时的安排和协调,让前后端同事工作并行,效率会比较高。
经接口调整后,接口整个速度都更上去了,页面卡顿有所缓解

减少 HTTP 请求

升级 webpack

之前的项目结构

1
2
3
yarn upgrade webpack@5.37.0
yarn add webpack-dev-server webpack-cli -D
npm-check-updates 一键升级所需的组件

由于各种考虑 vue 和 element-ui 没有升级

1
2
3
4
5
6
7
8
9
// package.json

"scripts": {
- "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
+ "dev": "npx webpack serve --config build/webpack.dev.conf.js --color --progress",
"start": "npm run dev",
"build": "node --max_old_space_size=2048 build/build.js"
},

webpack.base.conf.js

1
2
- const merge = require('webpack-merge')
+ const { merge } = require('webpack-merge')

新增 mode 选项

1
2
3
module.exports = {
+ mode: process.env.NODE_ENV,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
+ const VueLoaderPlugin = require('vue-loader/lib/plugin');

...
cacheGroups 对 chunks 的拆分起着关键的作用。可以通过 cacheGroups 来定制 chunks 拆分策略, 由于我们项目中存在很多插件,我们这里都把它单独拆分出来,配合cdn使用

optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
elementUI: {
name: "chunk-elementUI",
priority: 20,
test: /[\\/]node_modules[\\/]element-ui[\\/]/
},
wangeditor: {
name: "chunk-wangeditor",
priority: 21,
test: /[\\/]node_modules[\\/]wangeditor[\\/]/,
},
vue: {
name: "chunk-vue",
priority: 20,
test: /[\\/]node_modules[\\/]vue[\\/]/
},
moment: {
name: "chunk-moment",
priority: 15,
test: /[\\/]node_modules[\\/]moment[\\/]/,
},
lodash: {
name: "chunk-lodash",
priority: 15,
test: /[\\/]node_modules[\\/]lodash[\\/]/,
},
axios: {
name: "chunk-axios",
priority: 15,
test: /[\\/]node_modules[\\/]axios[\\/]/,
},
idValidator: {
name: "chunk-idValidator",
priority: 15,
test: /[\\/]node_modules[\\/]id-validator[\\/]/,
},
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
minChunks: 4,
priority: 10,
reuseExistingChunk: true,
},
common: {
name: "chunk-common",
test: resolve("src/components"), // 可自定义拓展你的规则
minChunks: 4, // 最小共用次数
priority: 5,
reuseExistingChunk: true
}
}
},
runtimeChunk: {
name: 'runtime'
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')],
exclude: /node_modules/
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]'),
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000000, // 注意到这里我设置的很大,因为字体文件太大,大概28k,也采取转换成base64,减少http请求
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
plugins: [
  - new webpack.optimize.CommonsChunkPlugin('common.js'),
+ new VueLoaderPlugin(),
-   new webpack.optimize.CommonsChunkPlugin('common.js'),
-   new webpack.ProvidePlugin({
-    jQuery: "jquery",
- jquery: "jquery",
- "window.jQuery":"jQuery",
-    $: "jquery"
- }),
],
+ externals: require('./cdn').externals
...

webpack.dev.conf.js

新增 mode 选项

1
2
3
4
module.exports = {
+ mode: 'development', // 'production', 'development' or '无 (none)'
+ externals: require('./cdn').externals
}

webpack.prod.conf.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
- const ExtractTextPlugin = require('extract-text-webpack-plugin')
+ const MiniCssExtractPlugin = require("mini-css-extract-plugin")
- const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
// new UglifyJsPlugin({
// uglifyOptions: {
// compress: {
// warnings: false,
// drop_debugger: true,
// drop_console: true
// }
// },
// sourceMap: config.build.productionSourceMap,
// parallel: true
// }),
// extract css into its own file
new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),

// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
hash: version,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
cdn: require('./cdn').cdn,
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
// new webpack.optimize.CommonsChunkPlugin({
// name: 'vendor',
// minChunks (module) {
// // any required modules inside node_modules are extracted to vendor
// return (
// module.resource &&
// /\.js$/.test(module.resource) &&
// module.resource.indexOf(
// path.join(__dirname, '../node_modules')
// ) === 0
// )
// }
// }),
// // extract webpack runtime and module manifest to its own file in order to
// // prevent vendor hash from being updated whenever app bundle is updated
// new webpack.optimize.CommonsChunkPlugin({
// name: 'manifest',
// minChunks: Infinity
// }),
// // This instance extracts shared chunks from code splitted chunks and bundles them
// // in a separate chunk, similar to the vendor chunk
// // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
// new webpack.optimize.CommonsChunkPlugin({
// name: 'app',
// async: 'vendor-async',
// children: true,
// minChunks: 3
// }),

// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]

build/util.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const ExtractTextPlugin = require('extract-text-webpack-plugin')
- const ExtractTextPlugin = require('extract-text-webpack-plugin')
+ const MiniCssExtractPlugin = require("mini-css-extract-plugin")

function generateLoaders(loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]

if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}

// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return [MiniCssExtractPlugin.loader].concat(loaders)
// return ExtractTextPlugin.extract({
// use: loaders,
// publicPath: '../../',
// fallback: 'vue-style-loader'
// })
} else {
return ['vue-style-loader'].concat(loaders)
}
}

splitChunks 分离代码后,过大的插件被提取出来

合理使用缓存

  • 静态图片和字体尽量缓存


  • cdn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const isProduction = process.env.NODE_ENV === 'production';

module.exports = {
cdn: {
css: [
'https://unpkg.com/element-ui@2.15.9/lib/theme-chalk/descriptions.css',
],
js: [
// "https://unpkg.com/vue@2.6.10/dist/vue.min.js",
// "https://unpkg.com/element-ui@2.15.7/lib/index.js",
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
'https://unpkg.com/wangeditor@4.7.11/dist/wangEditor.min.js',
'https://unpkg.com/xlsx@0.15.6/dist/xlsx.full.min.js',
'https://unpkg.com/echarts@4.9.0/dist/echarts.min.js',
'https://map.qq.com/api/gljs?v=1.exp&key=BSVBZ-5XSCX-EMQ4D-TIK2X-SIE2T-E6FQ5',
// 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js',
// 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/locale/zh-cn.min.js'
],
},
externals: {
// vue: "Vue",
// "element-ui": "ElementUI",
vuex: 'Vuex',
wangeditor: 'wangEditor',
XLSX: 'xlsx',
echarts: 'echarts',
TMap: 'TMap',
// moment: "moment",
},
};

找到 public/index.html。将 js 和 css 资源注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<head>
<title>${process.env.APP_ENV_NAME}</title>
<!-- 引入样式 -->
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%=css%>">
<% } %>
</head>

<body>
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>"></script>
<% } %>

<!-- built files will be auto injected -->
<div id="app"></div>
</body>

文件压缩 - 图片压缩

免费的 web 端工具TinyPNG将 images 拖进去,替换掉你的图片,尺寸大幅度压缩并保证质量。

  • gzip 压缩

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if (config.build.productionGzip) {
    const CompressionWebpackPlugin = require('compression-webpack-plugin');

    webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
    asset: '[path].gz[query]',
    algorithm: 'gzip',
    test: new RegExp(
    '\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
    ),
    threshold: 100,
    minRatio: 0.8,
    })
    );
    }

    折腾到这里,构建速度 40 左右,dist 压缩到 4M。首屏加载控制在 1s。效果显著!

组件升级,支持虚拟加载

老项目中,使用 el-select,el-table 都不支持大数据虚拟加载,低版本存在卡顿现象
封装列表组件比较费时费力,版本 element-ui plus 支持虚拟加载。目前仅 keep-alive 来降低 dom 渲染消耗

推荐阅读

如何零成本搭建一个博客 搭建组件私有仓库

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×