去年接手了公司一个项目,前一段时间,收到产品反馈说线上打开菜单非常的慢,点了几个列表页,网站就卡死了,再点击就没有反应了。用的 vue2.6.10 + webpack3 + element-ui2.15.7 项目很大,业务代码中使用了很多体积比较大的库,还有封装了大量的组件。
本地构建花费了 5 分钟,打包完 dist 文件有 16M 这么大,首屏加载大概有 7-8s,打开项目线上地址,我进去一看点了几个页面,果然出现产品和用户反馈的一样,卡顿明显,没几分钟,整个网站卡住了,刷新也还是卡。
那就开始整吧。
-
如何排查问题
-
使用 chrome 的开发者工具(打开网站,按 F12)
如何使用可看官网文档,这里不赘述了
- Performance
- Lighthouse
- Network 勾选 Preverse Log 保留日志,勾选 Disable Cache 屏蔽浏览器的接口缓存机制,No throtting 选择器 slow3G 可以对当前网络状态进行检测,查看接口的响应体积和顺序
-
npm run preview -- --report 来分析 webpack 打包之后的各个静态资源的大小。你可以发现占用空间最多的是第三方依赖,前提是安装了包 webpack-bundle-analyzer
-
-
接口慢
因为这部分需要后端同事协助,所以在我发现存在一部分接口没有分页,前端数据量很大,记录下接口地址,然后开会和后端负责人讨论改造工作。(工作需要及时的安排和协调,让前后端同事工作并行,效率会比较高。
经接口调整后,接口整个速度都更上去了,页面卡顿有所缓解 -
减少 HTTP 请求
-
升级 webpack 之前的项目结构
yarn upgrade webpack@5.37.0 yarn add webpack-dev-server webpack-cli -D npm-check-updates 一键升级所需的组件由于各种考虑 vue 和 element-ui 没有升级
// 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
- const merge = require('webpack-merge') + const { merge } = require('webpack-merge')新增 mode 选项
module.exports = { + mode: process.env.NODE_ENV, }+ 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 选项
module.exports = { + mode: 'development', // 'production', 'development' or '无 (none)' + externals: require('./cdn').externals }webpack.prod.conf.js
- 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
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
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 资源注入。
<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](https://tinify.cn/)将images拖进去,替换掉你的图片,尺寸大幅度压缩并保证质量。  - gzip压缩  ```js 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 渲染消耗
原文:xuxia2013.github.io/2022/08/31/…
欣赏此文?求鼓励,求支持!