一、背景

目前AI云平台很多前端项目启动和编译都比较缓慢,不少项目每次启动都要1分钟以上,每次修改代码保存编译后也需要好几十秒,
严重影响开发效率,而且有些子系统首次打开也比较缓慢,急需优化解决;

二、分析

启动和编译比较缓慢主要是因为脚手架过老,很多项目的前端脚手架都是四五年前的老版本,比如都还是webpack4版本的脚手架,
webpack每次启动会重新生成一下编译文件,这部分编译文件重复生成,也会造成启动缓慢,页面打开缓慢的主要原因是包的体积
比较大,没有进行压缩,删除无用的代码,也没有按需去加载;

三、解决方案

1、添加缓存
2、脚手架升级 webpack4 -> webpack5
3、开启摇树优化
4、代码压缩(开启gzip压缩)

四、webpack5对比webpack4的优点

  1. 更高的Node.js版本要求:相比Webpack 4,Webpack 5对Node.js的版本要求更高,通常建议使用10.13以上版本,这有助于移除大量历史遗留代码。
  2. 性能优化:Webpack 5引入了持久化缓存,可以更快地重新构建应用程序,提高开发效率。
  3. 更精确的代码分割:Webpack 5可以更精确地分割代码,有助于减少页面加载时间。
  4. WebAssembly和Asset Modules支持:Webpack 5支持使用WebAssembly和Asset Modules来处理文件,使得构建更加灵活。
  5. 现代化的资源模块类型:Webpack 5支持对ES modules和JSON modules进行构建,使得支持现代浏览器的特性更加容易。
  6. 移除的插件和功能:为了提高构建性能和可维护性,Webpack 5移除了一些过时的插件和功能,例如UglifyJS和CommonsChunkPlugin

五、具体配置 (vue.config.js)

const {defineConfig} = require ('@vue/cli-service');
const TerserPlugin = require ('terser-webpack-plugin');
const path = require ('path');
const webpack = require ('webpack');
const CompressionPlugin = require ('compression-webpack-plugin');
const CopyPlugin = require ('copy-webpack-plugin');
const BundleAnalyzerPlugin = require ('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin;
const resolve = dir => path.join (__dirname, dir);
// const packageName = require ('./package.json').name;
const isDev = process.env.NODE_ENV == 'local';
const SpeedMeasurePlugin = require ('speed-measure-webpack-plugin');
const WebpackBar = require ('webpackbar');
module.exports = defineConfig ({
  productionSourceMap: isDev ? true : false, // 关闭生产环境的 source map
  publicPath: '/deeplearn/',
  // outputDir: 'deeplearn',
  lintOnSave: false,
  transpileDependencies: isDev ? false : true, //转译依赖
  chainWebpack: config => {
    if (!isDev) {
      config.plugins.delete ('prefetch');
      // 移除 preload 插件
      config.plugins.delete ('preload');
    }
    config.plugin ('html').tap (args => {
      args[0].title = '学习平台';
      return args;
    });
    // 兼容部分IE和位置iview BUG
    config.module
      .rule ('view-design')
      .test (/view-design.src.*?js$/)
      .use ('babel-loader')
      .loader ('babel-loader')
      .end ()
      .rule ('icons')
      .test (/.svg$/)
      .include.add (resolve ('src/assets/svg'))
      .end ()
      .use ('svg-sprite-loader')
      .loader ('svg-sprite-loader')
      .options ({
        symbolId: 'icon-[name]',
      })
      .end ();
    config.module
      .rule ('md')
      .test (/.md/)
      .use ('vue-loader')
      .loader ('vue-loader')
      .end ()
      .use ('vue-markdown-loader')
      .loader ('vue-markdown-loader/lib/markdown-compiler')
      .options ({raw: true});
    config
      .plugin ('speed-measure-webpack-plugin')
      .use (SpeedMeasurePlugin)
      .end ();
  },
  configureWebpack: config => {
    if (!isDev) {
      // config.entry = './src/main.js';
      // config.output = {
      //   clean: true, // 在生成文件之前清空 output 目录
      //   compareBeforeEmit: false, // 当在磁盘中已经存在有相同内容的文件时,webpack 将不会写入输出文件。
      //   filename: '[name].[contenthash].bundle.js', // wenpack打包后的文件名
      //   chunkFilename: 'js/[name].[contenthash].bundle.js', // 异步加载的模块
      //   path: path.join (__dirname, 'deeplearn'),
      //   publicPath: isDev ? '/' : '/deeplearn/',
      // qiankun接入配置
      // library: `${packageName}-[name]`,
      // libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      // chunkLoadingGlobal: `webpackJsonp_${packageName}`, //webpack5 output.jsonpFunction 更名为 output.chunkLoadingGlobal
      // };
      // config.plugins.push (new BundleAnalyzerPlugin ());
      config.plugins.push (new WebpackBar ({name: 'PC', color: '#07c160'}));
      config.plugins.push (
        new CompressionPlugin ({
          filename: '[path].gz[query]',
          minRatio: 0.8, // 最小压缩比率,官方默认0.8
          algorithm: 'gzip', // 默认压缩是gzip
          test: /.(js|css)$/, // 匹配文件名
          threshold: 10240, // 对超过10k的数据压缩
          deleteOriginalAssets: false, // 不删除源文件
        })
      );
    }
    config.plugins.push (
      new webpack.ProvidePlugin ({
        _: 'lodash',
        introJs: ['intro.js'],
      })
    );
    config.plugins.push (
      new CopyPlugin ({
        patterns: [{from: 'src/assets/static', to: 'static'}],
        options: {
          concurrency: 10,
        },
      })
    );
    config.resolve.alias =
      // 设置路径别名,设置后需保持jsconfig.json内一致
      {
        '@': resolve ('src'),
        _c: resolve ('src/components'),
      };
    config.resolve.extensions = ['.js', '.json', '.vue'];
    let minimizeConfig = {
      minimize: true,
      minimizer: [
        new TerserPlugin ({
          parallel: 4,
          cache: true,
          sourceMap: false,
          terserOptions: {
            compress: {
              drop_console: isDev ? false : true,
              drop_debugger: isDev ? false : true,
            },
            output: {
              comments: false,
            },
          },
        }),
      ],
      concatenateModules: false, // 公共代码整合,生产环境下被启用
    };
    config.optimization = {
      // runtimeChunk: true,
      usedExports: isDev ? false : true, //开启要数优化 tree shaking
      sideEffects: false,
      splitChunks: {
        chunks: 'all',
        minSize: 20000,
        minRemainingSize: 0,
        minChunks: 1,
        maxAsyncRequests: 30,
        maxInitialRequests: 30,
        enforceSizeThreshold: 50000,
        cacheGroups: {
          //公用模块抽离
          common: {
            chunks: 'initial',
            minSize: 0, //大于0个字节
            minChunks: 2, //抽离公共代码时,这个代码块最小被引用的次数
          },
          //第三方库抽离
          vendor: {
            priority: 1, //权重
            test: /node_modules/,
            chunks: 'initial',
            minSize: 0, //大于0个字节
            minChunks: 2, //在分割之前,这个代码块最小应该被引用的次数
          },
          default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,
          },
        },
      },
    };
    if (!isDev) {
      config.optimization = Object.assign (config.optimization, minimizeConfig);
    }
    config.watchOptions = {
      ignored: /node_modules/, //忽略node_modules包文件
      aggregateTimeout: 600, //多次修改批量更新
      poll: 1000, //每秒检查一次变动
    };
    config.devtool = isDev ? 'source-map' : false; //错误信息
    config.cache = {
      type: 'filesystem',
      allowCollectingMemory: true,
      cacheDirectory: path.resolve (__dirname, '.temp_cache'),
    };
  },
  css: {
    extract: isDev ? false : true,
    sourceMap: isDev ? true : false, // 设置为 true 之后可能会影响构建的性能。
    loaderOptions: {
      less: {
        javascriptEnabled: true,
      },
    },
  },
  pluginOptions: {
    'style-resources-loader': {
      preProcessor: 'less',
      patterns: [
        path.resolve (__dirname, './src/assets/it-uiue/css/theme.less'),
      ],
    },
  },
  devServer: {
    // compress: true, //影响启动速度 启用 gzip compression
    port: 8080, //端口号
    // hot: true,//热模块替换影响启动速度
    // liveReload: true,//当监听到文件变化时 dev-server 将会重新加载或刷新页面
    // open: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
    client: {
      overlay: false,
    },
    // proxy: {
    //   '/proxyApi': {
    //     target: 'http://172.30.209.77:30016/',
    //     // target: 'http://10.1.197.99:8080/',
    //     // target: 'http://172.30.209.77:30016/',
    //     // pathRewrite: {
    //     //   '^/proxyApi': ''
    //     // },
    //     // target: 'http://127.0.0.1:8080/pitaya/api/v1',
    //     changeOrigin: true,
    //     ws: true,
    //   },
    // },
    watchFiles: {
      paths: ['src/**/*'],
      options: {
        usePolling: false,
      },
    },
  },
});

六、效果对比

1、优化前构建速度

webpack4升级到webpack5

2、优化后构建速度

webpack4升级到webpack5

七、问题记录

1、webpack裸的配置升级为vue-cli5.0时,很多插件配置要带过来比如CopyPlugin、和ProvidePlugin,以防项目出现引用,出现资源引用不到的问题;

2、v-show用法与内联display:flex会有冲突,导致v-show会失效,不能隐藏dom元素,改为通过class类名的方式就可以了;

3、iview组件库,Button默认的外边距会消失,需要额外加上;