[Webpack] 打包优化 二
optimization
optimization
这个配置项内部的很多优化策略都是在生产环境默认开启的,启用这些规则从一定程序上来讲可以显著减少 bundle 体积,优化代码生成,帮助长期缓存等。
optimization.minimize
optimization.minimize
指定 webpack 默认使用 terser-webpack-plugin
来压缩 JS 代码,或者使用其它在optimization.minimizer
定义的插件。在生产环境下,这个配置项默认是true
。
optimization.minimizer
optimization.minimizer
指定一个插件数组,其中包含使用的插件配置项。
比如我们希望将css文件压缩,可以使用插件CssMinimizerWebpackPlugin
pnpm add css-minimizer-webpack-plugin -D
注意:该插件需要联合MiniCssExtractPlugin
一起使用才有意义
optimization: {
minimizer: [
new CssMinimizerPlugin(),
"..." //使用 '...' 来访问默认值,如果没有默认值terser就会失去意义
],
},
我们也可以自定义单独指定一个针对terser-webpack-plugin
的配置项,比如,默认webpack5使用terser打包之后会多出一些LICENSE
的txt文件。
如果希望去除这些文件,可以单独配置terser-webpack-plugin
const TerserPlugin = require('terser-webpack-plugin');
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin({
parallel: true, // 开启多进程并行压缩
terserOptions: {
format: {
comments: false, // 去掉注释
},
},
extractComments: false, // 不将注释提取到单独的文件中
}),
],
}
console.log删除
new TerserPlugin({
parallel: true, // 开启多进程并行压缩
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ["console.log", "console.error"]
},
format: {
comments: false, // 去掉注释
},
},
extractComments: false, // 不将注释提取到单独的文件中
}),
不过需要注意一个问题,删除
console.log
以及去掉注释,这些配置和devtool
的source-map
的一些配置产生冲突,比如:如果你设置为devtool: 'eval-cheap-module-source-map'
,那么TerserPlugin
的console.log
等删除以及注释的删除将不起作用,因为devtool: 'eval-cheap-module-source-map'
本身就会产生文件内的注释
performance
performance
是 webpack 内置的负责进行打包性能评估的配置,它提示当前项目内部可能需要进行优化的点,将提示信息输出在控制台。默认如果一个资源超过 250kb,webpack 会在打包的时候输出到控制台提示信息。
如果将performance
直接设置成false
,就可以禁用它。其他的的配置项还有:
performance.hints
:指定 webpack 是否在控制台打印性能提示信息,只有三个可选值:warning
:仅作为提示error
:把提示信息当成错误来输出false
:禁用性能提示信息
performance.maxEntrypointSize
:配置提示入口文件的大小的限制,默认是250000
字节,当入口文件超过这个限制就会显示性能提示performance.maxAssetSize
:当 webpack 生成的 bundle 等文件超过performance.maxAssetSize
指定的大小时,就会发出性能提示信息,默认是250000
字节。
cache
缓存生成的 webpack 模块和 chunk,来改善构建速度。cache
会在开发模式
默认开启被设置成 type: 'memory'
。
我们可以配置cache: filesystem
,来缓存生成的 webpack 模块和 chunk,改善构建速度。
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename], // 依赖的模块版本发生改变时重建缓存内容
},
},
文件默认生成在node_modules/.cache/webpack
,可以通过cacheDirectory
属性修改,不过一般不会改动
这样可以明显加快webpack5的构建速度
注意:如果构建的时候出现错误,特别是一些过期插件导致的问题。修改错误之后。建议手动删除.cache下的缓存文件,再重新进行构建。不然可能会由于读取缓存文件,导致继续出现之前的错误。
生产环境首次构建
生产环境生成cache之后
开发环境首次运行
开发环境生成cache之后
虽然webpack5 cache开发环境和生产环境都能生效,不过开发环境效果更好,如果生产环境是在CI/CD自动化持续集成/部署下开发。例如:项目使用Jenkins打包并构建镜像,cache将失去意义,毕竟每次构建镜像都相当于第一次。不过一般情况下,本机开发效果还是非常明显的。
模块解析规则优化
rule.include 和 rule.exclude
在使用 loader 的时候通过include
或者exclude
属性传入必要的路径和文件,避免全局匹配,缩小Loader对文件的搜索范围,避免不必要的转译,可以提升 webpack 构建的速度。
babel-loader
忽略node_modules
内部的模块
{
test: /\.m?jsx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
},
}
]
},
在匹配图片文件的时候,指定具体的文件夹
{
test: /\.(png|jpe?g|gif|webp|avif)(\?.*)?$/,
include: path.resolve(__dirname, '../src/assets/images'), //仅包含图片文件夹
type: "asset", // webpack5内置的通用资源处理模块,默认8kb以下的资源会被转换成base64位
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 小于8kb转base64资源内联
}
},
generator:{
filename:'images/[name].[contenthash:6][ext]', // 输出到images目录下
},
},
cache缓存副本
配置cache缓存Loader对文件的编译副本,很多Loader/Plugin
都提供一个可用编译缓存的选项,通过包括cache前缀,比如babel-loader
与eslint-webpack-plugin
// babel-loader
{
test: /\.m?jsx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
},
}
]
},
// eslint-webpack-plugin
new ESLintPlugin({
extensions: [
'.js',
'.jsx',
'.vue',
'.ts',
'.tsx'
],
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
Resovle.alias
配置别名,不单单可以简化模块引入,其实也可以定向指定所需文件路径,提高文件搜索速度
resolve: {
alias: {
'@': path.resolve(__dirname, '../src'), // @ 代表 src 目录
vue$: 'vue/dist/vue.runtime.esm-bundler.js'
}
},
Resovle.extensions
解析的文件类型列表,引入文件时不需要加后缀
resolve: {
// 引入文件时不需要加后缀。
// 这里只配置ts,js,json和vue, 其他文件引入都要求带后缀,可以稍微提升构建速度
extensions: ['.ts', '.js', '.json','.vue'],
},
thread-loader
thread-loader
是 webpack 团队提供的一个 webpack loader.
thread-loader
利用的是 nodejs
的worker pool
,也就是nodejs
的事件轮询机制;当使用 node
启动 webpack
打包程序的时候,webpack
主程序会运行在事件循环的主线程上,还有 worker pool
负责处理高成本的任务。
安装
pnpm add thread-loader -D
使用
在其它 loader 的配置前面添加thread-loader
,则它们将会在一个 worker pool
中运行。每个 worker 都是一个独立的 node.js 进程,开启 worker 本身会产生额外开销的,每一个 worker 都会产生大于600ms
的延迟。也就是说,如果构建时间小于600ms,开启worker pool
就没有意义了,得不偿失
module: {
rules: [
{
test: /\.m?jsx?$/,
exclude: /node_modules/,
use: [
{
loader: "thread-loader",
},
......
]
},
......
]
}
区分环境
在开发过程中,切忌在开发环境使用生产环境才会用到的工具,如在开发环境下,应该排除 [fullhash]
/[chunkhash]
/[contenthash]
等工具。
同样,在生产环境,也应该避免使用开发环境才会用到的工具,如 webpack-dev-server 等插件
const nodeEnv = process.env.NODE_ENV;
const isProduction = nodeEnv === 'production';
module.exports = {
entry: path.resolve(__dirname, '../src/index.ts'), // 入口文件
output: {
path: path.resolve(__dirname, '../dist'), // 打包后的目录
filename: isProduction ? 'js/[name].[chunkhash:6].js' : 'js/[name].js',
chunkFilename: isProduction ? 'js/[name].[chunkhash:8].js' : 'js/[name].chunk.js',
},
plugins: [
......
isProduction && new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "css/[name].[contenthash:6].css",
}),
]
}
作用提升
作用提升可以分析模块间的依赖关系,把打包好的模块合并到一个函数中,好处是减少函数声明与内存花销。
在webpack
中只需将打包环境设置为生产环境
就能让作用提升
生效,或显式设置concatenateModules
。
直接生产环境即可:
mode: "production"
or
显示设置:
optimization: {
concatenateModules:true,
}
合理配置 externals
externals
可以直接将某些模块在打包过程中剔除,这样减少 webpack 打包时候的工作量,从而加快构建速度。而且一般配置了externals
,我们都是和CDN配合使用。
externals: {
"vue": "Vue",
"vue-router": "VueRouter",
"element-plus": 'ElementPlus',
"@vueuse/core": "VueUse",
"echarts": "echarts",
"vue-echarts": "VueECharts",
}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/element-plus@2.3.12/dist/index.min.css"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vue-echarts@6.6.1/dist/csp/style.min.css">
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4.2.4/dist/vue-router.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@vueuse/shared@10.4.1/index.iife.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@vueuse/core@10.4.1/index.iife.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-plus@2.3.12/dist/index.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-echarts@6.6.1/dist/index.umd.min.js"></script>
这里配置的key
一般是代码导入语句中import xxx from 'vue'
,也就是from后面的值,value
其实就是引入CDN之后,挂载到window
上的对应的全局变量
注意:现实开发中,应该避免使用免费的CDN。
另外,externals的模块如果还和其他模块有关联,要么全部externals,要么就都不要,因为挂载到window上的对象在使用上和node_modules中的使用情况容易不一致
DllPlugin 动态链接库
DllPlugin
与 externals 的作用相似,都是将依赖抽离出去,节约打包时间。区别是 DllPlugin 是将依赖单独打包,这样以后每次只构建业务代码,而 externals 是将依赖转化为 CDN 的方式引入。并且同时还大幅度提升了构建的速度。"DLL" 一词代表微软最初引入的动态链接库
当公司没有很好的 CDN 资源或不支持 CDN 时,就可以考虑使用 DllPlugin ,替换掉 externals。
或者公司使用CI/CD持续集成/部署的时候,希望提升构建速度,可以考虑使用 DllPlugin
使用DllPlugin
大致分为四步:
1、创建定制生成dll文件的 webpack.dll.config.js
配置文件
const webpack = require("webpack");
const path = require("path");
module.exports = {
// 需要抽离的依赖
entry: {
vendor: [
"vue",
"vue-router",
"element-plus",
"echarts",
"@vueuse/core",
"vue-echarts",
],
},
mode: "production",
output: {
filename: "[name].dll.js", // 输出路径和文件名称
library: "[name]", // 全局变量名称,其他模块会从此变量上获取里面模块
path: path.resolve(__dirname, "../dll"), // 输出目录路径(这里定义的是根目录下的dll文件夹)
},
plugins: [
new webpack.DllPlugin({
name: "[name]", // 全局变量名称:减小搜索范围,与output.library结合使用
path: path.resolve(__dirname, "../dll/[name]-manifest.json"), // 输出目录路径
}),
],
};
2、配置 package.json
脚本
"scripts": {
......
"build:dll":"webpack -c build/webpack.dll.config.js"
},
3、使用 CopyPlugin
插件,将生成的dll相关文件拷贝到最终打包的目录
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../dll'), // 生成的dll文件夹
to: path.resolve(__dirname, '../dist/dll'), // 最终打包目录
toType: 'dir'
},
......
],
}),
4、使用 DllReferencePlugin
将打包生成的 dll 文件,引用到需要的预编译的依赖上来,并通过 html-webpack-tags-plugin
在打包时自动插入 dll 文件
安装插件
pnpm add html-webpack-tags-plugin -D
生产环境配置
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin');
const prodConfig = {
mode: 'production',
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname,'../dll/vendor-manifest.json') // manifest文件路径
}),
new HtmlWebpackTagsPlugin({
append: false, // 在生成资源后插入
publicPath: '/', // 使用公共路径
tags: ['dll/vendor.dll.js'] // 资源路径
}),
],
......
}
Tree shaking
Tree shaking 的作用:消除无用的 JS 代码,减少代码体积
export const targetType = (target) => {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
export const deepClone = (obj) => {
if(typeof obj !== 'object' || obj === null) {
return obj
}
const result = Array.isArray(obj) ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key])
}
}
return result;
}
项目中只使用了 targetType
方法,但未使用 deepClone
方法,项目打包后,deepClone
方法不会被打包到项目里
tree-shaking 原理
依赖于 ES6 的模块特性,ES6 模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是 tree-shaking 的基础
静态分析就是不需要执行代码,就可以从字面量上对代码进行分析。ES6 之前的模块化,比如 CommonJS 是动态加载,只有执行后才知道引用的什么模块,就不能通过静态分析去做优化,正是基于这个基础上,才使得 tree-shaking 成为可能
合理的使用 Tree shaking
并不是说所有无用的代码都可以被消除,还是上面的代码,换个写法 tree-shaking
就失效了
// util.js
export default {
targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
},
deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
};
// 引入并使用
import util from '../util';
util.targetType(null);
export default 导出的是一个对象,无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效
比如,我们在项目中引入lodash
和lodash-es
,同样都是引入debounce
函数的情况下,结果是完全不一样的。
pnpm add lodash lodash-es
pnpm add @types/lodash @types/lodash-es -D
// import {debounce} from 'lodash';
import {debounce} from 'lodash-es';
debounce(() => {
console.log('debounce')
}, 1000)();
lodash
是使用commonjs规范编写的,并且打包的规范是umd,简单来说,无论怎么样,如果我们引入了lodash,那么就会引入lodash中的全部内容。而lodash-es本身就是ES module模块规范编写的,这样就会很方便的帮助我们Tree shaking
图片的优化
平常大部分性能优化工作都集中在 JS 方面,但图片也是页面上非常重要的部分
特别是对于移动端来说,完全没有必要去加载原图,浪费带宽。如何去压缩图片,让图片更快的展示出来,有很多优化工作可以做
图片的动态裁剪
很多云服务,比如阿里云或七牛云,都提供了图片的动态裁剪功能,效果很棒,使用也非常简单,只需在图片的url地址上动态添加参数即可。唯一的确点就是要花钱。虽然有免费的次数,看着有好几百次免费,不过在现实的线上环境就是瞬间的事情...
图片瘦身前后对比:
瘦身前:310KB
http://image.yanhongzhi.com/record/2.jpg
瘦身后:16.6KB
http://image.yanhongzhi.com/record/2.jpg?imageMogr2/thumbnail/300x300
image-minimizer-webpack-plugin
如果希望自己在项目中自动优化图片大小,可以使用这个插件,由于需要压缩不同的图片,因此插件还需要依赖不同图片的压缩算法
pnpm add image-minimizer-webpack-plugin -D
pnpm add imagemin imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
......
optimization: {
minimize: true,
minimizer: [
......
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
],
}
Gzip 压缩
线上的项目,一般都会结合构建工具 webpack 插件和服务端配置 Nginx,来实现 http 传输的 gzip 压缩,目的就是把服务端响应文件的体积尽量减小,优化返回速度。
Gzip的设置,Nginx
服务器就能独立完成,这对于Nginx
服务器就是几行配置的问题,就能自动的将适合类型和大小的文件转换成Gzip。这就是所谓的Nginx动态压缩:都是普通文件,请求来了再压缩,然后返回给前端
当然,也可以前端或者后端人员,将文件提前压缩好,就保存在服务端,需要用的时候直接返回,这就避免了每次请求响应的时候都要压缩的问题,这个其实就是Nginx静态压缩:提前把文件压缩成 .gz 格式,请求来了,直接返回即可
对于前端来说,可以使用压缩插件:compression-webpack-plugin
pnpm add compression-webpack-plugin -D
const CompressionPlugin = require('compression-webpack-plugin');
......
plugins: [
......
new CompressionPlugin({
algorithm: "gzip", // 压缩算法,默认gzip,也可以是brotliCompress
test: /\.(js|css)(\?.*)?$/i, //需要压缩的文件正则
threshold: 1024, //文件大小大于这个值时启用压缩
deleteOriginalAssets: true //压缩后是否删除原文件
})
]
注意:但使用 gzip 压缩的最大错误之一,就是用 gzip 压缩页面中的每个资源文件。这里定义threshold比较小仅仅是为了测试需要
当然,压缩成gz
文件之后,我们不能再通过之前那样,使用简单的server服务,需要服务器对gz资源进行处理。这里封装了一个简单的服务器,为了方便处理gz
静态资源,使用了sirv
插件
pnpm add sirv -D
server/app.cjs
const http = require('http')
const path = require('path')
const fs = require('fs')
// 提供静态文件服务
const sirv = require('sirv')
const defaultWD = process.cwd()
const publicPath = path.join(defaultWD, 'dist')
const assets = sirv(publicPath, { gzip: true, brotli: true })
function createServer() {
const server = http.createServer()
server.on('request', (req, res) => {
assets(req, res, () => {
res.statusCode = 404
res.end('File not found')
})
})
server.listen(8080, () => {
const { port } = server.address()
console.log(`server run on http://localhost:${port}`)
})
}
function main() {
if (!fs.existsSync(publicPath)) throw new Error('Please check your\'re already run \'npm run build\'')
createServer()
}
main()
package.json
"scripts": {
......
"server": "node ./server/app.cjs"
},
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2023-03-02 [Typescript] Create a Type-Safe Request Handler with Zod and Express
2023-03-02 [Typescript] Typing a Function Composition with Overloads and Generics
2021-03-02 [SCSS] SCSS and CSS Variables
2020-03-02 [Github] Create a GitHub PR Template
2020-03-02 [Github] Create a GitHub Issue Template
2017-03-02 [SVG] Combine Multiple SVGs into an SVG Sprite
2017-03-02 [Ramda] Difference between R.converge and R.useWith