生产环境极致优化:拆包、图片压缩、Gzip/Brotli 完全指南

作者:互联网

2026-03-23

Javascript教程

前言

当我们的应用从开发环境走向生产环境,真正的挑战才刚刚开始。用户不会关心我们的代码写得多么优雅,他们只关心页面加载快不快、交互流不流畅。一个未经优化的生产构建,可能让我们的用户在第一秒就流失。

为什么要优化生产构建?

一个真实的反面教材

我们先来看一个系统打包后的产物:

dist/
├── index.html                5KB
├── assets/index.abc123.js    2.8MB  ← 一个文件包含了所有代码
├── assets/vendor.def456.js   1.2MB  ← 第三方库
├── assets/style.ghi789.css   180KB
└── images/
    ├── logo.png              120KB  ← 未压缩
    ├── banner.jpg            850KB  ← 巨大
    └── ...

当用户访问这个系统时:

  • 下载 2.8MB + 1.2MB + 180KB + 970KB = 约 5MB
  • 4G 网络下需要 2 秒;3G 网络会更慢
  • 用户早跑了

构建优化的核心目标

优化维度目标收益
拆包优化分离业务代码和第三方库利用浏览器缓存,二次访问提速
图片压缩减少图片体积平均减少 60-80% 体积
Gzip/Brotli压缩文本资源减少 70-90% 传输体积
长期缓存文件名哈希,内容变化才更新最大化缓存利用率

优化能带来什么?

指标优化前优化后提升
首屏 JS 体积4.2 MB2.1 MB50%
图片总体积2.8 MB0.6 MB78%
传输体积(Gzip后)3.2 MB0.8 MB75%
首次加载时间3.2 秒1.1 秒65%
二次加载时间2.1 秒0.3 秒85%

先诊断,后开药 - 构建分析工具

为什么要先分析?

就像医生看病要先做检查一样,优化构建也要先找到问题在哪。在主观上,我们可能会觉得是不是某个依赖太大了?但实际上可能是另一个我们没想到的库!

使用 rollup-plugin-visualizer 分析

安装

npm install --save-dev rollup-plugin-visualizer

配置

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netimport { visualizer } https://images.jiaoben.netfrom https://images.jiaoben.net'rollup-plugin-visualizer'

https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netplugins: [
    https://images.jiaoben.netvisualizer({
      https://images.jiaoben.netfilename: https://images.jiaoben.net'dist/stats.html',  https://images.jiaoben.net// 输出文件
      https://images.jiaoben.netopen: https://images.jiaoben.nettrue,                   https://images.jiaoben.net// 构建后自动打开
      https://images.jiaoben.netgzipSize: https://images.jiaoben.nettrue,                https://images.jiaoben.net// 显示 gzip 后大小
      https://images.jiaoben.netbrotliSize: https://images.jiaoben.nettrue,              https://images.jiaoben.net// 显示 brotli 后大小
      https://images.jiaoben.nettemplate: https://images.jiaoben.net'treemap'            https://images.jiaoben.net// 图表类型: treemap, sunburst, network
    })
  ]
}

运行构建

npm run build
// 浏览器会自动打开一个酷炫的图表
// 一眼就能看出哪些文件最大

使用 vite-bundle-visualizer 分析

安装

npm install --save-dev vite-bundle-visualizer

运行分析

npx vite-bundle-visualizer

输出示例

┌───────────────────────┬─────────────┬──────────┬───────┐
│       Module          │    Size     │  Gzip    │ Brotli│
├───────────────────────┼─────────────┼──────────┼───────┤
│ node_modules/         │ 2.3 MB      │ 680 KB   │ 520 KB│
│   vue/                │ 680 KB      │ 210 KB   │ 160 KB│
│   element-plus/       │ 890 KB      │ 280 KB   │ 210 KB│
│   echarts/            │ 520 KB      │ 150 KB   │ 115 KB│
│   lodash-es/          │ 210 KB      │ 62 KB    │ 48 KB │
│ src/                  │ 1.8 MB      │ 480 KB   │ 360 KB│
└───────────────────────┴─────────────┴──────────┴───────┘

自定义分析脚本

https://images.jiaoben.net// scripts/analyze.js
https://images.jiaoben.netimport fs https://images.jiaoben.netfrom https://images.jiaoben.net'fs'
https://images.jiaoben.netimport path https://images.jiaoben.netfrom https://images.jiaoben.net'path'
https://images.jiaoben.netimport { gzipSizeSync } https://images.jiaoben.netfrom https://images.jiaoben.net'gzip-size'
https://images.jiaoben.netimport { brotliSizeSync } https://images.jiaoben.netfrom https://images.jiaoben.net'brotli-size'

https://images.jiaoben.netfunction https://images.jiaoben.netanalyzeDist(https://images.jiaoben.net) {
  https://images.jiaoben.netconst distDir = path.https://images.jiaoben.netresolve(https://images.jiaoben.net'./dist/assets')
  https://images.jiaoben.netconst files = fs.https://images.jiaoben.netreaddirSync(distDir)
  
  https://images.jiaoben.netlet totalSize = https://images.jiaoben.net0
  https://images.jiaoben.netlet totalGzip = https://images.jiaoben.net0
  https://images.jiaoben.netlet totalBrotli = https://images.jiaoben.net0
  
  https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net' 构建产物分析n')
  
  files
    .https://images.jiaoben.netfilter(https://images.jiaoben.nethttps://images.jiaoben.netf => f.https://images.jiaoben.netendsWith(https://images.jiaoben.net'.js') || f.https://images.jiaoben.netendsWith(https://images.jiaoben.net'.css'))
    .https://images.jiaoben.netforEach(https://images.jiaoben.nethttps://images.jiaoben.netfile => {
      https://images.jiaoben.netconst filePath = path.https://images.jiaoben.netjoin(distDir, file)
      https://images.jiaoben.netconst content = fs.https://images.jiaoben.netreadFileSync(filePath)
      https://images.jiaoben.netconst size = content.https://images.jiaoben.netlength
      https://images.jiaoben.netconst gzip = https://images.jiaoben.netgzipSizeSync(content)
      https://images.jiaoben.netconst brotli = https://images.jiaoben.netbrotliSizeSync(content)
      
      totalSize += size
      totalGzip += gzip
      totalBrotli += brotli
      
      https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net`https://images.jiaoben.net${file}:`)
      https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net`  Raw:    https://images.jiaoben.net${(size / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} KB`)
      https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net`  Gzip:   https://images.jiaoben.net${(gzip / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} KB (https://images.jiaoben.net${(gzip/size*https://images.jiaoben.net100).toFixed(https://images.jiaoben.net0)}%)`)
      https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net`  Brotli: https://images.jiaoben.net${(brotli / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} KB (https://images.jiaoben.net${(brotli/size*https://images.jiaoben.net100).toFixed(https://images.jiaoben.net0)}%)n`)
    })
  
  https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net' 总计:')
  https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net`  Raw:    https://images.jiaoben.net${(totalSize / https://images.jiaoben.net1024 / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} MB`)
  https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net`  Gzip:   https://images.jiaoben.net${(totalGzip / https://images.jiaoben.net1024 / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} MB`)
  https://images.jiaoben.netconsole.https://images.jiaoben.netlog(https://images.jiaoben.net`  Brotli: https://images.jiaoben.net${(totalBrotli / https://images.jiaoben.net1024 / https://images.jiaoben.net1024).toFixed(https://images.jiaoben.net2)} MB`)
}

https://images.jiaoben.netanalyzeDist()

看懂分析结果

分析结果能告诉我们什么?

1. 找出最大的依赖

  • echarts: 520KB → 考虑按需加载
  • monaco-editor: 2.8MB → 考虑动态导入

2. 找出重复的依赖

  • lodash 和 lodash-es 同时存在? → 统一用 lodash-es
  • moment 和 dayjs 同时存在? → 用 dayjs 替代 moment

3. 找出可以拆分的点

  • node_modules 打包在一起太大了 → 拆成多个 chunk
  • 所有页面代码都在一个文件里 → 按路由拆分

拆包策略 - 把大象放进冰箱

为什么要拆包?

用一个比喻来解释

不拆包:把所有东西都塞进一个行李箱
├─ 想拿牙刷 → 要翻遍整个箱子
├─ 箱子破了 → 所有东西都掉出来
└─ 箱子太大 → 搬不动

拆包:分成多个小包
├─ 洗漱包:牙刷、牙膏、毛巾
├─ 衣物包:衣服、裤子、袜子
├─ 电子包:充电器、数据线
├─ 哪个包破了 → 只损失那部分
└─ 每个包都很轻 → 好搬

技术层面的好处

不拆包:
├─ 修改一行代码 → 整个大文件缓存失效
└─ 用户每次更新都要重新下载所有代码

拆包后:
├─ 第三方库独立 → 几乎不变,长期缓存
├─ 业务代码拆分 → 只下载修改的部分
└─ 多个小文件可以并行下载

基础拆包配置

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netbuild: {
    https://images.jiaoben.netrollupOptions: {
      https://images.jiaoben.netoutput: {
        https://images.jiaoben.net// 最基本的拆包策略
        https://images.jiaoben.netmanualChunks: {
          https://images.jiaoben.net// 将 Vue 全家桶打包在一起
          https://images.jiaoben.net'vendor-vue': [https://images.jiaoben.net'vue', https://images.jiaoben.net'vue-router', https://images.jiaoben.net'pinia', https://images.jiaoben.net'vuex'],
          
          https://images.jiaoben.net// 将 UI 库打包在一起
          https://images.jiaoben.net'vendor-ui': [https://images.jiaoben.net'element-plus', https://images.jiaoben.net'@element-plus/icons-vue', https://images.jiaoben.net'ant-design-vue'],
          
          https://images.jiaoben.net// 将工具库打包在一起
          https://images.jiaoben.net'vendor-utils': [https://images.jiaoben.net'lodash-es', https://images.jiaoben.net'dayjs', https://images.jiaoben.net'axios', https://images.jiaoben.net'date-fns'],
          
          https://images.jiaoben.net// 将图表库打包在一起
          https://images.jiaoben.net'vendor-charts': [https://images.jiaoben.net'echarts', https://images.jiaoben.net'd3', https://images.jiaoben.net'chart.js']
        }
      }
    }
  }
}

智能拆包:根据依赖关系自动拆分

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netbuild: {
    https://images.jiaoben.netrollupOptions: {
      https://images.jiaoben.netoutput: {
        https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid: https://images.jiaoben.netstring) {
          https://images.jiaoben.net// node_modules 中的依赖
          https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
            https://images.jiaoben.net// 按包名拆分
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue'  https://images.jiaoben.net// 所有 vue 相关
            }
            
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'element-plus') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'antd')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-ui'   https://images.jiaoben.net// UI 库
            }
            
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'echarts') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'd3')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-charts' https://images.jiaoben.net// 图表库
            }
            
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'lodash') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'dayjs')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-utils'  https://images.jiaoben.net// 工具库
            }
            
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'monaco-editor')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-monaco'  https://images.jiaoben.net// 编辑器单独打包
            }
            
            https://images.jiaoben.net// 其他依赖打包在一起
            https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-other'
          }
          
          https://images.jiaoben.net// 业务代码按页面拆分
          https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'/src/views/')) {
            https://images.jiaoben.netconst match = id.https://images.jiaoben.netmatch(https://images.jiaoben.net//src/views/([^/]+)/)
            https://images.jiaoben.netif (match) {
              https://images.jiaoben.netreturn https://images.jiaoben.net`page-https://images.jiaoben.net${match[https://images.jiaoben.net1]}` https://images.jiaoben.net// 按页面拆分
            }
          }
          
          https://images.jiaoben.net// 公共组件按模块拆分
          https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'/src/components/')) {
            https://images.jiaoben.netconst match = id.https://images.jiaoben.netmatch(https://images.jiaoben.net//src/components/([^/]+)/)
            https://images.jiaoben.netif (match) {
              https://images.jiaoben.netreturn https://images.jiaoben.net`components-https://images.jiaoben.net${match[https://images.jiaoben.net1]}`
            }
          }
        }
      }
    }
  }
}

高级拆包:基于大小的自动拆分

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netbuild: {
    https://images.jiaoben.netrollupOptions: {
      https://images.jiaoben.netoutput: {
        https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid: https://images.jiaoben.netstring, { getModuleInfo }) {
          https://images.jiaoben.net// 如果模块大于 500KB,单独拆包
          https://images.jiaoben.netconst moduleInfo = https://images.jiaoben.netgetModuleInfo(id)
          https://images.jiaoben.netif (moduleInfo && moduleInfo.https://images.jiaoben.netcode) {
            https://images.jiaoben.netconst size = https://images.jiaoben.netBuffer.https://images.jiaoben.netbyteLength(moduleInfo.https://images.jiaoben.netcode, https://images.jiaoben.net'utf8')
            https://images.jiaoben.netif (size > https://images.jiaoben.net500 * https://images.jiaoben.net1024) { https://images.jiaoben.net// 500KB
              https://images.jiaoben.netconst name = id.https://images.jiaoben.netmatch(https://images.jiaoben.net/[^/]+.(js|ts|vue)$/)?.[https://images.jiaoben.net0]
              https://images.jiaoben.netreturn https://images.jiaoben.net`large-https://images.jiaoben.net${name}`  https://images.jiaoben.net// 大文件单独打包
            }
          }
          
          https://images.jiaoben.net// 继续其他拆分逻辑
          https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue'
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'element-plus')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-ui'
          }
        }
      }
    }
  }
}

异步 chunk 的命名优化

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netbuild: {
    https://images.jiaoben.netrollupOptions: {
      https://images.jiaoben.netoutput: {
        https://images.jiaoben.net// 异步 chunk 命名
        https://images.jiaoben.netchunkFileNames: https://images.jiaoben.net'assets/chunks/[name]-[hash].js',
        
        https://images.jiaoben.net// 入口文件命名
        https://images.jiaoben.netentryFileNames: https://images.jiaoben.net'assets/[name]-[hash].js',
        
        https://images.jiaoben.net// 资源文件命名
        https://images.jiaoben.netassetFileNames: https://images.jiaoben.net'assets/[ext]/[name]-[hash].[ext]',
        
        https://images.jiaoben.netmanualChunks: {
          https://images.jiaoben.net// ... 拆包配置
        }
      }
    }
  }
}

https://images.jiaoben.net// 输出结果:
https://images.jiaoben.net// assets/index-abc123.js                (入口)
https://images.jiaoben.net// assets/chunks/vendor-vue-def456.js    (Vue 相关)
https://images.jiaoben.net// assets/chunks/page-dashboard-ghi789.js (页面)
https://images.jiaoben.net// assets/images/logo-jkl012.png         (图片)

拆包后的效果

拆包方式文件数量缓存利用率适用场景
不拆包1个极低小项目
按依赖拆分5-10个中大型项目
按页面拆分10-50个较高多页面应用
按大小拆分可变中等有大文件的项目

图片压缩 - 看不见的优化

为什么图片是优化重点?

我们先来看一个典型的页面资源分布:

https://images.jiaoben.netconst pageResources = {
  https://images.jiaoben.netjs: https://images.jiaoben.net'2.8MB (40%)',
  https://images.jiaoben.netcss: https://images.jiaoben.net'180KB (3%)',
  https://images.jiaoben.netimages: https://images.jiaoben.net'3.5MB (50%)',  https://images.jiaoben.net// 图片占了一半!
  https://images.jiaoben.netfonts: https://images.jiaoben.net'500KB (7%)'
}

在页面中,图片通常占页面总体积的 50-70%,因此优化图片是最容易见效的!

vite-plugin-image-optimizer 配置

安装

npm install --save-dev vite-plugin-image-optimizer

配置

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netimport { https://images.jiaoben.netViteImageOptimizer } https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-image-optimizer'

https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netplugins: [
    https://images.jiaoben.netViteImageOptimizer({
      https://images.jiaoben.net// 配置文件类型和压缩参数
      https://images.jiaoben.netpng: {
        https://images.jiaoben.netquality: https://images.jiaoben.net80,  https://images.jiaoben.net// PNG 质量 0-100
        https://images.jiaoben.netcompressionLevel: https://images.jiaoben.net9, https://images.jiaoben.net// 压缩级别 0-9
      },
      https://images.jiaoben.netjpeg: {
        https://images.jiaoben.netquality: https://images.jiaoben.net75,  https://images.jiaoben.net// JPEG 质量
        https://images.jiaoben.netprogressive: https://images.jiaoben.nettrue, https://images.jiaoben.net// 渐进式 JPEG
      },
      https://images.jiaoben.netjpg: {
        https://images.jiaoben.netquality: https://images.jiaoben.net75,
      },
      https://images.jiaoben.netwebp: {
        https://images.jiaoben.netquality: https://images.jiaoben.net75,  https://images.jiaoben.net// WebP 质量
        https://images.jiaoben.netlossless: https://images.jiaoben.netfalse, https://images.jiaoben.net// 是否无损
      },
      https://images.jiaoben.netavif: {
        https://images.jiaoben.netquality: https://images.jiaoben.net60,  https://images.jiaoben.net// AVIF 质量
        https://images.jiaoben.netlossless: https://images.jiaoben.netfalse,
      },
      https://images.jiaoben.netsvg: {
        https://images.jiaoben.net// SVG 优化选项
        https://images.jiaoben.netplugins: [
          {
            https://images.jiaoben.netname: https://images.jiaoben.net'preset-default',
            https://images.jiaoben.netparams: {
              https://images.jiaoben.netoverrides: {
                https://images.jiaoben.netremoveViewBox: https://images.jiaoben.netfalse, https://images.jiaoben.net// 保留 viewBox
                https://images.jiaoben.netcleanupIds: https://images.jiaoben.netfalse,     https://images.jiaoben.net// 保留 ID
              },
            },
          },
        ],
      },
      https://images.jiaoben.nettiff: {
        https://images.jiaoben.netquality: https://images.jiaoben.net70,
      },
      https://images.jiaoben.netgif: {
        https://images.jiaoben.netoptimizationLevel: https://images.jiaoben.net3, https://images.jiaoben.net// 优化级别 1-3
      },
    })
  ]
}

不同图片类型的优化策略

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netplugins: [
    https://images.jiaoben.netViteImageOptimizer({
      https://images.jiaoben.net// 根据不同用途设置不同参数
      
      https://images.jiaoben.net// 1. 图标类:需要清晰,适当压缩
      https://images.jiaoben.net'src/assets/icons/**/*': {
        https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net90 },
        https://images.jiaoben.netsvg: { https://images.jiaoben.netplugins: [https://images.jiaoben.net'preset-default'] }
      },
      
      https://images.jiaoben.net// 2. 背景图:可以牺牲一些质量换取体积
      https://images.jiaoben.net'src/assets/backgrounds/**/*': {
        https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net65 },
        https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net60 }
      },
      
      https://images.jiaoben.net// 3. 产品图:平衡质量和体积
      https://images.jiaoben.net'src/assets/products/**/*': {
        https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net80 },
        https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net75 }
      },
      
      https://images.jiaoben.net// 4. 用户上传:保持较好质量
      https://images.jiaoben.net'src/assets/uploads/**/*': {
        https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net85 },
        https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net85 }
      }
    })
  ]
}

使用现代图片格式

配置

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netplugins: [
    https://images.jiaoben.netViteImageOptimizer({
      https://images.jiaoben.net// 生成 WebP 版本(浏览器支持更好)
      https://images.jiaoben.netwebp: {
        https://images.jiaoben.netquality: https://images.jiaoben.net75
      },
      
      https://images.jiaoben.net// 生成 AVIF 版本(压缩率更高)
      https://images.jiaoben.netavif: {
        https://images.jiaoben.netquality: https://images.jiaoben.net60
      }
    })
  ]
}

在组件中配合使用

https://images.jiaoben.nettemplate>
  https://images.jiaoben.net
  https://images.jiaoben.netpicture>
    https://images.jiaoben.net
    https://images.jiaoben.netsource https://images.jiaoben.netsrcset=https://images.jiaoben.net"/image.avif" https://images.jiaoben.nettype=https://images.jiaoben.net"image/avif">
    https://images.jiaoben.net
    https://images.jiaoben.netsource https://images.jiaoben.netsrcset=https://images.jiaoben.net"/image.webp" https://images.jiaoben.nettype=https://images.jiaoben.net"image/webp">
    https://images.jiaoben.net
    https://images.jiaoben.netimg https://images.jiaoben.netsrc=https://images.jiaoben.net"/image.jpg" https://images.jiaoben.netalt=https://images.jiaoben.net"图片" https://images.jiaoben.netloading=https://images.jiaoben.net"lazy">
  https://images.jiaoben.netpicture>
https://images.jiaoben.nettemplate>

懒加载与图片优化结合

https://images.jiaoben.nettemplate>
  https://images.jiaoben.netimg 
    https://images.jiaoben.netv-lazy=https://images.jiaoben.net"optimizedImageUrl"
    https://images.jiaoben.net:data-srcset=https://images.jiaoben.net"`
      ${smallImage} 400w,
      ${mediumImage} 800w,
      ${largeImage} 1200w
    `"
    https://images.jiaoben.netsizes=https://images.jiaoben.net"(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
    https://images.jiaoben.netloading=https://images.jiaoben.net"lazy"
    https://images.jiaoben.net:alt=https://images.jiaoben.net"alt"
  >
https://images.jiaoben.nettemplate>

https://images.jiaoben.netscript https://images.jiaoben.netsetup>https://images.jiaoben.net
https://images.jiaoben.netimport { computed } https://images.jiaoben.netfrom https://images.jiaoben.net'vue'

https://images.jiaoben.netconst props = defineProps<{ 
  https://images.jiaoben.netimagePath: string,
  alt?: string 
}>()

https://images.jiaoben.net// 根据视图宽度选择合适大小的图片
https://images.jiaoben.netconst optimizedImageUrl = https://images.jiaoben.netcomputed(https://images.jiaoben.net() => {
  https://images.jiaoben.net// 假设构建时生成了不同尺寸的图片
  https://images.jiaoben.net// logo-small.jpg, logo-medium.jpg, logo-large.jpg
  https://images.jiaoben.netconst width = https://images.jiaoben.nettypeof https://images.jiaoben.netwindow !== https://images.jiaoben.net'undefined' ? https://images.jiaoben.netwindow.https://images.jiaoben.netinnerWidth : https://images.jiaoben.net1200
  
  https://images.jiaoben.netif (width < https://images.jiaoben.net600) {
    https://images.jiaoben.netreturn props.https://images.jiaoben.netimagePath.https://images.jiaoben.netreplace(https://images.jiaoben.net/.(jpg|png)$/, https://images.jiaoben.net'-small.$1')
  }
  https://images.jiaoben.netif (width < https://images.jiaoben.net1200) {
    https://images.jiaoben.netreturn props.https://images.jiaoben.netimagePath.https://images.jiaoben.netreplace(https://images.jiaoben.net/.(jpg|png)$/, https://images.jiaoben.net'-medium.$1')
  }
  https://images.jiaoben.netreturn props.https://images.jiaoben.netimagePath.https://images.jiaoben.netreplace(https://images.jiaoben.net/.(jpg|png)$/, https://images.jiaoben.net'-large.$1')
})
https://images.jiaoben.netscript>

图片优化的效果

图片类型优化前优化后节省
PNG 图标120KB35KB71%
JPG 产品图850KB180KB79%
WebP 背景650KB110KB83%
SVG 矢量15KB8KB47%
总体积2.8MB0.6MB78%

Gzip/Brotli 压缩 - 让传输更轻盈

什么是 Gzip/Brotli?

我们可以用快递来比喻,比如我们有一件很大的“羽绒服”要邮寄给浏览器:

  • 原始文件:一件羽绒服(很大,但很轻)
  • Gzip:真空压缩袋,把羽绒服压扁
  • Brotli:更好的真空压缩袋,压得更扁

当浏览器收到压缩后的文件,它只需要打开压缩袋,羽绒服(文件)就可以恢复原状!

压缩算法的对比

算法压缩率压缩速度解压速度浏览器支持
Gzip中等所有浏览器
Brotli中等现代浏览器 (92%)
Deflate极快极快所有浏览器

相同文件对比

  • 原始 JS: 1000 KB
  • Gzip: 280 KB (72% 减少)
  • Brotli: 220 KB (78% 减少)
  • Brotli 比 Gzip 再减少 21% 体积

使用 vite-plugin-compression 配置

安装

npm install --save-dev vite-plugin-compression

配置

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netimport compression https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-compression'

https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netplugins: [
    https://images.jiaoben.net// Gzip 压缩
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
      https://images.jiaoben.netext: https://images.jiaoben.net'.gz',
      https://images.jiaoben.netthreshold: https://images.jiaoben.net10240, https://images.jiaoben.net// 10KB 以上才压缩
      https://images.jiaoben.netdeleteOriginFile: https://images.jiaoben.netfalse, https://images.jiaoben.net// 保留原文件
      https://images.jiaoben.netverbose: https://images.jiaoben.nettrue, https://images.jiaoben.net// 输出压缩信息
      https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css|html|svg)$/ https://images.jiaoben.net// 只压缩文本文件
    }),
    
    https://images.jiaoben.net// Brotli 压缩
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
      https://images.jiaoben.netext: https://images.jiaoben.net'.br',
      https://images.jiaoben.netthreshold: https://images.jiaoben.net10240,
      https://images.jiaoben.netdeleteOriginFile: https://images.jiaoben.netfalse,
      https://images.jiaoben.netverbose: https://images.jiaoben.nettrue,
      https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css|html|svg)$/
    })
  ]
}

https://images.jiaoben.net// 构建结果:
https://images.jiaoben.net// index.abc123.js
https://images.jiaoben.net// index.abc123.js.gz    (Gzip)
https://images.jiaoben.net// index.abc123.js.br    (Brotli)

智能压缩策略 - 多算法混合策略

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netimport compression https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-compression'

https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netplugins: [
    https://images.jiaoben.net// 对不同的资源使用不同的策略
    
    https://images.jiaoben.net// 1. HTML: 使用 Brotli(最高压缩率)
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
      https://images.jiaoben.netext: https://images.jiaoben.net'.br',
      https://images.jiaoben.netfilter: https://images.jiaoben.net/.html$/,
      https://images.jiaoben.netthreshold: https://images.jiaoben.net1024
    }),
    
    https://images.jiaoben.net// 2. JS/CSS: 同时生成 Gzip 和 Brotli
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
      https://images.jiaoben.netext: https://images.jiaoben.net'.gz',
      https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/,
      https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
    }),
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
      https://images.jiaoben.netext: https://images.jiaoben.net'.br',
      https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/,
      https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
    }),
    
    https://images.jiaoben.net// 3. 大文件用 Brotli,小文件用 Gzip
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
      https://images.jiaoben.netext: https://images.jiaoben.net'.br',
      https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/,
      https://images.jiaoben.netthreshold: https://images.jiaoben.net51200 https://images.jiaoben.net// 50KB 以上用 Brotli
    }),
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
      https://images.jiaoben.netext: https://images.jiaoben.net'.gz',
      https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/,
      https://images.jiaoben.netthreshold: https://images.jiaoben.net10240, https://images.jiaoben.net// 10-50KB 用 Gzip
      https://images.jiaoben.netdeleteOriginFile: https://images.jiaoben.nettrue https://images.jiaoben.net// 小文件可以删除原文件
    })
  ]
}

Nginx 配置示例

https://images.jiaoben.net# nginx.conf
server {
  listen 80;
  server_name example.com;
  root /usr/share/nginx/html;
  
  https://images.jiaoben.net# 开启 Gzip
  gzip on;
  gzip_vary on;
  gzip_min_length 10240;
  gzip_types text/plain text/css text/xml text/javascript 
             application/javascript application/x-javascript 
             application/xml application/json;
  gzip_comp_level 6;
  gzip_buffers 16 8k;
  gzip_http_version 1.1;
  
  https://images.jiaoben.net# Brotli 支持(需要编译 brotli 模块)
  brotli on;
  brotli_min_length 10240;
  brotli_types text/plain text/css text/xml text/javascript 
               application/javascript application/x-javascript 
               application/xml application/json;
  brotli_comp_level 6;
  
  location / {
    try_files https://images.jiaoben.net$uri https://images.jiaoben.net$uri/ /index.html;
    
    https://images.jiaoben.net# 尝试 Brotli,然后是 Gzip,最后是原始文件
    location ~* .(js|css)$ {
      try_files https://images.jiaoben.net$uri.br https://images.jiaoben.net$uri.gz https://images.jiaoben.net$uri =404;
      
      https://images.jiaoben.net# 根据 Accept-Encoding 设置正确的 Content-Encoding
      https://images.jiaoben.netif (https://images.jiaoben.net$http_accept_encoding ~* br) {
        add_header Content-Encoding br;
        add_header Content-Type https://images.jiaoben.net$content_type;
      }
      https://images.jiaoben.netif (https://images.jiaoben.net$http_accept_encoding ~* gzip) {
        add_header Content-Encoding gzip;
        add_header Content-Type https://images.jiaoben.net$content_type;
      }
      
      https://images.jiaoben.net# 长期缓存
      expires 1y;
      add_header Cache-Control https://images.jiaoben.net"public, immutable";
      add_header Vary Accept-Encoding;
    }
    
    https://images.jiaoben.net# 图片缓存
    location ~* .(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
      expires 30d;
      add_header Cache-Control https://images.jiaoben.net"public";
    }
  }
}

验证压缩效果

https://images.jiaoben.net# 使用 curl 验证压缩

https://images.jiaoben.net# 查看是否支持压缩
curl -H https://images.jiaoben.net"Accept-Encoding: gzip, br" -I 

https://images.jiaoben.net# 响应头应该包含
Content-Encoding: br
Content-Type: application/javascript
Content-Length: 220000

https://images.jiaoben.net# 下载并解压验证
curl -H https://images.jiaoben.net"Accept-Encoding: br"  | brotli -d

https://images.jiaoben.net# 或者使用 httpie
http  Accept-Encoding:br

长期缓存策略:让缓存最大化

文件名哈希的原理

https://images.jiaoben.net// 构建后的文件名
https://images.jiaoben.net// index.[hash].js

https://images.jiaoben.net// 哈希是基于文件内容生成的
https://images.jiaoben.net// 内容不变 → 哈希不变 → 缓存有效
https://images.jiaoben.net// 内容变化 → 哈希变化 → 重新下载

dist/
├── index.https://images.jiaoben.netabc123.https://images.jiaoben.netjs    https://images.jiaoben.net// 哈希基于内容生成
├── index.https://images.jiaoben.netdef456.https://images.jiaoben.netjs    https://images.jiaoben.net// 内容变化,哈希变化
├── vendor-vue.123abc.https://images.jiaoben.netjs https://images.jiaoben.net// 第三方库几乎不变
└── vendor-ui.456def.https://images.jiaoben.netjs   https://images.jiaoben.net// UI 库偶尔更新

配置文件名哈希

https://images.jiaoben.net// vite.config.ts
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netbuild: {
    https://images.jiaoben.netrollupOptions: {
      https://images.jiaoben.netoutput: {
        https://images.jiaoben.net// 入口文件
        https://images.jiaoben.netentryFileNames: https://images.jiaoben.net'assets/[name].[hash].js',
        
        https://images.jiaoben.net// 异步 chunk
        https://images.jiaoben.netchunkFileNames: https://images.jiaoben.net'assets/chunks/[name].[hash].js',
        
        https://images.jiaoben.net// 资源文件
        https://images.jiaoben.netassetFileNames: https://images.jiaoben.net'assets/[ext]/[name].[hash].[ext]',
        
        https://images.jiaoben.netmanualChunks: {
          https://images.jiaoben.net// 稳定的第三方库单独打包(几乎不变)
          https://images.jiaoben.net'vendor-stable': [
            https://images.jiaoben.net'vue',
            https://images.jiaoben.net'vue-router',
            https://images.jiaoben.net'pinia',
            https://images.jiaoben.net'vuex'
          ],
          
          https://images.jiaoben.net// 可能更新的 UI 库单独打包
          https://images.jiaoben.net'vendor-ui': [
            https://images.jiaoben.net'element-plus',
            https://images.jiaoben.net'@element-plus/icons-vue',
            https://images.jiaoben.net'ant-design-vue'
          ],
          
          https://images.jiaoben.net// 可能更新的工具库
          https://images.jiaoben.net'vendor-utils': [
            https://images.jiaoben.net'lodash-es',
            https://images.jiaoben.net'dayjs',
            https://images.jiaoben.net'axios'
          ]
        }
      }
    },
    
    https://images.jiaoben.net// 生成 manifest.json
    https://images.jiaoben.netmanifest: https://images.jiaoben.nettrue
  }
}

Nginx 缓存配置

https://images.jiaoben.net# nginx.conf
server {
  https://images.jiaoben.net# 静态资源缓存配置
  
  https://images.jiaoben.net# JS/CSS 长期缓存(带 hash 的文件)
  location ~* .(js|css)$ {
    https://images.jiaoben.net# 匹配带 hash 的文件
    https://images.jiaoben.netif (https://images.jiaoben.net$uri ~* https://images.jiaoben.net".[a-f0-9]{8,20}.(js|css)$") {
      expires 1y;
      add_header Cache-Control https://images.jiaoben.net"public, immutable";
    }
    
    https://images.jiaoben.net# 如果不带 hash,短时间缓存
    expires 1h;
    add_header Cache-Control https://images.jiaoben.net"public";
    
    https://images.jiaoben.net# 尝试压缩版本
    try_files https://images.jiaoben.net$uri.br https://images.jiaoben.net$uri.gz https://images.jiaoben.net$uri =404;
    add_header Vary Accept-Encoding;
  }
  
  https://images.jiaoben.net# 图片等资源
  location ~* .(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
    expires 30d;
    add_header Cache-Control https://images.jiaoben.net"public";
  }
  
  https://images.jiaoben.net# 字体文件
  location ~* .(woff2?|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control https://images.jiaoben.net"public, immutable";
    add_header Access-Control-Allow-Origin https://images.jiaoben.net"*";
  }
  
  https://images.jiaoben.net# HTML 文件不缓存
  location ~* .html$ {
    expires -1;
    add_header Cache-Control https://images.jiaoben.net"no-cache, must-revalidate";
  }
}

Service Worker 缓存策略

https://images.jiaoben.net// sw.js
https://images.jiaoben.netconst https://images.jiaoben.netCACHE_NAME = https://images.jiaoben.net'v1'
https://images.jiaoben.netconst https://images.jiaoben.netCACHE_URLS = [
  https://images.jiaoben.net'/',
  https://images.jiaoben.net'/index.html',
  https://images.jiaoben.net'/manifest.json'
]

https://images.jiaoben.net// 安装时缓存核心资源
self.https://images.jiaoben.netaddEventListener(https://images.jiaoben.net'install', https://images.jiaoben.nethttps://images.jiaoben.netevent => {
  event.https://images.jiaoben.netwaitUntil(
    caches.https://images.jiaoben.netopen(https://images.jiaoben.netCACHE_NAME)
      .https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netcache => cache.https://images.jiaoben.netaddAll(https://images.jiaoben.netCACHE_URLS))
  )
})

https://images.jiaoben.net// 缓存策略:缓存优先,网络回退
self.https://images.jiaoben.netaddEventListener(https://images.jiaoben.net'fetch', https://images.jiaoben.nethttps://images.jiaoben.netevent => {
  https://images.jiaoben.netconst url = https://images.jiaoben.netnew https://images.jiaoben.netURL(event.https://images.jiaoben.netrequest.https://images.jiaoben.neturl)
  
  https://images.jiaoben.net// 静态资源使用 Cache First 策略
  https://images.jiaoben.netif (url.https://images.jiaoben.netpathname.https://images.jiaoben.netmatch(https://images.jiaoben.net/.(js|css|png|jpg|webp)$/)) {
    event.https://images.jiaoben.netrespondWith(
      caches.https://images.jiaoben.netmatch(event.https://images.jiaoben.netrequest)
        .https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netresponse => {
          https://images.jiaoben.net// 缓存命中直接返回
          https://images.jiaoben.netif (response) https://images.jiaoben.netreturn response
          
          https://images.jiaoben.net// 未命中则请求网络并缓存
          https://images.jiaoben.netreturn https://images.jiaoben.netfetch(event.https://images.jiaoben.netrequest).https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netresponse => {
            https://images.jiaoben.netconst clone = response.https://images.jiaoben.netclone()
            caches.https://images.jiaoben.netopen(https://images.jiaoben.netCACHE_NAME).https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netcache => {
              cache.https://images.jiaoben.netput(event.https://images.jiaoben.netrequest, clone)
            })
            https://images.jiaoben.netreturn response
          })
        })
    )
  } 
  https://images.jiaoben.net// HTML 使用 Network First 策略
  https://images.jiaoben.netelse https://images.jiaoben.netif (url.https://images.jiaoben.netpathname.https://images.jiaoben.netendsWith(https://images.jiaoben.net'.html') || url.https://images.jiaoben.netpathname === https://images.jiaoben.net'/') {
    event.https://images.jiaoben.netrespondWith(
      https://images.jiaoben.netfetch(event.https://images.jiaoben.netrequest)
        .https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netresponse => {
          https://images.jiaoben.netconst clone = response.https://images.jiaoben.netclone()
          caches.https://images.jiaoben.netopen(https://images.jiaoben.netCACHE_NAME).https://images.jiaoben.netthen(https://images.jiaoben.nethttps://images.jiaoben.netcache => {
            cache.https://images.jiaoben.netput(event.https://images.jiaoben.netrequest, clone)
          })
          https://images.jiaoben.netreturn response
        })
        .https://images.jiaoben.netcatch(https://images.jiaoben.net() => caches.https://images.jiaoben.netmatch(event.https://images.jiaoben.netrequest))
    )
  }
})

缓存命中率的提升

文件类型更新频率缓存策略命中率
vendor-vue.js几乎不变永久缓存99%
vendor-ui.js偶尔更新永久缓存92%
page-*.js经常更新永久缓存65%
图片很少更新30天缓存95%
字体从不更新永久缓存99%

实战案例:一个中大型项目的构建优化

优化前的状态

https://images.jiaoben.net// 项目信息
https://images.jiaoben.net// - 页面数量:45 个
https://images.jiaoben.net// - 组件数量:850 个
https://images.jiaoben.net// - 第三方依赖:230 个
https://images.jiaoben.net// - 图片数量:1200 张

https://images.jiaoben.net// 构建产物
dist/ 总大小: https://images.jiaoben.net45 https://images.jiaoben.netMB
├── js/      https://images.jiaoben.net28 https://images.jiaoben.netMB
├── css/     https://images.jiaoben.net2.5 https://images.jiaoben.netMB
├── images/  https://images.jiaoben.net14 https://images.jiaoben.netMB
└── others/  https://images.jiaoben.net0.5 https://images.jiaoben.netMB

https://images.jiaoben.net// 性能指标
https://images.jiaoben.net// - 构建时间:3 分 45 秒
https://images.jiaoben.net// - 首屏体积:4.2 MB
https://images.jiaoben.net// - 加载时间:3.2 秒

优化步骤

第一步:分析找出问题

https://images.jiaoben.net# 运行分析
npx vite-bundle-visualizer

https://images.jiaoben.net# 发现问题
echarts: 1.2MB        ← 太大
monaco-editor: 2.8MB  ← 巨大!
lodash-es: 210KB      ← 还好
moment: 450KB         ← 可以用 dayjs 替代

第二步:优化拆包

https://images.jiaoben.net// vite.config.js
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netbuild: {
    https://images.jiaoben.netrollupOptions: {
      https://images.jiaoben.netoutput: {
        https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid) {
          https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
            https://images.jiaoben.net// 把 echarts 单独打包
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'echarts')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-echarts'
            }
            
            https://images.jiaoben.net// 把 monaco-editor 单独打包
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'monaco-editor')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-monaco'
            }
            
            https://images.jiaoben.net// 其他分组
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue'
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'element-plus')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-ui'
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'lodash') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'dayjs')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-utils'
            }
            
            https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-other'
          }
          
          https://images.jiaoben.net// 按页面拆分
          https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'/src/views/')) {
            https://images.jiaoben.netconst match = id.https://images.jiaoben.netmatch(https://images.jiaoben.net//src/views/([^/]+)/)
            https://images.jiaoben.netif (match) https://images.jiaoben.netreturn https://images.jiaoben.net`page-https://images.jiaoben.net${match[https://images.jiaoben.net1]}`
          }
        }
      }
    }
  }
}

第三步:图片压缩

https://images.jiaoben.net// vite.config.js
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netplugins: [
    https://images.jiaoben.netViteImageOptimizer({
      https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net75 },
      https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net70 },
      https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net70 },
      https://images.jiaoben.netavif: { https://images.jiaoben.netquality: https://images.jiaoben.net60 }
    })
  ]
}

第四步:开启压缩

https://images.jiaoben.net// vite.config.js
https://images.jiaoben.netexport https://images.jiaoben.netdefault {
  https://images.jiaoben.netplugins: [
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
      https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
    })
  ]
}

第五步:按需加载

https://images.jiaoben.net// 大组件使用动态导入
https://images.jiaoben.netconst https://images.jiaoben.netMonacoEditor = https://images.jiaoben.netdefineAsyncComponent(https://images.jiaoben.net() => 
  https://images.jiaoben.netimport(https://images.jiaoben.net'monaco-editor')
)

https://images.jiaoben.net// 路由懒加载
https://images.jiaoben.netconst routes = [
  {
    https://images.jiaoben.netpath: https://images.jiaoben.net'/dashboard',
    https://images.jiaoben.netcomponent: https://images.jiaoben.net() => https://images.jiaoben.netimport(https://images.jiaoben.net'./views/Dashboard.vue')  https://images.jiaoben.net// 按需加载
  }
]

优化后的结果

指标优化前优化后提升
构建时间3 分 45 秒2 分 20 秒38%
总大小45 MB18 MB60%
首屏 JS 体积4.2 MB1.8 MB57%
图片体积14 MB3.5 MB75%
传输体积3.2 MB0.8 MB75%
加载时间3.2 秒1.1 秒65%

常见问题与解决方案

问题一:拆包过多导致请求数爆炸

https://images.jiaoben.net//  错误:拆得太细
https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid) {
  https://images.jiaoben.net// 每个依赖都单独打包
  https://images.jiaoben.netreturn id.https://images.jiaoben.netmatch(https://images.jiaoben.net/node_modules/([^/]+)/)?.[https://images.jiaoben.net1]
}
https://images.jiaoben.net// 结果:产生 200+ 个文件,HTTP/1.1 下性能差

https://images.jiaoben.net//  正确:合理分组
https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid) {
  https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
    https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue'
    https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'lodash')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-utils'
    https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'echarts')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-charts'
    https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'monaco')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-monaco'
    https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-other' https://images.jiaoben.net// 其他合并
  }
}

问题二:图片压缩后质量下降

https://images.jiaoben.net// 解决方案:选择性压缩
https://images.jiaoben.netViteImageOptimizer({
  https://images.jiaoben.net// 图标保留较高品质
  https://images.jiaoben.net'src/assets/icons/**/*': {
    https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net90 },
    https://images.jiaoben.netsvg: { https://images.jiaoben.netplugins: [https://images.jiaoben.net'preset-default'] }
  },
  
  https://images.jiaoben.net// 背景图可以接受较低品质
  https://images.jiaoben.net'src/assets/backgrounds/**/*': {
    https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net65 },
    https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net60 }
  },
  
  https://images.jiaoben.net// 产品图需要平衡
  https://images.jiaoben.net'src/assets/products/**/*': {
    https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net80 },
    https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net75 }
  }
})

https://images.jiaoben.net// 或者使用图片 CDN 动态处理
"https://cdn.example.com/image.jpg?x-oss-process=image/resize,w_400/quality,q_80">

问题三:Brotli 压缩太慢

https://images.jiaoben.net//  解决方案:选择性使用 Brotli
https://images.jiaoben.netcompression({
  https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
  https://images.jiaoben.netthreshold: https://images.jiaoben.net50000,  https://images.jiaoben.net// 50KB 以上才用 Brotli
  https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/
})

https://images.jiaoben.net// 小文件继续用 Gzip
https://images.jiaoben.netcompression({
  https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
  https://images.jiaoben.netthreshold: https://images.jiaoben.net10240,  https://images.jiaoben.net// 10-50KB 用 Gzip
  https://images.jiaoben.netfilter: https://images.jiaoben.net/.(js|css)$/
})

问题四:CDN 不支持 Brotli

https://images.jiaoben.net#  解决方案:同时生成 Gzip 和 Brotli
location /assets {
    https://images.jiaoben.net# 优先尝试 Brotli
    try_files https://images.jiaoben.net$uri.br https://images.jiaoben.net$uri.gz https://images.jiaoben.net$uri =404;
    
    https://images.jiaoben.net# 根据 Accept-Encoding 返回正确的 Content-Encoding
    https://images.jiaoben.netif (https://images.jiaoben.net$http_accept_encoding ~* br) {
        add_header Content-Encoding br;
    }
    https://images.jiaoben.netif (https://images.jiaoben.net$http_accept_encoding ~* gzip) {
        add_header Content-Encoding gzip;
    }
}

生产环境优化的最佳实践

优化检查清单

  • 使用 visualizer 分析构建产物
  • 配置 manualChunks 合理拆包
  • 图片资源压缩优化
  • 启用 Gzip/Brotli 压缩
  • 配置长期缓存策略
  • 设置性能预算
  • 在 CI/CD 中集成检查
  • 定期监控 Web Vitals

配置文件模板

https://images.jiaoben.net// vite.config.ts - 生产环境优化完整配置
https://images.jiaoben.netimport { defineConfig } https://images.jiaoben.netfrom https://images.jiaoben.net'vite'
https://images.jiaoben.netimport vue https://images.jiaoben.netfrom https://images.jiaoben.net'@vitejs/plugin-vue'
https://images.jiaoben.netimport { visualizer } https://images.jiaoben.netfrom https://images.jiaoben.net'rollup-plugin-visualizer'
https://images.jiaoben.netimport { https://images.jiaoben.netViteImageOptimizer } https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-image-optimizer'
https://images.jiaoben.netimport compression https://images.jiaoben.netfrom https://images.jiaoben.net'vite-plugin-compression'

https://images.jiaoben.netexport https://images.jiaoben.netdefault https://images.jiaoben.netdefineConfig(https://images.jiaoben.net(https://images.jiaoben.net{ mode }) => ({
  https://images.jiaoben.netplugins: [
    https://images.jiaoben.netvue(),
    
    https://images.jiaoben.net// 图片压缩
    https://images.jiaoben.netViteImageOptimizer({
      https://images.jiaoben.netpng: { https://images.jiaoben.netquality: https://images.jiaoben.net75 },
      https://images.jiaoben.netjpeg: { https://images.jiaoben.netquality: https://images.jiaoben.net70 },
      https://images.jiaoben.netwebp: { https://images.jiaoben.netquality: https://images.jiaoben.net70 },
      https://images.jiaoben.netavif: { https://images.jiaoben.netquality: https://images.jiaoben.net60 }
    }),
    
    https://images.jiaoben.net// Gzip 压缩
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'gzip',
      https://images.jiaoben.netext: https://images.jiaoben.net'.gz',
      https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
    }),
    
    https://images.jiaoben.net// Brotli 压缩
    https://images.jiaoben.netcompression({
      https://images.jiaoben.netalgorithm: https://images.jiaoben.net'brotliCompress',
      https://images.jiaoben.netext: https://images.jiaoben.net'.br',
      https://images.jiaoben.netthreshold: https://images.jiaoben.net10240
    }),
    
    https://images.jiaoben.net// 构建分析(只在需要时开启)
    process.https://images.jiaoben.netenv.https://images.jiaoben.netANALYZE && https://images.jiaoben.netvisualizer({
      https://images.jiaoben.netopen: https://images.jiaoben.nettrue,
      https://images.jiaoben.netfilename: https://images.jiaoben.net'dist/stats.html',
      https://images.jiaoben.netgzipSize: https://images.jiaoben.nettrue,
      https://images.jiaoben.netbrotliSize: https://images.jiaoben.nettrue
    })
  ].https://images.jiaoben.netfilter(https://images.jiaoben.netBoolean),
  
  https://images.jiaoben.netbuild: {
    https://images.jiaoben.nettarget: https://images.jiaoben.net'es2015',
    https://images.jiaoben.netminify: https://images.jiaoben.net'terser',
    https://images.jiaoben.netterserOptions: {
      https://images.jiaoben.netcompress: {
        https://images.jiaoben.netdrop_console: mode === https://images.jiaoben.net'production',
        https://images.jiaoben.netdrop_debugger: https://images.jiaoben.nettrue
      }
    },
    
    https://images.jiaoben.netrollupOptions: {
      https://images.jiaoben.netoutput: {
        https://images.jiaoben.netentryFileNames: https://images.jiaoben.net'assets/[name].[hash].js',
        https://images.jiaoben.netchunkFileNames: https://images.jiaoben.net'assets/chunks/[name].[hash].js',
        https://images.jiaoben.netassetFileNames: https://images.jiaoben.net'assets/[ext]/[name].[hash].[ext]',
        
        https://images.jiaoben.netmanualChunks(https://images.jiaoben.netid) {
          https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'node_modules')) {
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'vue')) https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-vue'
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'element-plus') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'antd')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-ui'
            }
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'echarts') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'd3')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-charts'
            }
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'lodash') || id.https://images.jiaoben.netincludes(https://images.jiaoben.net'dayjs')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-utils'
            }
            https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'monaco-editor')) {
              https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-monaco'
            }
            https://images.jiaoben.netreturn https://images.jiaoben.net'vendor-other'
          }
          
          https://images.jiaoben.netif (id.https://images.jiaoben.netincludes(https://images.jiaoben.net'/src/views/')) {
            https://images.jiaoben.netconst match = id.https://images.jiaoben.netmatch(https://images.jiaoben.net//src/views/([^/]+)/)
            https://images.jiaoben.netif (match) https://images.jiaoben.netreturn https://images.jiaoben.net`page-https://images.jiaoben.net${match[https://images.jiaoben.net1]}`
          }
        }
      }
    },
    
    https://images.jiaoben.netchunkSizeWarningLimit: https://images.jiaoben.net500,
    https://images.jiaoben.netsourcemap: mode !== https://images.jiaoben.net'production',
    https://images.jiaoben.netmanifest: https://images.jiaoben.nettrue
  }
}))

性能目标参考

指标优秀一般
首屏 JS 体积< 200KB200-500KB> 500KB
总构建体积< 2MB2-5MB> 5MB
图片体积占比< 30%30-50%> 50%
压缩率> 70%50-70%< 50%
缓存命中率> 80%50-80%< 50%
FCP< 1.5s1.5-2.5s> 2.5s
LCP< 2.5s2.5-4s> 4s

三个核心原则

  1. 测量优先:没有数据的优化是盲目的
  2. 渐进改进:每次只优化一个指标
  3. 用户优先:始终以用户体验为导向

结语

优化的终极目标是让用户感受不到加载的存在。当用户打开我们的应用时,内容瞬间呈现,交互立即响应,这就说明我们的优化成功了!

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!