webpack各种优化
上一章节我们已经掌握了webpack常见的所有配置
这一节我们来看看如何实现webpack中的优化,我们先来编写最基本的webpack配置,然后依次实现各种优化!
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = mode => {
return {
mode: mode,
entry: "./src/main.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: "file-loader"
},
{
test: /\.js$/,
use: "babel-loader" // .babelrc已经配置支持react
},
{
test: /\.css$/,
use: [
mode !== "development"
? MiniCssExtractPlugin.loader
: "style-loader",
"css-loader"
]
}
]
},
plugins: [
new PurgecssPlugin({
paths: glob.sync(`${path.join(__dirname, "src")}/**/*`, { nodir: true }) // 不匹配目录,只匹配文件
}),
mode !== "development" &&
new MiniCssExtractPlugin({
filename: "css/[name].css"
}),
new HtmlWebpackPlugin({
template: "./src/template.html",
filename: "index.html"
})
].filter(Boolean)
};
};
.babelrc
配置文件
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
1.删除无用的Css样式
先来看编写的代码
import './style.css'
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(hello,document.getElementById('root'));
body{
background: red
}
.class1{
background: red
}
这里的
.class1
显然是无用的,我们可以搜索src
目录下的文件,删除无用的样式
const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');
// 需要配合mini-css-extract-plugin插件
mode !== "development" && new PurgecssPlugin({
paths: glob.sync(`${path.join(__dirname, "src")}/**/*`, { nodir: true }) // 不匹配目录,只匹配文件
}),
2.图片压缩插件
将打包后的图片进行优化
npm install image-webpack-loader --save-dev
在file-loader之前使用压缩图片插件
loader: "image-webpack-loader",
options: {
mozjpeg: {
progressive: true,
quality: 65
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.90, 0.95],
speed: 4
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75
}
}
可以发现图片大小是有了明显的变化
3.CDN加载文件
我们希望通过cdn的方式引入资源
const AddAssetHtmlCdnPlugin = require('add-asset-html-cdn-webpack-plugin')
new AddAssetHtmlCdnPlugin(true,{
'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
})
但是在代码中还希望引入jquery
来获得提示
import $ from 'jquery'
console.log('$',$)
但是打包时依然会将jquery
进行打包
externals:{
'jquery':'$'
}
在配置文件中标注jquery
是外部的,这样打包时就不会将jquery进行打包了
4.Tree-shaking && Scope-Hoisting
4.1 Tree-shaking
顾名思义就是将没用的内容摇晃掉,来看下面代码
main.js
import { minus } from "./calc";
console.log(minus(1,1));
calc.js
import {test} from './test';
export const sum = (a, b) => {
return a + b + 'sum';
};
export const minus = (a, b) => {
return a - b + 'minus';
};
test.js
export const test = ()=>{
console.log('hello')
}
console.log(test());
观察上述代码其实我们主要使用
minus
方法,test.js
代码是有副作用的!
默认mode:production
时,会自动tree-shaking
,但是打包后'hello'
依然会被打印出来,这时候我们需要配置不使用副作用
在package.json
中配置
"sideEffects":false,
如果这样设置,默认就不会导入css
文件啦,因为我们引入css也是通过import './style.css'
这里重点就来了,tree-shaking
主要针对es6模块,我们可以使用require
语法导入css,但是这样用起来有点格格不入,所以我们可以配置css
文件不是副作用
"sideEffects":[
"**/*.css"
]
在开发环境下默认tree-shaking
不会生效,可以配置标识提示
optimization:{
usedExports:true
}
4.2 Scope Hoisting
作用域提升,可以减少代码体积,节约内存
let a = 1;
let b = 2;
let c = 3;
let d = a+b+c
export default d;
// 引入d
import d from './d';
console.log(d)
最终打包后的结果会变成
console.log(6)
- 代码量明显减少
- 减少多个函数后内存占用也将减少
5.DllPlugin && DllReferencePlugin
每次构建时第三方模块都需要重新构建,这个性能消耗比较大,我们可以先把第三方库打包成动态链接库,以后构建时只需要查找构建好的库就好了,这样可以大大节约构建时间
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(hello
,document.getElementById('root'))
5.1 DllPlugin
这里我们可以先将
react
、react-dom
单独进行打包
单独打包创建webpack.dll.js
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
entry:['react','react-dom'],
mode:'production',
output:{
filename:'react.dll.js',
path:path.resolve(__dirname,'dll'),
library:'react'
},
plugins:[
new DllPlugin({
name:'react',
path:path.resolve(__dirname,'dll/manifest.json')
})
]
}
执行"webpack --config webpack.dll.js
命令,可以看到dll目录下创建了两个文件分别是manifest.json
,react.dll.js
关系是这个酱紫的,到时候我们会通过manifest.json
找到react.dll.js
文件中的模块进行加载
5.2 DllReferencePlugin
在我们的项目中可以引用刚才打包好的动态链接库
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
// 构建时会引用动态链接库的内容
new DllReferencePlugin({
manifest:path.resolve(__dirname,'dll/manifest.json')
}),
// 需要手动引入react.dll.js
new AddAssetHtmlWebpackPlugin(
{ filepath: path.resolve(__dirname,'dll/react.dll.js') }
)
使用DllPlugin可以大幅度提高构建速度
6.动态加载
实现点击后动态加载文件
let btn = document.createElement('button');
btn.innerHTML = '点击加载视频';
btn.addEventListener('click',()=>{
import('./video').then(res=>{
console.log(res.default);
});
});
document.body.appendChild(btn);
给动态引入的文件增加名字
output:{
chunkFilename:'[name].min.js'
}
import(/* webpackChunkName: "video" */ './video').then(res=>{
console.log(res.default);
})
这样打包后的结果最终的文件就是
video.min.js
7.打包文件分析工具
安装webpack-bundle-analyzer
插件
npm install --save-dev webpack-bundle-analyzer
使用插件
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
mode !== "development" && new BundleAnalyzerPlugin()
默认就会展现当前应用的分析图表
8.SplitChunks
我们在来看下SplitChunks这个配置,他可以在编译时抽离第三方模块、公共模块
将项目配置成多入口文件
entry:{
a:'./src/a.js',
b:'./src/b.js'
}
我们让a,b两个模块同时引用jquery
,别忘了去掉之前的externals
配置
配置SplitChunks
插件
默认配置在此,我一个个描述下含义
splitChunks: {
chunks: 'async', // 分割异步模块
minSize: 30000, // 分割的文件最小大小
maxSize: 0,
minChunks: 1, // 引用次数
maxAsyncRequests: 5, // 最大异步请求数
maxInitialRequests: 3, // 最大初始化请求数
automaticNameDelimiter: '~', // 抽离的命名分隔符
automaticNameMaxLength: 30, // 名字最大长度
name: true,
cacheGroups: { // 缓存组
vendors: { // 先抽离第三方
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20, // 优先级
reuseExistingChunk: true
}
}
}
我们将
async
改为initial
我们在为每个文件动态导入lodash
库,并且改成async
import('lodash')
为每个入口引入
c.js
,并且改造配置文件
splitChunks: {
chunks: 'all',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minSize:1, // 不是第三方模块,被引入两次也会被抽离
minChunks: 2,
priority: -20,
}
}
}
这样再反过来看
chunks
的参数是不是就了然于胸啦!
9.热更新
模块热替换(HMR - Hot Module Replacement)是 webpack 提供的最有用的功能之一。它允许在运行时替换,添加,删除各种模块,而无需进行完全刷新重新加载整个页面
- 保留在完全重新加载页面时丢失的应用程序的状态
- 只更新改变的内容,以节省开发时间
- 调整样式更加快速,几乎等同于就在浏览器调试器中更改样式
启用热更新,默认样式可以支持热更新,如果不支持热更新则采用强制刷新
devServer:{
hot:true
}
new webpack.NamedModulesPlugin(),
让js
支持热更新
import sum from './sum';
console.log(sum(1,2));
if(module.hot){ // 如果支持热更新
module.hot.accept(); // 当入口文件变化后重新执行当前入口文件
}
10.IgnorePlugin
忽略 import
和require
语法
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
11.费时分析
可以计算每一步执行的运行速度
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
module.exports =smw.wrap({
});
12.noParse
module.noParse
,对类似jq这类依赖库,内部不会引用其他库,我们在打包的时候就没有必要去解析,这样能够增加打包速率
noParse:/jquery/
13.resolve
resolve: {
extensions: [".js",".jsx",".json",".css"],
alias:{},
modules:['node_modules']
},
14.include/exclude
在使用loader
时,可以指定哪些文件不通过loader
,或者指定哪些文件通过loader
{
test: /\.js$/,
use: "babel-loader",
// include:path.resolve(__dirname,'src'),
exclude:/node_modules/
},
15.happypack
多线程打包,我们可以将不同的逻辑交给不同的线程来处理
npm install --save-dev happypack
使用插件
const HappyPack = require('happypack');
rules:[
{
test: /\.js$/,
use: 'happypack/loader?id=jsx'
},
{
test: /\.less$/,
use: 'happypack/loader?id=styles'
},
]
new HappyPack({
id: 'jsx',
threads: 4,
loaders: [ 'babel-loader' ]
}),
new HappyPack({
id: 'styles',
threads: 2,
loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
})
webpack.config.js
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const glob = require("glob"); // 主要功能就是查找匹配的文件
// 主要的作用删除无意义的css,只能配合 mini-css-extract-plugin
const PurgeCssWebpackPlugin = require("purgecss-webpack-plugin");
const AddCdnPlguin = require("add-asset-html-cdn-webpack-plugin");
const DLLReferencePlugin = require("webpack").DllReferencePlugin;
const AddAssetHtmlPlugin = require("add-asset-html-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
// 写个循环
const smw = new SpeedMeasureWebpackPlugin();
// ts-loader @babel/preset-react
module.exports = env => {
return smw.wrap({
mode: env,
// entry 有三种写法 字符串 数组 对象
entry: {
a: "./src/a.js",
b: "./src/b.js"
},
// 在生产环境下 将第三方模块进行抽离
optimization: {
splitChunks: {
// inital 只操作同步的 all 所有的 async 异步的
chunks: "all", // 默认支持异步的代码分割 import()
minSize: 30000, // 文件超过30k 我就会抽离他
maxSize: 0,
minChunks: 1, // 最少模块引用一次才抽离
maxAsyncRequests: 5, // 最多5个请求
maxInitialRequests: 3, // 最多首屏加载3个请求
automaticNameDelimiter: "~", // xxx~a~b
automaticNameMaxLength: 30, // 最长名字打大小
name: true,
cacheGroups: { // 缓存组
react: {
test: /[\\/]node_modules[\\/](jquery)|(lodash)/,
priority: -2
},
react: {
test: /[\\/]node_modules[\\/](react)|(react-dom)/,
priority: -2
},
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
commons: { // common~a~b
minChunks: 2,
minSize:1, // 如果公共代码 多一个字节就抽离
priority: -20,
reuseExistingChunk: true
}
}
}
},
// entry:'./src/index.js', // 入口
output: {
// 出口
filename: "[name].js", // 同步打包的名字
// chunkFilename: "[name].min.js",
path: path.resolve(__dirname, "dist") // 出口必须是绝对路径 都用绝对
},
// externals:{
// 'jquery':'$' // 不去打包代码中的jquery
// },
module: {
rules: [
{
test: /\.js/,
use: {
loader: "babel-loader",
options: {
// .babelrc
presets: ["@babel/preset-env", "@babel/preset-react"]
}
}
},
{
// 降低分辨率 清晰度
test: /\.(jpe?g|png|gif)/,
use: [
{
loader: "file-loader"
},
env !== "development" && {
// 可以在使用file-loader之前 对图片进行压缩
loader: "image-webpack-loader",
options: {
mozjpeg: {
progressive: true,
quality: 65
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false
},
pngquant: {
quality: [0.65, 0.9],
speed: 4
},
gifsicle: {
interlaced: false
},
// the webp option will enable WEBP
webp: {
quality: 75
}
}
}
].filter(Boolean)
},
{
test: /\.css$/,
use: [
// link
env !== "development"
? MiniCssExtractPlugin.loader
: "style-loader",
"css-loader"
]
}
]
},
// optimization:{
// usedExports:true // 使用了哪个模块你和我说一下
// },
plugins: [
env !== "development" && new MiniCssExtractPlugin(),
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "index.html"
// chunks:['a']
}),
// vue-cli 怎么配置 mpa cli官方 pages
// new HtmlWebpackPlugin({
// template:'./src/index.html',
// filename:'login.html',
// chunksSortMode:'manual', // 手动按照我的顺序来执行
// chunks:['b','a'] // 打包的顺序 按照我自己排列的
// }),
new PurgeCssWebpackPlugin({
paths: glob.sync("./src/**/*", { nodir: true })
}),
// 打包的时候 会配置 cleanwebpackplugin
// new DLLReferencePlugin({
// manifest: path.resolve(__dirname, "dll/manifest.json")
// }),
// new AddAssetHtmlPlugin({
// filepath: path.resolve(__dirname, "./dll/react.dll.js")
// }),
env !== "development" && new BundleAnalyzerPlugin()
// 当前这个dll.js没有在页面中引用
// 添加cdn的插件
// 分割代码
// new AddCdnPlguin(true,{
// 'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
// })
].filter(Boolean)
});
};
// 一般我们会用到开发 环境上 dll插件
package.json
{
"name": "base2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --env=development",
"dev:build": "webpack --env=development",
"build": "webpack --env=production",
"dll": "webpack --config webpack.dll.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"sideEffects": [
"**/*.css"
],
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0",
"add-asset-html-cdn-webpack-plugin": "^1.0.0",
"add-asset-html-webpack-plugin": "^3.1.3",
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"file-loader": "^4.2.0",
"glob": "^7.1.4",
"html-webpack-plugin": "^3.2.0",
"image-webpack-loader": "^6.0.0",
"mini-css-extract-plugin": "^0.8.0",
"purgecss-webpack-plugin": "^1.6.0",
"style-loader": "^1.0.0",
"webpack": "^4.40.2",
"webpack-bundle-analyzer": "^3.5.0",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1"
},
"dependencies": {
"jquery": "^3.4.1",
"lodash": "^4.17.15",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"speed-measure-webpack-plugin": "^1.3.1"
}
}