webpack优化
production 模块打包自带优化
tree shaking打包时移除JS中未引用的代码,依赖于import和export的静态结构特性scope hoisting将模块之间的关系进行结果推测,让打包出来的代码文件更小、运行更快- 代码压缩 所有代码使用
UglifyJsPlugin插件进行压缩、混淆
CSS 优化
mini-css-extract-plugin是用于将CSS提取为独立的文件的插件,对每个包含CSS的js文件都会创建一个CSS文件,支持按需加载CSS和sourceMap。异步加载,不重复编译,只针对CSS 安装 -D
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
new MiniCssExtractPlugin({
filename: '[name].css'
})
····
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader,'css-loader']
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader,'css-loader','sass-loader']
},
],
},
自动添加 CSS 前缀
安装postcss-loader autoprefixer
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader,'css-loader','postcss-loader']
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader,'css-loader','postcss-loader','sass-loader']
},
],
在根目录新建postcss.config.js
module.exports = {
plugins: [require('autoprefixer')],
}
css 压缩
安装optimize-css-assets-webpack-plugin terser-webpack-plugin
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
...
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},
js 代码分离
代码分离可以用于获取更小的bundle,以及控制资源加载优先级
- 使用
entry配置手动分离代码 - 使用
SplitChunksPlugin去重和分离chunk
optimization: {
splitChunks: {
chunks: 'all'
}
}
- 动态导入,使用模块的内联函数调用来分离代码,用到那个模块才会加载哪个模块,可以提高
SPA首屏加载速度webpack4默认允许import语法动态导入,但需要babel插件支持 安装@babel/plugin-syntax-dynamic-import,并加入.bablelrc配置文件添加改插件
function getComponent() {
return import('jquery').then(({ default: $ }) => {
return $('<div></div>').html('main')
})
}
window.onload = function () {
document.getElementById('btn').onclick = function () {
getComponent().then((res) => res.appendTo('body'))
}
}
splitChunksPlugin 默认配置
optimization: {
splitChunks: {
chunks: 'async', // 只对异步加载的模块进行拆分,可选值还有all | initial
minSize: 30000, // 模块最少大于30KB才拆分
maxSize: 0, // 模块大小无上限,只要大于30KB都拆分
minChunks: 1, // 模块最少引用一次才会被拆分
maxAsyncRequests: 5, // 异步加载时同时发送的请求数量最大不能超过5,超过5的部分不拆分
maxInitialRequests: 3, // 页面初始化时同时发送的请求数量最大不能超过3,超过3的部分不拆分
automaticNameDelimiter: '~', // 默认的连接符
name: true, // 拆分的chunk名,设为true表示根据模块名和CacheGroup的key来自动生成,使用上面连接符连接
cacheGroups: { // 缓存组配置,上面配置读取完成后进行拆分,如果需要把多个模块拆分到一个文件,就需要缓存,所以命名为缓存组
vendors: { // 自定义缓存组名
test: /[\\/]node_modules[\\/]/, // 检查node_modules目录,只要模块在该目录下就使用上面配置拆分到这个组
priority: -10 // 权重-10,决定了哪个组优先匹配,例如node_modules下有个模块要拆分,同时满足vendors和default组,此时就会分到vendors组,因为-10 > -20
},
default: { // 默认缓存组名
minChunks: 2, // 最少引用两次才会被拆分
priority: -20, // 权重-20
reuseExistingChunk: true // 如果主入口中引入了两个模块,其中一个正好也引用了后一个,就会直接复用,无需引用两次
}
}
}
}
noParse
在引入一些第三方模块时,我们知道其内部肯定不会依赖其他模块,此时再去解析他们的内部依赖关系是非常浪费时间的
module: {
noParse: /jquery/,
},
IgnorePlugin
在引入一些第三方模块时,内部会做i18n国际化处理,可以忽略语言包,然后按需引入 ① 查看源码,分析可得出locale目录就是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]
}
- 使用
IgnorePlugin插件来忽略掉moment模块的locale目录 参数 1 表示要忽略的资源路径,参数 2 要忽略的资源 context
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
- 按需引入语言包
import moment from 'moment'
import 'moment/locale/zh-cn'
moment.locale('zh-CN')
console.log(moment().subtract(6, 'days').calendar())
DllPlugin
在引入一些第三方模块时,vue/react/angular等框架文件一般都是不会修改的,而每次打包都需要去解析,会影响打包速度,做拆分只提高了上线后用户访问速度,并不会提高构建速度,应该使用动态链接库的方式,借助DllPlugin插件实现将这些框架座位一个个的动态链接库,只构建一次,以后每次构建都只生成自己的业务代码,主要将一些不做修改的依赖文件提前打包
vue/react 项目中的库抽取成 Dll
- 在 build 目录新建一个文件
webpack.vue.js或webpack.react.js配置入口:将多个要做成dll的库全放进来 配置出口:一定要设置library属性,将打包好的结果全暴露在全局 配置 plugin:设置打包后dll文件名和 manifest 文件所在地
const path = require('path')
const { webpack } = require('webpack')
module.exports = {
mode: 'produce',
entry: {
vue: ['vue/dist/vue.js', 'vue-router'],
// 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'),
}),
],
}
- 在
webpack.base.js中进行插件配置,使用DllReferencePlugin指定manifest文件的位置
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dist/manifest.json'),
})
- 配置插件自动添加
script标签到Html中,安装add-asset-html-webpack-plugin
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dist/vue_dll.js'),
})
happypack
由于webpack在node环境中运行打包构建,所以是单线程的模式,在打包众多资源时效率会比较低下,早期可以通过Happypack来实现多进程打包。当然,这个问题只出现在低版本的webpack中,现在的webpack性能已经非常强劲了,所以无需使用Happypack也可以实现高性能打包
安装happypack,引入插件修改配置规则,配置插件
const HappyPack = require('happypack')
...
{
test: /.js$/,
use: {
loader: 'happypack/loader'
},
include: path.resolve(__dirname, '../src'),
exclude: /node_modules/
}
...
new HappyPack({
loaders: [ 'babel-loader' ]
})
浏览器缓存
在做了众多代码分离的优化后,其目的是为了利用浏览器缓存,达到提高访问速度的效果,所以构建项目时做代码分割是必须的,例如将固定的第三方模块抽离,下次修改了业务代码,重新发布上线不重启服务器,用户再次访问服务器就不需要再次加载第三方模块了。如果再次打包上线不重启服务器,客户端会把以前的业务代码和第三方模块同时缓存,再次访问时依旧会访问缓存中的业务代码,所以会导致业务代码也无法更新,需要在output节点的filename中使用placeholder语法,根据代码内容生成文件名的hash,之后每次打包业务代码时,如果有改变,会生成新的hash作为文件名,浏览器就不会使用缓存了,而第三方模块不会重新打包生成新的名字,则会继续使用缓存
output: {
path: path.join(__dirname,'../dist/'),
filename: '[name].[contenthash:8].bundle.js',
publicPath: '/'
},
打包分析 安装webpack-bundle-analyzer -D
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin()
],
Prefetching 和 Preloading
在Chrome浏览器控制台ctrl+shift+p,查找coverage,可查看覆盖率
在懒加载时使用魔法注释Prefetching,在首页资源加载完毕后空闲时将动态导入的资源加载进来,这样可以提高首屏加载速度,也可以解决懒加载可能会影响用户体验的问题
function getComponent() {
return import(/* webpackPrefetch: true */ 'jquery').then(({ default: $ }) => {
return $('<div></div>').html('我是main')
})
}
