概念理解
-
Tree-Shaking(摇树优化):基于 ESM 的静态结构特性,打包工具在编译阶段通过静态分析识别没有被引用的代码,并将这些未使用的代码剔除,从而减小打包体积。
-
ESM 和 CJS:
- ESM(ES Module) :ES6 模块规范,使用
import/export语法,依赖关系在编译阶段就能确定,支持 Tree-Shaking 优化。 - CJS(CommonJS) :Node.js 模块规范,使用
require/module.exports语法,依赖关系在运行时才确定,不支持 Tree-Shaking。
- ESM(ES Module) :ES6 模块规范,使用
-
原子 CSS :将样式拆分为最小粒度的原子类(如
m-4、p-2、text-center),通过组合这些原子类来构建样式。 -
代码压缩混淆:通过删除多余的空格、换行、注释,结合 Tree-Shaking 删除死代码,简化变量声明,以及混淆变量名来减小代码体积、提升加载性能,并降低源码可读性。
-
雪碧图(Sprite):将多个小图片合并成一张大图,通过 CSS 背景定位来显示不同的图片区域。可以减少 HTTP 请求次数,提升加载性能。
打包体积优化
1. 利用 Tree-Shaking 特性
基于 ESM 的静态结构特性,import/export 在编译阶段就能确定依赖关系,打包工具可以通过静态分析识别没有被引用的代码。
在编译阶段就能确定导入导出,并将多余代码剔除,不依赖运行时逻辑。
javascript
export const a = 1;
import { a } from "xxx";
- 静态依赖分析 :打包工具(Rollup、Webpack)构建阶段遍历代码构建模块依赖图,分析哪些
export导出的内容被其他模块通过import实际引用。未引用的内容,会被标记为未使用(unused)。 - 剔除死代码(摇树) :代码压缩阶段(摇树阶段 Tree-shaking),例如使用
Terser,打包工具会将标记为未使用的代码变量和函数直接删除,只保留被使用的代码,以此减少产物体积。
无法优化的场景:
import * from "xxx"命名空间导入:直接引入了整个模块的命名空间,构建工具无法确定使用了哪些内容。- 副作用代码 :
console.log、修改全局变量、原型链扩展的副作用代码无法直接被构建工具优化,因为构建工具无法确认影响范围,所以默认不会被移除。 - 使用动态导入/导出 :如
export default { func1, func2 },只能通过import xxx from "xxx"去访问属性,导出的对象属性在运行时访问,构建工具无法判断哪些内容被使用,所以无法优化。
CJS 为什么不支持动态导入【运行时确定依赖】:
CJS 模块规范指定,模块依赖关系在运行时才确定,导入导出依赖运行时逻辑。
- 动态导入导出无法确定,只能全量引入整个模块。
require()是函数调用,module.exports本身是普通对象,支持动态赋值。
javascript
module.exports = {};
// 可以动态修改
module.exports.func = () => {};
if (cond) {
module.exports.extra = extraFunc;
}
属性的访问和修改,在代码运行时才确定,执行 require() 的时候,才会拿到 module.exports 的运行时结果,但 Tree-Shaking 依赖编译时的静态依赖分析,所以在实现上 CJS 和 Tree-Shaking 无法兼容。
但 CJS 的运行时导入可以实现其他功能,例如:
- 运行时确定,因此支持动态参数(如
require("./" + filename))。 - 这里注意在 Node.js 环境下(Webpack 开发环境)require 本身有缓存机制,所以重新导入模块时,需要执行
delete require.cache[cacheModuleKey]去清空缓存。
这块推荐阅读 《NodeJS 深入浅出》去了解
2. Code Spliting 代码分割和多入口
可以通过多入口 entry,把不同业务模块/页面拆分为独立的 chunk。
javascript
// vue.config.js
module.exports = {
entry: {
// 适用于多页应用
app: "./src/pages/app/main.js",
admin: "./src/pages/admin/main.js",
},
output: {
filename: "[name].[contenthash].js", // 通过 contenthash 生成文件名,用于 Etag 强缓存更新
path: path.join(__dirname, "dist"),
clean: true, // 自动清理
},
};
可以使用 Webpack 内置的 SplitChunksPlugin 将多个模块共用的 Chunk 依赖提取为独立的 Chunk,避免重复打包。
提升公共模块的资源利用率。
javascript
// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
hidePathInfo: true, // 隐藏文件名中的路径信息(比如把"src_components_a.js"简化为"a.js")
chunks: "all", // 对哪些代码生效?all=所有代码(包括页面首次加载的和按需加载的)
minSize: 30000, // 拆分出的文件最小多大才会被拆分?30000字节(约30KB),太小的不拆
minChunks: 1, // 被引用1次就可能被拆分
maxAsyncRequests: 5, // 按需加载时,最多同时加载5个拆分出的文件(避免过渡拆分,影响加载效率)
maxInitialRequests: 3, // 页面首次加载时,最多加载3个拆分出的文件(避免过渡拆分请求过多,拖慢首屏)
automaticNameDelimiter: "~", // 文件名分隔符
name: true, // 自动生成 chunk 文件名
cacheGroups: {
// 第三方库(node_modules里的)
vendors: {
test: /[\\/]node_modules[\\/]/, // 只匹配"node_modules"文件夹里的文件(第三方库,比如vue、axios)
priority: -10, // 优先级-10(比default低,所以如果一个模块同时符合两个组,会优先进default)
automaticNamePrefix: "vendors", // 拆分出的文件名前缀是"vendors"(比如"vendors~xxx.js")
},
// 项目内公共模块拆分
default: {
minChunks: 2, // 一个模块被至少2个地方引用,才会被分到这个组
priority: -20, // 优先级 -20(数字越大越优先)
reuseExistingChunk: true, // 复用已拆分的 chunk
},
},
},
},
},
};
结合 webpack-plugin-report 或 vite-plugin-visualizer 来进一步分析模块的打包情况。
3. 代码压缩混淆
使用 TerserPlugin 来完成项目的压缩混淆处理(Vue-cli 和 Vite 默认集成)
- 减小代码体积:删除多余的空格、换行、注释,结合 Tree-Shaking 删除死代码,简化变量声明。
- 提升加载性能:通过压缩减小代码体积,可以提升浏览器解析和编译 JS 的时间。
- 保护源码 :本质是通过随机变量名和简化降低源码的可读性,如果有很强的代码混淆和反调试需求,可以用
javascript‑obfuscator,但是会增大代码体积,降低执行性能,具体要根据业务考虑权衡,记住所有的加密手段都是为了加大破解难度,而不是避免被破解。(这里记得生产环境关闭source-map,否则 DevTool 还是能预览到你原有的源码结构)
javascript
// vite.js
import { defineConfig } from "vite";
export default defineConfig({
build: {
sourcemap: false, // 关闭生产环境 source map
minify: "terser", // 使用 Terser 压缩
terserOptions: {
compress: {
drop_console: true, // 移除所有 console 语句
drop_debugger: true, // 移除所有 debugger 语句
},
mangle: {
enabled: true, // 混淆变量名
reserved: ["$", "Vue"], // 不混淆特定变量名
},
output: {
comments: false, // 移除注释(生产环境建议关闭)
// 若需保留特定注释,可使用正则:
// comments: /@license|@preserve/
},
},
},
});
// vue.config.js
module.exports = {
productionSourceMap: false, // 关闭生产环境 source map
configureWebpack: {
optimization: {
minimizer: [
// 配置 Terser 插件
new (require("terser-webpack-plugin"))({
terserOptions: {
// 压缩选项
compress: {
drop_console: true, // 生产环境删除 console.log
drop_debugger: true, // 删除 debugger
},
// 混淆选项(默认开启,可关闭)
mangle: {
enabled: true, // 是否混淆变量名(默认 true)
reserved: ["$", "Vue"], // 不混淆的变量名(如 Vue 实例、$ 符号)
},
// 保留注释(可选)
output: {
comments: false, // 是否保留注释(默认 false,生产环境建议关闭)
// 若需要保留特定注释(如 @license),可改为:
// comments: /@license|@preserve/
},
},
// 是否启用多进程压缩(加快打包速度,默认开启)
parallel: true,
}),
],
},
},
};
4. CSS 压缩
cssnano(postcss 插件)
用于压缩 CSS,移除空格、注释、多余声明等。
- 第三方库压缩:
npx postcss input.css > output.css。 - 使用
CssMinimizerWebpackPlugin内置 cssnano 优化压缩。
javascript
// vue.config.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
configureWebpack: {
optimization: {
// 生产环境启用压缩
minimizer: [
// 添加 CSS 压缩插件
new CssMinimizerPlugin({
// 压缩配置
minimizerOptions: {
preset: [
"default",
{
discardComments: { removeAll: true }, // 移除所有注释
mergeLonghand: false, // 禁用长属性合并(避免某些场景下的样式问题)
cssDeclarationSorter: false, // 禁用属性排序(保持源码顺序)
},
],
},
// 多进程压缩(加快打包速度)
parallel: true,
}),
],
},
},
};
CSS 原子化
结合原子 CSS 例如 TailwindCSS、WindiCSS、UNOCSS 进行优化。
- CSS 原子化可以减少额外 CSS 编写。
- 编译阶段自动去掉未使用的类(Tree-Shaking CSS)。
- 通过按需引入 CSS 原子类,减少无用样式,以此减少文件体积和占用。
UNOCSS (推荐,灵活)
UNOCSS 本身是编译型原子 CSS 引擎,没有预设的原子 CSS 类,需要安装预设原子 CSS 插件(例如 Tailwind 预设),以及手动在 unocss.config.js 文件中预设自己的 CSS 规则。
javascript
// 1.框架配置
// vue.config.js
const UnoCSS = require("@unocss/webpack").default;
module.exports = {
plugins: [UnoCSS()],
configureWebpack: {
optimization: {
realContentHash: true,
},
},
};
// vite.config.js
import Unocss from "unocss/vite";
export default defineConfig({
plugins: [Unocss()],
});
// 2. 配置文件支持 Tailwind4 CSS
// uno.config.js
import { defineConfig } from "unocss";
import presetWind4 from "@unocss/preset-wind4";
export default defineConfig({
presets: [presetWind4()],
// 支持动态规则
rules: [
[/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
[/^p-(\d+)$/, (match) => ({ padding: `${match[1] / 4}rem` })],
],
});
// 3. 文件入口配置(通常是 main.js)
// webpack
import "uno.css";
// vite
// 解析虚拟模块,触发 unocss 插件去扫描项目文件,收集类名和规则,输出按需生成的 CSS 内容
import "virtual:uno.css";
TailwindCSS(主流,稳定)
TailwindCSS 是目前使用较多的预设原子 CSS 框架,用于快速开发,目前已经支持 JIT (Just-In-Time)模式,按需生成使用的类,控制生产环境体积。
javascript
// tailwind4
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});
// 公共 CSS 文件中
@import "tailwindcss";
WindiCSS:
在过去作为 TailwindCSS 的按需引入 CSS 原子框架,目前使用较少。
4.图片压缩
图片资源过大会占用首屏加载的性能,可以通过插件在生产环境下对图片进行压缩。
普通图片压缩
- 使用
image-webpack-loader进行压缩(Vite 使用vite-plugin-imagemin)。 - 配合
url-loader或asset类型处理小图片转 base64。
javascript
// vue.config.js
module.exports = {
chainWebpack: (config) => {
// 针对图片文件(png/jpg/jpeg/gif/svg)添加 image-webpack-loader
const imagesRule = config.module.rule("images");
// 清除原有的 loader(避免重复配置)
imagesRule.uses.clear();
// 重新配置 loader,先通过 url-loader 处理,再用 image-webpack-loader 压缩
imagesRule
.test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
.use("url-loader") // 先判断图片大小,小图片转 base64
.loader("url-loader")
.options({
limit: 10240, // 10KB 以下的图片转 base64(减少请求)
name: "img/[name].[hash:8].[ext]", // 超过 10KB 的图片输出路径
})
.end()
.use("image-webpack-loader") // 对图片进行压缩
.loader("image-webpack-loader")
.options({
// 压缩配置(根据需要调整)
mozjpeg: { quality: 80 }, // JPG 压缩,quality 0-100(越高质量越好)
optipng: { enabled: false }, // PNG 压缩,enabled: false 禁用
pngquant: { quality: [0.6, 0.8] }, // PNG 压缩,quality 范围 0-1
gifsicle: { interlaced: false }, // GIF 压缩,interlaced 开启交错扫描
webp: { quality: 80 }, // 自动将 JPG/PNG 转为 WebP(可选)
})
.end();
},
};
// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import viteImagemin from "vite-plugin-imagemin";
export default defineConfig({
plugins: [
vue(),
viteImagemin({
gifsicle: {
optimizationLevel: 7,
interlaced: false,
},
optipng: {
optimizationLevel: 7,
},
mozjpeg: {
quality: 80,
},
pngquant: {
quality: [0.6, 0.8],
speed: 4,
},
svgo: {
plugins: [{ removeViewBox: false }, { removeEmptyAttrs: true }],
},
webp: {
quality: 80,
},
}),
],
});
SVG 压缩 + 雪碧图
svg-sprite-loader + svgo-loader:icon-svg 优化。
- 基于
svg-sprite-loader实现雪碧图效果,发送一次请求所有 SVG 图标,减少额外请求。 - 基于
svgo-loader压缩 SVG 图片(image-webpack-plugin和vite-plugin-imagemin都集成了svgo-loader)。
javascript
module.exports = {
// ...
module: {
rules: [
{
test: /\.svg$/,
type: "asset",
loader: "svgo-loader",
},
],
},
};
5. 其他/第三方插件
- lodash-webpack-plugin:lodash 导入优化,或使用 lodash-es。
- Babel plugin component 和 unplugin-vue-components/resolvers + unplugin-auto-import:优化 Element 按需引入。
json
// .babelrc
{
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
javascript
// 手动处理
import Vue from "vue";
import { Button, Select } from "element-ui";
import App from "./App.vue";
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
new Vue({
el: "#app",
render: (h) => h(App),
});
javascript
// vite.config.js
import { defineConfig } from "vite";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
// ...
plugins: [
// ...
// 还支持 vue、vue-router、pinia 的按需引入支持
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
});
- ECharts 基于 ESM 方式按需引入:
javascript
// utils/chart.js
import * as echarts from "echarts/core";
import { LineChart } from "echarts/charts";
import { TitleComponent, TooltipComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
// 注册必须的图表类型和组件
echarts.use([LineChart, TitleComponent, TooltipComponent, CanvasRenderer]);
// 默认导出已注册的 echarts 实例
export default echarts;
- lodash-webpack-plugin:lodash 导入优化,或使用 lodash-es 通过 ESM 支持 Tree-Shaking。
- dayjs 和 moment :日期库。
- 使用 dayjs 体积占用小,moment.js 占用体积大,可以通过
moment-locales-webpack-plugin剔除无用的多语言内容。 - 注意 dayjs 小日期解析速度快,大日期计算速度慢于 moment,严重的时候甚至有 2 倍差距,如果有极致的性能要求,建议按场景使用。
- 使用 dayjs 体积占用小,moment.js 占用体积大,可以通过
javascript
// webpack.config.js
const MomentLocalesPlugin = require("moment-locales-webpack-plugin");
module.exports = {
plugins: [
// 或者:仅保留 "en", "es-us" 和 "ru" 的本地化文件
new MomentLocalesPlugin({
localesToKeep: ["zh-cn"],
}),
],
};
总结
-
核心优化策略:
- Tree-Shaking:基于 ESM 的静态结构特性,在编译阶段通过静态分析识别并剔除未使用的代码。
- 代码分割:通过多入口和 SplitChunksPlugin 将不同业务模块拆分为独立的 chunk,提取公共依赖,提升资源利用率。
- 代码压缩混淆:使用 TerserPlugin 删除多余空格、换行、注释,结合 Tree-Shaking 删除死代码,混淆变量名。
- CSS 优化:使用 cssnano 压缩 CSS,结合原子 CSS 框架(TailwindCSS、UNOCSS)实现按需生成和 Tree-Shaking CSS,减少无用样式。
- 图片优化 :使用
image-webpack-loader或vite-plugin-imagemin压缩图片,小图片转 base64,使用 SVG 雪碧图减少请求次数。 - 第三方库优化 :使用 ESM 版本的库(如
lodash-es),通过按需引入插件优化组件库(如 Element Plus),使用轻量级替代方案(如 dayjs 替代 moment)。
-
注意事项:
- Tree-Shaking 只对 ES Module 有效,CommonJS 不支持,第三方库优先选择 ESM 版本,支持 Tree-Shaking 优化。
- 代码分割要适度,避免过度分割导致 HTTP 请求增加。
- 生产环境关闭 source map,避免源码泄露。
- 图片压缩要根据实际需求调整质量参数,小于 10KB 的图片可以内置减少非必要请求。