webpack性能优化(减少webpack打包时间,让webpack打出来的包更小)

本文介绍如何通过优化Loader、使用HappyPack、DllPlugin等方法减少Webpack打包时间,包括按需加载、ScopeHoisting、TreeShaking等高级技巧,以及代码压缩和减少文件体积的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

减少webpack打包时间
1.优化Loader
对于Loader来说,影响打包效率首当齐冲必属Babel了。因为babel会将代码转为字符串生成AST,然后对AST继续进行转变最后生成新的代码,项目越大,转化代码越多,效率就越低。当然了,我们是有办法优化的。

module.exports={
	module:[
		rules:[
			{
				//js文件才使用babel
				test:'\.js$',
				loader:'babel-loader',
				//只在src文件夹下查找
				include:[resolve('src')],
				//不会去查找路径
				exclude:/node_modules/
			}
		]
	}
}

对于Babel来说,我们肯定是希望只作用在JS代码上的,然后node_modules中使用的到吗都是编译过的,所以我们也完全没有必要再去处理一遍…
当然这样做还不够,我们还可以将Babel编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样我们可以大幅度加快打包时间。

loader:'babel-loader?cacheDirectory=true'

HappyPack
受限于Node是单线程运行的,所以Webpack在打包的过程中也是单线程的,特别是在执行Loader的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack可以将Loader的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了。

module:{
	loaders:[
		{
			test:/\.js$/,
			include:[resolve('src')],
			exclude:/node_modules/,
			//id后面的内容下面
			loader:'happypack/loader?id=happybabel'
		}	
	]
},
plugins:[
	new HappyPack({
		id:'Happybabel',
		loaders:['babel-loader?cacheDirectory'],
		//开启4个线程
		threads:4	
	})
]

DllPlugin
DllPlugin可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。

//单独配置在一个文件中
const path=require('path');
const webpack=require('webpack');
module.export={
	entry:{
		//统一打包类库
		vendor:['react'];	
	},
	output:{
		path:path.join(__dirname,'dist'),
		filename:'[name].dll.js',
		library:'[name]-[hash]'	
	},
	plugins:[
		new webpack.Dllplugin({
			name:'[name]-[hash]',
			context:__dirname,
			path:path.join(__dirname,'dist','[name]-manifest.json')	
		})	
	]
}

然后我们需要指向这个配置文件生成依赖文件,接下来我们需要使用DllReferencePlugin将依赖文件引入项目中

//webpack.conf.js
module.exports={
	plugins:[
		new webpack.DllRefercePlugin({
			context:__dirname,
			manifest:require('./dist/vendor-manifest.json'),
		})
	]
}

代码压缩
在webpack3中,我们一般使用UglifyJS来压缩代码,但是这个是单线程运行的,为了加快效率,我们可以使用webpack-parallel-uglify-plugin来运行UglifyJS,从而提交效率。
在webpack4中,我们就不需要以上操作了,只需要将mode设置为production就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不知可以压缩JS代码,还可以压缩HTML、CSS代码,并且在压缩JS代码的过程中,我们还可以通过配置实现比如删除console.log这类代码的功能。
一些小的优化点
reslove.extensions:用来表明文件后缀名列表,默认查找顺序是[’.js’,’.json’],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面。
resolve.alias:可以通过别名的方式来映射一个路径,能让webpack更快找到路径
module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让webpack不扫描该文件,这种方式碎玉大型类库很有帮助
减少webpack打包后的文件体积
按需加载
相比大家在开发SPA项目的时候,项目中都会在十几甚至更多的路由页面。如果我们将这些页面全部打包进入一个JS文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,我们肯定希望首页能加载文件体积越小越好,这时候我们就可以使用按序加载,将每个路由页面单独为一个文件,当然不仅仅路由可以按需加载,对于loadash这种大型类库同样可以使用这个功能。按需加载的底层机制都是当使用的时候再去下载的对应文件,返回一个Promise,当Promise成功以后去执行回调。
Scope Hoisting
Scope Hoisting会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
比如我们希望打包两个文件

//test.js
export const a=1
//index.js
import {a} from './text.js'

对于这种情况,我们打包出来的代码会类似这样

[
	/*0*/
	function(module,exports,require){
		//...	
	}
	/*1*/
	function (module,exports,require){
		//...	
	}
]

但是如果我们使用Scope Hoisting的话,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码

[
	/*0*/
	function (module,exports,require){
		//...
	}
]

这样的打包方式生成的代码明显比之前少很多,如果在webpack4中你希望开启这个功能,只需要启用optimization.concatenateModules就可以了。

module.exports={
	optimaization:{
		concatenateModules:true
	}
}

Tree Shaking
Tree Shaking可以实现删除项目中未被引用的代码,比如:

//test.js
export const a=1
export const b=2
//index.js
import {a} from './test.js'

对于以上情况,test文件中的变量b如果没有在项目中使用到的话,就不会被打包到文件中。如果你使用webpack4的话,开启生产环境就会自动启动这个优化功能。
打包工具的核心原理:
1.找出入口文件所有的依赖关系
2.然后通过构建CommonJS代码来获取exports导出的内容
可以实现以下两个功能的打包工具
1.将ES6转化为ES5
2.支持在JS文件中import CSS文件
首先安装一些Babel先关的工具

yarn add babylon babel-traverse babel-core babel-preset-env

接下来将这些工具引入文件中

const fs=require('fs');
const  path=requir('path');
const babylon=require('babylon');
const traverse= require('babel-traverse').default
const {transformFromAst}=require('babel-core');

首先先来实现如何使用Babel转化代码

function readCode(filePath){
	//读取文件内容
	const content=fs.readFileSync(filePath,'utf-8');
	//生成AST,抽象语法树,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
	const ast=babylon.parse(content,{
		sourceType:'module'
	})
	//寻找当前文件的依赖关系
	const dependencies=[];
	traverse(ast,{
		ImportDeclaration:({node})=>{
			dependencies.push(node.socure.value)	
		}	
	})
	//通过AST将代码转为ES5
	const {code}=transformFromAst(ast,null,{
		presets:['env']
	})
	return {
		filePath,
		dependencies,
		code	
	}
}

接下来需要实现一个函数,这个函数的功能有以下几点:
调用readCode函数,传入入口文件
分析入口文件的依赖
识别JS和CSS文件

function getDependencies(entry){
	//读取入口文件
	const entryObject=readCode(entry);
	const dependencies=[entryObject];
	//遍历所有文件依赖关系
	for(const asset of dependencies){
		//获取文件目录
		const dirname=path.dirname(asset.filePath);
		asset.dependencies.forEach(relativePath=>{
			//获得绝对定位
			const absolutePath=path.join(dirname,relativePath);
			if(/\.css$/.test(absolutePath)){
				const content=fs.readFileSync(absolutePath,'utf-8');
				const code=`
					const style=document.createElement('style');
					style.innerText=${JSON.stringify(content).replace(/\\r\\n/g,'')};
					document.head.appendChild(style);
				`;
				dependencies.push({
					filePath:absolutePath,
					relativePath,
					dependencies;[],
					code
				})
			}else{
				//JS代码需要继续查找是否有依赖关系
				const child=readCode(absolutePath);
				child.relativePath=relativePath;
				dependencies.push(child)
			}
		})
	}
	return dependencies
}

首先我们读取入口文件,然后创建一个数组,该数组的目的是存储代码中涉及到的文件
接下来我们遍历这个数组,一开始这个数组中只有入口文件,在遍历的过程中,如果入口文件有依赖其他的文件,那么就会被push到这个数组中
在遍历的过程中,我们先获得该文件对应的目录,然后遍历当前文件的依赖关系
在边路文件依赖过程中,首先生成依赖文件的绝对路径,然后判断当前文件CSS文件还是JS文件
如果是CSS文件的话,我们不用Babel去编译了,只需要读取CSS文件中的代码,然后创建一个style标签,将代码插入进标签并且放入head中即可
如果是js文件的话,我们还需要分析JS文件是否还有别的依赖关系
最后将读取文件后的对象push进数组中
现在我们已经获取到了所有的依赖文件,接下来就是实现打包的功能

function bundle(dependencies,entry){
	let modules='';
	dependencies.forEach(dep=>{
		const filePath=dep.relativePath||entry;
		modules+=`'${filePath}':(
			function (module,exports,require){${dep.code}}	
		),`	
	})
	//构建require函数,目的是为了获取模块暴露出来的内容
	const result=`
		(function (modules){
			function require(id){
				const module={exports:{}}
				modules[id](module,module.exports.require)
				return module.exports
			}	
		})({${modules}})
	`
	//当生成的内容写入到文件中
	fs.writeFileSync('./bundle.js',result)
}

这段代码结合Babel转化后的代码来看,这样大家就会能理解为什么需要这样写了

//entry.js
var _a=require('./a.js')
var _a2=_interopRequireDefault(_a);
function _interopRequireDefault(obj){
	return obj&&obj.__esModule?obj:{default:obj}
}
console.log(_a2.default);
//a.js
Object.defineProperty(exports,'__esModule',{
	value:true
})
var a=1;
exports.default=a;

首先遍历所有依赖文件,构建出一个函数参数对象
对象的属性就是当前文件的相对路径,属性值是一个函数,函数体是当前文件下的代码,函数接受三个参数module、exports、require
module参数对应CommonJS中的module
exports参数对应CommonJS中的module.export
require参数对应我们自己创建的require函数
接下来就是构造一个使用参数的函数,函数做的事情很简单,就是内部创建一个require函数,然后调用require(entry),也就是require(’./entry.js’),这样就会从函数中找到./entry.js对应的函数并执行,最后将导出的内容通过module.export的方式让外部获取到
最后再将打包出来的内容写入到单独的文件中
如果你对上面的代码的实现还有疑问的话,可以阅读下打包后的部分简化代码

;(function(module){
	function require(id){
		//构造一个CommonJS导出代码
		const module={exports:{}}
		//去参数获取文件对应的函数并执行
		modules[id](module,module,exports,require)
		return module.exports
	}
	require('./entry.js')
})({
	'./entry.js': function(module, exports, require) {
    // 这里继续通过构造的 require 去找到 a.js 文件对应的函数
    var _a = require('./a.js')
    console.log(_a2.default)
  },
  './a.js': function(module, exports, require) {
    var a = 1
    // 将 require 函数中的变量 module 变成了这样的结构
    // module.exports = 1
    // 这样就能在外部取到导出的内容了
    exports.default = a
  }
  // 省略
})

转载链接:https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5bdc775a6fb9a049f818b879

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值