性能优化 - 构建体积优化

概念理解

  • Tree-Shaking(摇树优化):基于 ESM 的静态结构特性,打包工具在编译阶段通过静态分析识别没有被引用的代码,并将这些未使用的代码剔除,从而减小打包体积。

  • ESM 和 CJS

    • ESM(ES Module) :ES6 模块规范,使用 import/export 语法,依赖关系在编译阶段就能确定,支持 Tree-Shaking 优化。
    • CJS(CommonJS) :Node.js 模块规范,使用 require/module.exports 语法,依赖关系在运行时才确定,不支持 Tree-Shaking。
  • 原子 CSS :将样式拆分为最小粒度的原子类(如 m-4p-2text-center),通过组合这些原子类来构建样式。

  • 代码压缩混淆:通过删除多余的空格、换行、注释,结合 Tree-Shaking 删除死代码,简化变量声明,以及混淆变量名来减小代码体积、提升加载性能,并降低源码可读性。

  • 雪碧图(Sprite):将多个小图片合并成一张大图,通过 CSS 背景定位来显示不同的图片区域。可以减少 HTTP 请求次数,提升加载性能。

打包体积优化

1. 利用 Tree-Shaking 特性

基于 ESM 的静态结构特性,import/export 在编译阶段就能确定依赖关系,打包工具可以通过静态分析识别没有被引用的代码。

在编译阶段就能确定导入导出,并将多余代码剔除,不依赖运行时逻辑。

javascript 复制代码
export const a = 1;

import { a } from "xxx";
  1. 静态依赖分析 :打包工具(Rollup、Webpack)构建阶段遍历代码构建模块依赖图,分析哪些 export 导出的内容被其他模块通过 import 实际引用。未引用的内容,会被标记为未使用(unused)。
  2. 剔除死代码(摇树) :代码压缩阶段(摇树阶段 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-reportvite-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-loaderasset 类型处理小图片转 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-pluginvite-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 倍差距,如果有极致的性能要求,建议按场景使用。
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-loadervite-plugin-imagemin 压缩图片,小图片转 base64,使用 SVG 雪碧图减少请求次数。
    • 第三方库优化 :使用 ESM 版本的库(如 lodash-es),通过按需引入插件优化组件库(如 Element Plus),使用轻量级替代方案(如 dayjs 替代 moment)。
  • 注意事项

    • Tree-Shaking 只对 ES Module 有效,CommonJS 不支持,第三方库优先选择 ESM 版本,支持 Tree-Shaking 优化。
    • 代码分割要适度,避免过度分割导致 HTTP 请求增加。
    • 生产环境关闭 source map,避免源码泄露。
    • 图片压缩要根据实际需求调整质量参数,小于 10KB 的图片可以内置减少非必要请求。
相关推荐
GISer_Jing32 分钟前
前端GEO优化:AI时代的SEO新战场
前端·人工智能
顾林海33 分钟前
Android Profiler实战宝典:揪出CPU耗时元凶与内存泄露小偷
android·面试·性能优化
没想好d35 分钟前
通用管理后台组件库-4-消息组件开发
前端
航Hang*36 分钟前
第六章:综合布线技术 —— 干线子系统的设计与施工
网络·笔记·学习·期末·复习
文艺理科生37 分钟前
Google A2UI 解读:当 AI 不再只是陪聊,而是开始画界面
前端·vue.js·人工智能
晴栀ay39 分钟前
React性能优化三剑客:useMemo、memo与useCallback
前端·javascript·react.js
JS_GGbond39 分钟前
JavaScript继承大冒险:从“原型江湖”到“class殿堂”
前端
XiaoYu200239 分钟前
第6章 Postgres数据库安装
前端·postgresql
洛卡卡了40 分钟前
从活动编排到积分系统:事件驱动在业务系统中的一次延伸
前端·后端·面试
知其然亦知其所以然41 分钟前
别再死记硬背了,一篇文章搞懂 JS 乘性操作符
前端·javascript·程序员