vue2旧项目 极速打包实践

背景

公司项目的体量较大,每次serve需要1分钟左右,build需要3分多钟,这是在电脑资源空闲时的速度,如果浏览器开了10几个标签啥的,更慢了。每次改点东西打包发测试环境都很难受。


项目技术栈

// package.json

{
	"dependencies": {
		"vue": "^2.6.10",
		"vuex": "^3.1.2",
		"vue-router": "^3.1.3",
		"core-js": "^3.4.3",
	},
	"devDependencies": {
	    "@vue/cli-plugin-babel": "^4.1.0",
	    "@vue/cli-plugin-router": "^4.1.0",
	    "@vue/cli-plugin-vuex": "^4.1.0",
	    "@vue/cli-service": "^4.1.0",
	    "vue-template-compiler": "^2.6.10"
  }
}

项目性能分析

打包耗时分析

// vue.config.js
module.exports = {
	chainWebpack: (config) => {
		 // 分析 webpack 的总打包耗时以及每个 plugin 和 loader 的打包耗时
	      config.plugin('speed-measure')
	        .use(require('speed-measure-webpack-plugin'))
	}
}

打包后文件体积分析

// vue.config.js
module.exports = {
	chainWebpack: (config) => {
		  // 体积分析
	      config.plugin('bundle-analyzer')
	        .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [{analyzerPort: 8880}])
	}
}

优化方向


查看项目的webpack配置

因为vue脚手架隐藏了默认的webpack配置文件,让你在vue.config.js中通过打补丁的方式修改配置。
优化的前提,必须要知道原来的配置和你修改后的配置是否生效,这就必须要把webpack配置文件的导出来


idea终端或者CMD终端中运行命令,会生成最终的webpack配置文件
开发环境:npx vue-cli-service inspect --mode development >> webpack.config.development.js
生产环境:npx vue-cli-service inspect --mode production >> webpack.config.production.js

在产生的 js 文件开头,添加:module.exports =,然后格式化即可查看。


DLL缓存

// vue.config.js

// dll缓存的依赖包
const DllConfig = require('./webpack.dll.config')

// 动态插入资源到html中
const AddAssetHtmlPlugin = require("add-asset-html-webpack-plugin");

module.exports = {
	configureWebpack: (config) => {
        // 缓存依赖包
	    config.plugins = config.plugins.concat([
	      ...Object.keys(DllConfig.entry).map(key => (
	        new webpack.DllReferencePlugin({
	          context: process.cwd(),
	          manifest: require(`./public/vendor/${key}-manifest.json`)
	        })
	      )),
	
	      // 将 dll 注入到 生成的 html 模板中
	      new AddAssetHtmlPlugin({
	        // dll文件位置
	        filepath: require("path").resolve("./public/vendor/*.js"),
	        // dll 引用路径
	        publicPath: "./vendor",
	        // dll最终输出的目录
	        outputPath: "./vendor",
	      }),
	    ]);
	}
}
// webpack.dll.config.js

const path = require("path");
const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

// dll文件存放的目录
const dllPath = "public/vendor";

module.exports = {
  entry: {
    // 需要提取的库文件

    vueMain: ['vue', 'vue-router', 'vuex'],
    vuePlugin: ['vue-print-nb', 'vuedraggable', 'vue-i18n'],
    element: ['element-ui'],
    dll: [
      "file-saver",
      "html2canvas",
      "jszip",
      "qrcodejs2",
      "socket.io-client",
      "viewerjs",
      "cropperjs",
      "laravel-echo",
      "nprogress",
      "tinymce"
    ],
  },

  output: {
    path: path.join(__dirname, dllPath),
    filename: "[name].dll.js",
    // vendor.dll.js中暴露出的全局变量名
    // 保持与 webpack.DllPlugin 中名称一致
    library: "[name]_[hash]",
  },

  plugins: [
    // 清除之前的dll文件
    new CleanWebpackPlugin(),

    // 设置环境变量
    new webpack.DefinePlugin({
      "process.env": {
        NODE_ENV: JSON.stringify("production"),
      },
    }),

    // manifest.json 描述动态链接库包含了哪些内容
    new webpack.DllPlugin({
      path: path.join(__dirname, dllPath, "[name]-manifest.json"),
      // 保持与 output.library 中名称一致
      name: "[name]_[hash]",
      context: process.cwd(),
    }),
  ],
};
// package.json

{
	"scripts": {
    	"dll": "webpack --progress --config ./webpack.dll.config.js",
	 },
}

DLL是把一些静态资源包提前打包好,放到public/vendor目录下,打包时跳过它们,直接在index.html中的script标签引用


缺点
有了splitChunks后,这个就不怎么好用了,DLL会把资源全量打包然后插入html,不能按需加载,splitChunks可以把重复使用的资源打包到一个chunk里,然后引用它的地址,做到按需导入和避免资源重复打包

最后
打包速度并没有提升多少,所以PASS


splitChunks分包

// vue.config.js

module.exports = {
	chainWebpack: (config) => {
		// 拆包
	    config.optimization.splitChunks({
	       chunks: "all",
	       minSize: 20000, // 允许新拆出 chunk 的最小体积,也是异步 chunk 公共模块的强制拆分体积
	       cacheGroups: {
	         libs: { // 第三方库
	           name: "chunk-libs",
	           test: /[\\/]node_modules[\\/]/,
	           priority: 10,
	           chunks: "initial", // 只打包初始时依赖的第三方
	         },
	         elementUI: { // elementUI 单独拆包
	           name: "chunk-elementUI",
	           test: /[\\/]node_modules[\\/]element-ui[\\/]/,
	           priority: 20 // 权重要大于 libs
	         },
		     components: { // 组件包
	            name: `chunk-components`,
	            test: /[\\/]components[\\/]/,
	            minChunks: 2,
	            priority: 1,
	            reuseExistingChunk: true
	          },
	         commons: { // 公共模块包
	           name: `chunk-commons`,
	           minChunks: 2,
	           priority: 0,
	           reuseExistingChunk: true
	         },
	       }
	     })
	}
}

拆包的目的是,分解体积很大的包,复用重复打包的文件,拆分不常改变的模块,更好的复用webpack的文件缓存,避免文件hash受到影响而使缓存失效


缺点
每次打包都要分析文件依赖,执行拆包的操作
自己一顿操作分包,还没webpack默认配置分的均匀,下面是我的分包和webpack默认的分包对比




最后
分包这个还是挑项目的,如果你的项目有什么特殊情况默认配置不能满足的话,或许有点用,但是大部分都不需要另外去配的,webpack团队想的肯定比我们要充分。
这个对打包速度也没有提升,PASS


图片压缩

// vue.config.js
module.exports = {
	chainWebpack: (config) => {
	    // 图片压缩
	    config.module.rule('images')
	           .use('image-webpack-loader')
	           .loader('image-webpack-loader')
	           .options({
	              disable: process.env.NODE_ENV === 'development', // 开发模式下调试速度更快
	            })
	           .end()
	}

缺点

  1. 因为另外引入loader处理,所以打包起来会更慢
  2. 后面开发基本都会把图片放包OSS上,或者用iconfont,不会在本地加图片,所以压缩意义不大

最后
对打包速度没提升,PASS


CDN加载文件

// vue.config.js

const cdn = {
  // 开发环境
  dev: {
    css: [],
    js: [],
  },
  // 生产环境
  build: {
    css: [
      // jsmind
      "./themes/jsmind.css"
      
      // element
      // "http://xx.css"
    ],
    js: [
      // 采用oss
      // [email protected]
      // "http://xx.js",
      
      // [email protected]
      "//xx.js",

      // [email protected]
      "//xx.js",
     
      // aliyun-oss-sdk-6.3.1.min.js
      "//xx.js",

      // xlsx
      "//xx.js",
      
      // [email protected]
      "//xx.js",
      
      // jsmind
      "//xx.js",

      // gojs
      "//xx.js",
    ],
  },
};

module.exports = {
	chainWebpack: (config) => {
		 // 注入cdn
	    config.plugin("html").tap((args) => {
	      args[0].cdn = cdn.build;
	      return args;
	    });
	},

	configureWebpack: (config) => {
		    // cdn预加载使用
		    // 告诉 webpack 这些依赖是外部环境提供的,在打包时可以忽略它们,就不会再打到 chunk-vendors.js 中
		    // e.g. 
			 config.externals = {
		      "echarts": "echarts",
		      "xlsx-style": "XLSX",
		      "ali-oss": "OSS",
		      "jsmind": "jsMind",
		      "gojs": "go",
		      "axios": "axios",
		      "xlsx": "ODS"
		    };
	}
}

这里是原来就有的,我不知道为什么一些上CDN,一些又不上,免得出问题背锅,还是不动的好

最后
PASS


happypack多线程处理

// vue.config.js

const HappyPack = require("happypack");
const os = require("os");
const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length}); // 开辟一个线程池,拿到系统CPU的核数,happypack 将利用所有线程编译

module.exports = {
	chainWebpack: (config) => {
	    // todo plugin的名字要唯一,不然后面同名的会覆盖
	    config.plugin('happypack-babel').use(HappyPack, [{
	      id: 'babel',
	      loaders: ['babel-loader'],
	      threadPool: happyThreadPool
	    }])

	    config.plugin('happypack-styles').use(HappyPack, [{
	      id: 'styles',
	      loaders: [{
	        loader: 'style-resources-loader',
	        options: {
	          patterns: [
	            './public/themes/default/commom/commonFunc.less',
	            './public/themes/global.less'
	          ]
	        }
	      }],
	      threadPool: happyThreadPool
	    }])

	 	const jsRule = config.module.rule('js')
	   	jsRule.uses.delete('thread-loader')
	    jsRule.uses.delete('babel-loader')
	    jsRule.use('happypack').loader('happypack/loader?id=babel')

	 	const oneOfsMap = config.module.rule("less").oneOfs.store;
	   	oneOfsMap.forEach(item => {
	      item.uses.delete('style-resources-loader')
	      item.use("happypack").loader('happypack/loader?id=styles')
    	})
	}
}


最后
重复打包,发现没什么效果,不知道是不是我的姿势不对,PASS


thread-loader 多线程处理

// vue.config.js

module.exports = {
	chainWebpack: (config) => {
		let originUse = config.module.rule('images').toConfig().use
	    let newLoader = { loader: 'thread-loader' }
	    originUse.splice(0, 0, newLoader)
	    config.module.rule('images').uses.clear()
	    config.module.rule('images').merge({ use: originUse })
	}
}

package.json.lock,vue/cli已经内置了thread-loader,
webpack.config.production.js,thread-loader默认被用来处理babel-loader


最后
webpack官网上的【thread-loader】支持的node版本是 >= 16.10.0,但是我们项目统一的node版本是 14.16.0,虽然用起来没报错,但好像也没什么效果,PASS


Gzip压缩

// vue.config.js

module.exports = {
	 configureWebpack: (config) => {
   		// gzip打包压缩
     	// todo nginx需要开启 gzip_static,才能返回前端打包好的.gz文件,不然默认在服务器压缩
     	config.plugins.push(
	       new CompressionWebpackPlugin({
	          filename: "[path][base].gz", // 压缩后的文件名
	          algorithm: "gzip",
	          test: /\.(js|css)(\?.*)?$/i, // 需要压缩的文件正则
	          threshold: 10240,            // 对10K以上的数据进行压缩
	          minRatio: 0.8,               // 只有压缩率小于这个值的资源才会被处理
	
	          // 是否删除原文件(.js),只保留压缩文件(.gz)
	          // 删除原文件后,打包上线 Uncaught SyntaxError: Unexpected token < 【参考 https://stackoverflow.com/questions/54082652/webpack-gzip-bundle-uncaught-syntaxerror-unexpected-token】
	          deleteOriginalAssets: false,
	        })
	      );
	}
}

打包时生成.gz结尾的文件,nginx服务器开启gzip压缩后,如果生产包上有同名的.gz结尾的文件,则直接返回前端压缩好的文件,不然用服务器资源来进行动态压缩生成.gz文件后返回给浏览器。
目的:减少用户请求资源的时间


最后
服务器只配置了gzip: on,没有配置gzip_static,所以服务器总是动态压缩,没有用前端打包好的文件,再者说前端压缩还要拖慢我打包的速度,PASS


hard-source-webpack-plugin

// vue.config.js

module.exports = {
	chainWebpack: (config) => {
	 	// 重复构建,使用缓存,加快构建
	    config.plugin('hard-source')
	           .use(require('hard-source-webpack-plugin'));
	}
}

这个插件第一次build时会在node_modules/.cache下面建缓存,后面重复打包可以读缓存,减少构建时间

最后
重复构建能从3分多到1分多,提升还是挺大的,但是改点东西再打包,又会久点,因为要重新建缓存,但都比原来快,【


升级脚手架

当大部分配置都不能有效提升时,直接升脚手架吧,版本的提升带来的效益是巨大的,毕竟人家是专业的

【vue/cli4 升级 vue/cli5】

cli5内置了webpack5,带来了文件【持久缓存】,解析文件时生成缓存,后续没有改变的文件直接读取快照,不重复执行编译,打包速度从3分多到20多秒,提升巨大


// vue.config.js
module.exports = {
	configureWebpack: (config) => {
	  config.cache = {
	    type: 'filesystem',
	    allowCollectingMemory: true
	  }
	}
}

最后
面向未来才是关键,新东西就是好用,旧项目也能焕发第二春

你可能感兴趣的:(性能优化,webpack,前端,vue.js)