webpack优化

1. production 模式 打包自带优化

  • tree shaking
    tree shaking 是一个术语, 通常用语打包时移出 JavaScript 中的未引用的代码(dead-code), 它依赖于 ES6 模块系统中 importexport的静态结构特性.
    require 是动态导入, 可以在if判断时进行导入, 所以如果使用require, webpack不能判断后面是不是还需要用到导入的模块, 无法执行 tree shaking的操作.
    开发时引入一个模块后, 如果只使用其中一个功能, 上线打包时只会把用到的功能打包进 bundle, 其他没用到的功能都不会打包进来, 可以实现最基础的优化.

  • scope hoisting
    scope hoisting的作用是将模块之间的关系进行结果推测, 可以让 Webpack 打包出来的代码文件更小, 运行的更快
    scope hoisting 的实现原理其实很简单: 分析出模块之间的依赖关系, 尽可能的把打散的模块合并到一个函数中去, 但前提是不能造成代码冗余.
    因此只有那些被引用了一次的模块才能被合并(ES6 import)
    由于 scope hoisting 需要分析出来模块之间的依赖关系, 因此源码必须采用 ES6 模块化语句, 不然它将无法生效.
    原因和 tree shaking一样.
    同样功能的插件有ModuleConcatenationPlugin, 默认在 production 模式下自动开启, 如果想要在其他模式下开启, 需要手动设置:

    	new webpack.optimize.ModuleConcatenationPlugin();
    
  • 代码压缩
    所有代码使用 UglifyJsPlugin插件进行压缩, 混淆.

2. CSS优化

2.1 将css提取到独立文件中
mini-css-extract-plugin 是用于将CSS提取为独立的文件的插件, 对每个包含css的js文件都会创建一个CSS文件, 支持按需加载css和sourceMap
只能用在webpack4中, 有如下优势:

  • 异步加载
  • 不重复编译, 性能很好
  • 容易使用
  • 只针对CSS

使用方法:

  1. 安装
    npm i -D mini-css-extract-plugin
  2. 在webpack配置文件中引入插件
	const MiniCssExtractPlugin = require('mini-css-extract-plugin')
  1. 创建插件对象, 配置抽离的css文件名, 支持placeholder语法
	new MiniCssExtractPlugin({
		filename: '[name].css'
	})
  1. 将原来配置的所有style-loader 替换为 MiniCssExtractPlugin.loader
	{
	test: /\.css$/,
	// webpack读取loader时, 是从右到左的读取, 会将css文件先交给最右侧的loader来处理
	// loader的执行顺序是从右到左以管道的放回寺链式调用
	// css-loader: 解析css文件
	// style-loader: 将解析出来的结果, 放到html中, 使其生效
	// use: ['style-loader', 'css-loader']
	use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
	},
	// { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']},
	{ test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']},
	// { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader']},
	{ test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']},

2.2 自动添加CSS前缀
使用postcss, 需要用到 postcss-loaderautoprefixer 插件

  1. 安装
    npm i -D postcss-loader autoprefixer
  2. 修改webpack配置文件中的loader, 将postcss-loader 放置在 css-loader 的右边 (调用链从右到左)
	{
	test: /\.css$/,
	// webpack读取loader时 是从右到左的读取, 会将css文件先交给最右侧的loader来处理
	// loader的执行顺序是从右到左以管道的方式链式调用
	// css-loader: 解析css文件
	// style-loader: 将解析出来的结果 放到html中, 使其生效
	// use: ['style-loader', 'css-loader']
	use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
	},
	// { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
	{ test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] },
	// { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
	{ test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] },
  1. 项目根目录下添加postcss的配置问阿金: postcss.config.js
  2. postcss的配置文件中使用插件
	module.exports = {
		plugins: [require('autoprefixer')]
	}

2.3 开启CSS压缩

需要使用optimize-css-assets-webpack-plugin 插件来完成css压缩

但是由于配置css压缩时会覆盖掉webpack默认的优化配置, 导致JS代码无法压缩, 所以还需要手动把JS代码压缩插件导入进来: terser-webpack-plugin

  1. 安装
    npm i -D optimize-css-assets-webpack-plugin terser-webpack-plugin
  2. 导入插件
	const TerserJSPlugin = require('terser-webpack-plugin')
	const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
  1. 在webpack配置文件中添加配置节点
	optimization: {
		minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
	},

tips: webpack4默认采用的JS压缩插件为: ugllifyjs-webpack-plugin, 在mini-css-extract-plugin 上一个版本中还推荐使用该插件, 但最新的v0.6中建议使用teser-webpack-plugin 来完成js代码压缩, 具体原因未在官网说明, 我们就按照最新版的官方文档来做即可.

3. js优化

Code Splitting是webpack打包时用到的重要的优化特性之一, 磁特性能够把代码分离到不同的bundle中, 然后可以按需加载或并行加载这些文件. 代码分离可以用于获取更小的bundle, 以及控制资源加载优先级, 如果使用合理, 会极大影响加载时间.

有三种常用的代码分离方法:

  1. 入口起点(entry points): 使用entry配置手动地分离代码
  2. 防止重复(prevent duplication): 使用SplitChunksPlugin 去重和分离 chunk.
  3. 动态导入(dynamic imports): 通过模块的内联函数调用来分离代码.

3.1 手动配置多入口

  1. 在webpack配置文件中配置多个入口
	entry: {
		main: './src/main.js',
		other: './src/other.js'
	},
	output: {
		// path.resolve(): 解析当前相对路径的绝对路径
		// path: path.resolve('./dist/'),
		// path: path.resolve(__dirname, './dist/'),
		path: path.join(__dirname, '..', './dist/'),
		// filename: '[name].bundle.js',
		publicPaath: '/'
	},
  1. 在main.js和other.js中都引入同一个模块, 并使用其功能
    main.js

    	import $ from 'jquery'
    	
    	$(function(){
    		$('<div></div>').html('main').appendTo('body')
    	})
    

    other.js

    	import $ from 'jquery'
    				
        $(function(){
    		$('<div></div>').html('other').appendTo('body')
    	})
    
  2. 修改package.json的脚本, 添加一个使用dev配置文件进行打包的脚本 (目的是不压缩代码检查打包的bundle时更方便)

    	"scripts": {
    		"build": "webpack --config ./build/webpack.prod.js",
    		"dev-build": "webpack --config ./build/webpack.dev.js"
    	}
    
  3. 运行 npm runn dev-build, 进行打包

  4. 查看打包后的结果, 发现other.bundle.js和main.bundle.js都同时打包了jQuery源文件
    在这里插入图片描述在这里插入图片描述

这种方法存在一些问题:

  • 如果入口 chunks 之间包含重复的模块, 那些重复的模块都会被引入到各个 bundle中.
  • 这种方法不够灵活, 并且不能间隔核心应用程序逻辑进行动态拆分代码.

3.2 抽取公共代码

tips: webpack v4以上使用的插件为SplitChunkPlugin, 以前使用的CommonsChunkPlugin 已经被移除了, 最新版的webpack只需要在配置文件中的optimization 节点下添加一个splitChunks 属性即可进行相关配置

  1. 修改webpack配置文件

    	optimization: {
    		splitChunks: {
    			chunks: 'all'
    		}
    	},
    
  2. 运行 npm run dev-build 重新打包

  3. 查看dist 目录
    在这里插入图片描述

  4. 查看vendors~main~other.bundle.js, 其实就是把都用到的jQuery打包到了一个单独的js中
    在这里插入图片描述

3.3 动态导入 (懒加载)

webpack4默认是允许import语法动态导入的, 但是需要babel的插件支持, 最新版babel的插件包为: @babel/plugin-syntax-dynamic-import, 以前老版本不是@babel 开头, 已经无法使用, 需要注意

动态导入最大的好处是实现了懒加载, 用到哪个模块才会加载哪个模块, 可以提高SPA应用程序的首屏加载速度, Vue, React, Angular框架的路由懒加载原理一样.

  1. 安装babel插件
    npm install -D @babel/plugin-syntax-dynamic-import

  2. 修改.babelrc配置文件, 添加@babel/plugin-syntax-dynamic-import 插件

    	{
    		"presets": ["@babel/env"],
    		"plugins": [
    			"@babel/plugin-proposal-class-properties",
    			"@babel/plugin-transform-runtime",
    			"@babel/plugin-syntax-dynamic-import"
    		]
    	}
    
  3. 将jQuery模块进行动态导入

    	function getComponent() {
    		return import('jquery').then((default: $) => {
    			return $('<div></div>').html('main')
    		})
    	}
    
  4. 给某个按钮添加点击事件, 点击后调用geetComponent函数创建元素并添加到页面

    	window.onload = function () {
    		document.getElementById('btn').onclick = function () {
    			getComponent().then(item => {
    				item.appendTo('body')
    			})
    		}
    	}
    

3.4 SplitChunksPlugin配置参数

webpack4之后, 使用SplitChunksPlugin 插件替代了以前CommonsChunkPlugin
SplitChunksPlugin 的配置, 只需要在webpack配置文件中的optimization 节点下的splitChunks 进行修改即可, 如果没有任何修改, 则会使用默认配置

默认的SplitChunksPlugin 配置适用于绝大多数用户
webpack会基于如下默认原则自动分割代码:

  • 公用代码块或来自 node_modules 文件夹的组件模块
  • 打包的代码块大小超过 30k (最小化压缩之前)
  • 按需加载代码块时, 同时发送的请求最大数量不应该超过5
  • 页面初始化时, 同时发送的请求最大数量不应该超过3

以下是 SplitChunksPlugin 的默认配置:

	module.exports = {
	  //...
	  optimization: {
	    splitChunks: {
	      // 只对异步加载的模块进行拆分, 可选值还有all | initial
	      chunks: 'async', 
	      // 模块最少大于30KB才拆分
	      minSize: 30000, 
	      // 模块大小无上限, 只要大于30KB都拆分
	      maxSize: 0, 
	      // 异步加载时同时发送的请求数量最大不能超过5, 超过5的部分不拆分
	      maxAsyncRequests: 5,
	      // 页面初始化时同时发送的请求数量最大不能超过3, 超过3的部分不拆分 
	      manxInitialRequests: 3, 
	      // 默认的连接符
	      automaticNameDelimiter: '~', 
	      // 拆分的chunk名, 设为true表示根据模块名和CacheGroup的key来自动生成,使用上面连接符连接
	      name: true, 
	      // 缓存组配置, 上面配置读取完成后进行拆分, 如果需要把多个模块拆分到一个文件, 就需要缓存, 所以命名为缓存组
	      cacheGroup: { 
	          // 自定义缓存组名
		      vendors: { 
		        // 检查node_modules目录, 只要模块在该目录下就是用上面配置拆分到这个组
		        test: /[\\]node_modules[\\/]/,
		        // 权重-10, 决定了哪个组优先匹配,例如node_modules下有个模块要拆分, 同时满足vendors和default组, 此时就会分到vendors组, 因为 -10 > -20 
		        priority: -10 
		      },
		      // 默认缓存组名
		      default: { 
		       // 最少引用两次才会拆分
		        minChunks: 2, 
		        // 权重-20
		        priority: -20, 
		        // 如果主入口中引入了两个模块, 其中一个正好也引用了后一个, 就会直接复用, 无需引用两次
		        reuseExistingChunk: true 
		      }
	      }
	    }
	  }
	};

4. noParse

在引入一些第三方模块时, 例如 jQuery、bootstrap等, 我们知道其内部肯定不会依赖其他模块, 因为最终我们用到的只是一个单独的js文件或css文件

所以此时如果 webpack 再去解析他们的内部依赖关系, 其实是非常浪费时间的, 我们需要阻止 webpack 浪费精力去解析这些明知道没有依赖的库

可以在 webpack配置文件的 module 节点下加上 noParse,并配置正则来确定不需要依赖关系的模块

	module: {
		noParse: /jquery|bootstrap/
	}

5. IgnorePlugin

在引入一些第三方模块时, 例如 moment, 内部会做 i18n国际化处理, 所以会包含很多语言包, 而语言包打包时会比较占用空间, 乳沟我们项目只需要用到中文, 或者少数语言, 可以忽略掉所有的语言包, 然后按需引入语言包从而使得构建效率更高, 打包生成的文件更小

需要忽略第三方模块内部依赖的其他模块, 只需要三步:

  1. 首先要找到moment依赖的语言包是什么
  2. 使用IgnorePlugin插件忽略其依赖
  3. 需要使用某些依赖时自行手动引入

具体实现如下:

  1. 通过查看moment的源码来分析:

    	function loadLocale(name) {
    		var oldLocale = null;
    		// TODO: Find a better way to register and load all the locales in Node
    		if (!locales[name] && (typeof module !== 'undefined') && module && module.exports) 					{
    			try {
    				oldLocale = globalLocale._abbr;
    				var aliasedRequire = require;
    				aliasedRequire('./locale/' + name);
    				getSetGlobalLocale(oldLocale);
    			} catch (e) {}
    		}
    		return locales[name];
    	}
    

    观察上方代码, 同时查看moment目录下确实有locale目录, 其中放着所有国家的语言包, 可以分析得出:
    locale目录就是moment所依赖的语言包目录

  2. 使用IgnorePlugin插件来忽略掉moment模块的locale目录
    在webpack配置我呢间中安装插件, 并传入配置项
    参数1: 表示要忽略的资源路径
    参数2: 要忽略的资源上下文 (所在哪个目录)
    两个参数都是正则对象

    	new webpack.IgnorePlugin(/\.\/locale/,/moment/)
    
  3. 使用moment时需要手动引入语言包, 否则默认使用英文

    	import moment from 'moment'
    	import 'moment/locale/zh-cn'
    	moment.locale('zh-CN')
    	console.log(moment().substract(6, 'days').calendar())
    

6. DllPlugin(动态链接库)

在引入一些第三方模块时,例如 vue,react,angular等框架,这些框架的文件一般都是不会修改的,而每次打包都需要去解析它们,也会影响打包速度,哪怕做拆分, 也只是提高了上线后用户访问速度, 并不会提高构建速度,所以如果需要提高构建速度, 应该使用动态链接库的方式, 类似于Windows中的dll文件.

借助DllPlugin插件实现将这些框架作为一个个的动态链接库,值构建一次, 以后每次构建都只生成自己的业务代码, 可以大大提高构建效率!

主要思想在于, 将一些不做修改的依赖文件, 提前打包, 这样我们开发代码发布的时候就不需要再对这部分代码进行打包, 从而节省了了打包时间.

涉及两个插件:

  1. DllPlugin
    使用一个单独webpack配置创建一个dll文件, 并且它还创建一个manifesst.json. DllReferencePlugin使用该json文件来做映射依赖性. (这个文件会告诉我们的哪些文件已经提取打包好了)

    配置参数:

    • context (可选): manifest文件中请求的上下文, 默认为该webpack文件上下文.
    • name: 公开的dll函数的名称, 和output.library保持一致即可.
    • path: manifest.json生成的文件夹及名字
  2. DllReferencePlugin
    这个插件用于主webpack配置, 它引用的dll需要预先构建的依赖关系

    • context: manifest文件中请求的上下文.
    • manifest: DllPlugin插件生成的manifest.json
    • content(可选): 请求的映射模块id(默认为manifest.content)
    • name(可选): dll暴露的名称
    • scope(可选): 前缀用于访问dll的内容
    • sourceType(可选): dll是如何暴露(libraryTarget)
将Vue项目中的库抽取成Dll
  1. 准备一份将Vue打包成DLL的webpack配置文件
    在build目录下新仙剑一个文件: webpack.vue.js
    配置入口: 将多个要做成dll的库全放进来
    配置出口: 一定要设置library属性, 将打包好的结果暴露在全局
    配置plugin: 设置打包后dll文件名和manifest文件所在地

    	const path = require('path')
    	const webpack = require('webpack')
    	module.exports = {
    	  mode: 'development',
    	  entry: {
    	    vue: [
    	      'vue/dist/vue.js',
    	      'vue-router'
    	    ]
    	  },
    	  output: {
    	    filename: '[name]_dll.js',
    	    path: path.resolve(__dirname, '../dist'),
    	    library: '[name]_dll'
    	  },
    	  plugins: [
    	    new webpack.DllPlugin({
    	      name: '[name]_dll',
    	      path: path.resolve(__dirname, '../dist/manifest.json')
    	    })
    	  ]
    	}
    
  2. webpack.base.js中进行插件的配置
    使用DLLReferencePlugin指定manifest文件的位置即可

    	new webpack.DllReferencePlugin({
    	  manifest: path.resolve(__dirname, '../dist/manifest.json')
    	})
    
  3. 安装add-asset-html-webpack-plugin

    npm i add-asset-html-webpack-plugin -D
    
  4. 配置插件自动添加script标签到HTML中

    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dist/vue_dll.js')
    })
    
将React项目中的库抽取成Dll
  1. 准备一份将React打包成DLL的webpack配置文件
    在build目录下新建一个文件: webpack.vue.js
    配置入口: 将多个要做成dll的库全放进来
    配置出口: 一定要设置library属性, 将打包好的结果暴露在全局
    配置plugin: 设置打包后dll文件名和manifest文件所在地

    const path = require('path')
    const webpack = require('webpack')
    module.exports = {
      mode: 'development',
      entry: {
        react: [
          'react',
          'react-dom'
        ]
      },
      output: {
        filename: '[name]_dll.js',
        path: path.resolve(__dirname, '../dist'),
        library: '[name]_dll'
      },
      plugins: [
        new webpack.DllPlugin({
          name: '[name]_dll',
          path: path.resolve(__dirname, '../dist/manifest.json')
        })
      ]
    }
    
  2. 在webpack.base.js中进行插件的配置
    使用DLLReferencePlugin指定manifest文件的位置即可

    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dist/manifest.json')
    })
    
  3. 安装add-asset-html-webpack-plugin

    npm i add-asset-html-webpack-plugin -D
    
  4. 配置插件自动添加script标签到HTML中

    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dist/react_dll.js')
    })
    

7. Happypack

在这里插入图片描述
由于webpack在node环境中运行打包构建, 所以是单线程的模式, 在打包众多资源时效率会比较低下, 早起可以通过 Happypack 来实现多进程打包. 当然, 这个问题只出现在低版本的webpack中, 现在的webpack性能已经非常强劲了, 所以无需使用Happypack也可以实现高性能打包

使用方法:

  1. 安装插件
    npm i -D happypack

  2. 在webpack配置文件中引入插件

    	const HappyPack = require('happypack')
    
  3. 修改loader的配置规则

    {
      test: /.js$/,
      use: {
    	loader: 'happypack/loader'
      },
      include: path.resolve(__dirname, '../src'),
      exclude: /node_modules/
    }
    
  4. 配置插件

    new HappyPack({
      loaders: ['babel-loader']
    })
    
  5. 运行打包命令
    npm run build

8. 浏览器缓存

在做了众多代码分离的优化后, 其目的是为了利用浏览器缓存, 达到提高访问速度的效果, 所以构建项目时做代码分割是必须的, 例如将固定的第三方模块抽离, 下次修改了业务代码, 重新发布上线不重启服务器, 用户再次访问服务器就不需要再次加载第三方模块了

但此时会遇到一个新的问题, 如果再次打包上线不重启服务器, 客户端会把以前的业务代码和第三方模块同时缓存, 再次访问时依旧会访问缓存中的业务代码, 所以会导致业务代码也无法更新

需要在output节点的filename中使用placeholder语法, 根据代码内容生成文件名的hash:

	output: {
	  // path.resolve(): 解析当前相对路径的绝对路径
	  // path: path.resolve('./dist/'),
	  // path: path.resolve(__dirname, './dist/'),
	  path: path.join(__dirname, '..', './dist/'),
	  // filename: 'bundle.js',
	  filename: '[name].[contenthash:8].bundle.js',
	  publicPath: '/'
	},

之后每次打包业务代码时, 如果有改变, 会生成新的hash作为文件名, 浏览器就不会使用缓存了, 而第三方模块不糊重新打包生成新的名字, 则会继续使用缓存

9. 打包分析 (bundle analysis)

项目构建完成后, 需要通过一些工具对打包后的bundle进行分析, 通过分析才能总结出一些经验, 官方推荐的分析方法有两步完成:

  1. 使用--profile --json 参数, 以json格式来输出打包后的 结果到某个指定文件中
    webpack --profile --json > stats.json

  2. 将stats.json文件放入工具中进行分析
    官方工具: official analyze tool
    官方推荐的其他四个工具:

    其中 webpack-bundle-analyzer是一个插件, 可以以插件的方式安装到项目中

10. Prefetching和Preloading

在优化访问性能时, 除了充分利用浏览器缓存之外, 还需要涉及一个性能指标: coverage rate(覆盖率)

可以再Chrome浏览器的控制台中按: ctrl + shift + p, 查找coverage, 打开覆盖率面板

开始录制后刷新网页, 即可看到每个js文件的覆盖率, 以及总的覆盖率在这里插入图片描述
想提高覆盖率, 需要尽可能多的使用动态导入, 也就是懒加载功能, 将一切能使用懒加载的地方都使用懒加载, 这样可以大大提高覆盖率

但有时候使用懒加载会影响用户体验, 所以可以在懒加载时使用魔法注释: Prefetching, 是指在首页资源加载完毕后 空闲时间时, 将动态导入的资源加载进来, 这样既可以提高首屏加载速度, 也可以解决懒加载可能会影响用户体验的问题, 一举两得!

function getComponent() {
  return import(/* webpackPrefetch: true */ 'jquery').then(({default: $ }) => {
	return $('<div></div>').html('我是main')
  })
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值