Vite 构建层面的图片优化:从压缩到转换

前言

图片资源是 Web 应用中最大的性能负担。据统计,图片通常占据网页总带宽的 70% 以上 ,直接影响着 LCP(最大内容绘制)、FCP(首次内容绘制)等核心性能指标。一个未经优化的图片策略,可能导致首屏加载时间增加数秒。

Vite 作为现代前端构建工具,提供了强大的图片优化能力。本文将深入探讨从压缩算法原理到实际配置的全链路优化方案,并手写一个简单的图片压缩插件。

为什么图片是性能的头号杀手?

一个真实的问题

javascript 复制代码
// 假设我们的页面有这些图片
const images = [
  { name: 'hero-banner.jpg', size: '850KB' },   // 首屏大图
  { name: 'product-1.jpg', size: '320KB' },     // 商品图
  { name: 'product-2.jpg', size: '310KB' },     // 商品图
  { name: 'product-3.jpg', size: '315KB' },     // 商品图
  { name: 'icon-home.png', size: '45KB' },      // 图标
  { name: 'icon-user.png', size: '42KB' },      // 图标
  { name: 'logo.png', size: '120KB' }           // Logo
]

上述图片有 2MB,需要几秒才能加载完;而通常情况下,我们的用户会在 2 秒内就直接关掉页面!

图片优化的核心目标

目标1:减小体积

  • 压缩:丢掉人眼看不到的细节
  • 转换:用更高效的格式(WebP、AVIF)
  • 结果:体积减少 30-70%

目标2:减少请求

  • 雪碧图:把多个小图标合并成一个
  • 内联:小图片变成代码,不发起请求
  • 结果:请求数从 N 降到 1

目标3:加速加载

  • 懒加载:只加载用户看得到的
  • 预加载:提前加载即将看到的
  • 结果:首屏加载快,后面加载不影响

图片压缩 - 让每张图都"瘦身"

图片压缩工具的核心区别在于有损与无损压缩算法 以及实现语言

压缩算法对比

压缩类型 原理 适用场景 代表工具
无损压缩 优化编码结构,移除元数据,像素完全不变 Logo、图标、法律文档 jpegtran, SVGO
有损压缩 丢弃人眼不易察觉的颜色信息 照片、UI 背景图 pngquant, WebP

Sharp 与 Imagemin 的压缩工具对比

Sharp

  • 基于 libvips,使用 C 语言编写,Node.js 绑定
  • 优势:处理速度极快(比 ImageMagick 快 4-5 倍),内存占用低
  • 适用:实时处理、批量转换、现代格式(AVIF/WebP)生成

Imagemin

  • 基于独立的二进制工具(mozjpeg、pngquant、SVGO)
  • 优势:插件生态丰富,配置精细,社区成熟
  • 适用:构建时优化,精细控制每种格式的压缩参数

1.3 有损 vs 无损的实战选择

javascript 复制代码
// PNG 有损压缩(pngquant)- 适合 UI 图标
imageminPngquant({
  quality: [0.6, 0.8],  // 60-80% 质量,人眼几乎无感知
  speed: 4               // 平衡压缩率与速度
})

// JPEG 无损压缩(jpegtran)- 适合需要绝对精度的图片
imageminJpegtran({
  progressive: true,      // 渐进式 JPEG,提升用户体验
  arithmetic: false       // 使用霍夫曼编码
})

// SVG 优化(SVGO)- 纯文本优化,移除冗余
imageminSvgo({
  plugins: [
    { name: 'removeViewBox', active: false },  // 保留 viewBox 以便缩放
    { name: 'cleanupIDs', active: true }       // 移除冗余 ID
  ]
})

实践建议

  • 对用户上传内容(如头像)使用有损压缩,设置 quality: 75-85
  • 对品牌 Logo 优先尝试无损方案,再根据体积决定是否启用有损
  • SVG 是文本格式,通过移除注释、空格和元数据可减少 30-50% 体积

格式转换 - 用更快的格式

为什么需要 WebP/AVIF?

格式 相比 JPEG 体积减少 浏览器支持
WebP 30% 95%+(Chrome/Firefox/Edge 全支持,Safari 14+)
AVIF 50% 90%+(基于 AV1 编码,压缩率更高)

使用 vite-plugin-imagemin 实现自动转换

javascript 复制代码
// vite.config.js
import viteImagemin from 'vite-plugin-imagemin'

export default {
  plugins: [
    viteImagemin({
      // 有损压缩配置
      gifsicle: {
        optimizationLevel: 3,  // 1-3,级别越高压缩越强
        colors: 64              // 减少颜色数以压缩体积
      },
      // PNG 有损压缩
      pngquant: {
        quality: [0.65, 0.8],   // 最小/最大质量范围
        speed: 4                 // 1(慢)-11(快)
      },
      // JPEG 无损优化
      jpegtran: {
        progressive: true        // 生成渐进式 JPEG
      },
      // SVG 优化
      svgo: {
        plugins: [
          { name: 'removeViewBox', active: false },
          { name: 'cleanupIDs', active: true }
        ]
      },
      // WebP 转换
      webp: {
        quality: 80,              // 有损质量 (0-100)
        lossless: false           // 是否无损模式
      }
    })
  ]
}

自动生成 WebP 和 AVIF

javascript 复制代码
// vite.config.js
import viteImagemin from 'vite-plugin-imagemin'

export default {
  plugins: [
    viteImagemin({
      // 生成 WebP 版本
      webp: {
        quality: 80,
        lossless: false
      },
      
      // 生成 AVIF 版本(更高压缩率)
      avif: {
        quality: 60,
        lossless: false
      }
    })
  ]
}

自动降级策略:<picture> 元素

现代浏览器可以通过 <picture> 元素支持内容协商,实现平滑降级:

html 复制代码
<picture>
  <!-- 浏览器按顺序匹配第一个支持的格式 -->
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="fallback" loading="lazy">
</picture>

工作原理

  1. 浏览器检查 Accept 请求头
  2. 依次尝试 AVIF → WebP → JPEG
  3. 只下载第一个支持的格式

Nginx/CDN 层面的内容协商

bash 复制代码
# nginx.conf
map $http_accept $img_format {
  default jpg;
  ~*avif avif;
  ~*webp webp;
}

server {
  location ~* ^/images/(.*)$ {
    add_header Vary "Accept";  # 告知 CDN 根据 Accept 头缓存不同版本
    try_files /images/$1.$img_format /images/$1.jpg =404;
  }
}

雪碧图 - 减少请求数

为什么需要雪碧图?

对于大量的小图标(尤其是 SVG)会导致过多的 HTTP 请求。浏览器对并发请求有限制(通常 6-8 个),过多的请求会造成加载排队 。

假设页面有 100 个 SVG 图标,将会产生 100 个 HTTP 请求 。

雪碧图的工作原理

graph LR A[多个SVG图标] --> B[合并为单个SVG文件] B --> C[每个图标作为 symbol] C --> D[通过 use 引用]

vite-plugin-sprites 配置示例

javascript 复制代码
// vite.config.js
import { createSpritesPlugin } from 'vite-plugin-sprites'

export default {
  plugins: [
    createSpritesPlugin({
      // 图标目录
      input: 'src/assets/icons',
      // 输出配置
      output: {
        filename: 'sprite.[hash].svg',  // 添加哈希用于缓存
        prefix: 'icon-',                 // symbol ID 前缀
        css: true                         // 同时生成 CSS 文件
      },
      // 优化选项
      svgo: true,  // 对合并后的 SVG 进行优化
      padding: 2   // 图标间距
    })
  ]
}

雪碧图的使用方式

html 复制代码
<!-- 生成的雪碧图包含所有 symbol -->
<svg style="display: none;">
  <symbol id="icon-home" viewBox="0 0 24 24">
    <!-- 图标路径 -->
  </symbol>
  <symbol id="icon-user" viewBox="0 0 24 24">
    <!-- 图标路径 -->
  </symbol>
</svg>

<!-- 使用时只需一个 HTTP 请求 -->
<svg class="icon">
  <use xlink:href="#icon-home"></use>
</svg>

优点

  • 只会发起 1 次 HTTP 请求(对比之前的 100 次)
  • SVG 作为矢量图形,不会受文字抗锯齿算法影响
  • 可通过 CSS 控制颜色和大小

内联阈值 - 小图片变代码

什么是内联?

text 复制代码
原始图片:logo.png (3KB)
    ↓
转换成 Base64:data:image/png;base64,iVBORw0KGgo...
    ↓
嵌入到 JS/CSS 中
    ↓
不发 HTTP 请求,直接显示

Vite 的默认策略

Vite 内置了智能的资源内联机制 :

  • 体积 < 4KB:转换为 Base64 内联,避免额外 HTTP 请求
  • 体积 ≥ 4KB:提取为单独文件,通过哈希命名

为什么是 4KB?

  • 4KB 是 HTTP/1.1 的典型阈值
  • 小于 4KB 的文件,内联比发请求更快

内联阈值的配置

javascript 复制代码
// vite.config.js
export default {
  build: {
    // 设置内联阈值 (单位:字节)
    assetsInlineLimit: 8 * 1024,  // 8KB
    
    // 静态资源输出目录
    assetsDir: 'static',
    
    rollupOptions: {
      output: {
        assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
      }
    }
  }
}

阈值怎么选?

阈值 优点 缺点 适用场景
4KB(默认) 避免过多内联导致 JS/CSS 膨胀 小文件仍需请求 通用项目
8KB 减少更多请求数 增加 JS/CSS 体积 移动端、HTTP/1.1 场景
0(禁用) 所有资源单独文件 请求数爆炸 HTTP/2 多路复用场景

重要注意事项

1. 库模式下的特殊行为

如果指定了 build.lib,内联阈值将被忽略;此时无论文件大小,资源都会被内联!

2. SVG 的特殊处理

  • Vite 不支持 SVG 转 Base64(设计如此)
  • 原因:Base64 会使 SVG 体积增大 33-36%,直接使用 UTF-8 Data URL 更优
  • 替代方案:
javascript 复制代码
import iconSvg from './icon.svg?raw';  // 获取纯文本

const toSVGDataUrl = (str) => {
  // 推荐使用 UTF-8 编码,而非 Base64
  return `data:image/svg+xml;utf8,${encodeURIComponent(str)}`;
};

缓存策略 - 让图片只下载一次

文件名哈希的原理

javascript 复制代码
// 构建前的文件名
logo.png
banner.jpg

// 构建后的文件
logo.abc123.png   // 哈希基于内容生成
logo.def456.png   // 图片内容变化,哈希变化

优势

  • 设置长期缓存:Cache-Control: max-age=31536000, immutable
  • 内容变化时,URL 自动变化,强制客户端更新
  • 当浏览器访问时:
    • 这个 URL 我没见过 → 重新下载
    • 这个 URL 我见过 → 直接用缓存

Vite 的哈希配置

javascript 复制代码
// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        // 静态资源命名规则
        assetFileNames: (assetInfo) => {
          // 根据文件类型和内容生成哈希
          const ext = assetInfo.name.split('.').pop()
          return `assets/${ext}/[name]-[hash].[ext]`
        }
      }
    },
    // 生成 manifest.json,方便服务器端映射
    manifest: true
  }
}

CDN 缓存刷新策略

javascript 复制代码
// 构建时生成的 manifest 文件
{
  "src/assets/logo.png": "assets/png/logo-abc123.png",
  "src/assets/banner.jpg": "assets/jpg/banner-def456.jpg"
}

// 在代码中动态获取
const getAssetUrl = (src) => {
  return manifest[src] || src
}

Nginx 缓存配置

bash 复制代码
# nginx.conf
location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|avif)$ {
  # 长期缓存
  expires 1y;
  
  # 告诉浏览器可以永久缓存
  add_header Cache-Control "public, immutable";
  
  # 如果有版本参数,去掉
  if ($args ~* "^v=") {
    rewrite ^(.*)$ $1? permanent;
  }
}

手写一个简单的 Vite 图片压缩插件

插件模板与钩子

Vite 插件的基本结构 :

typescript 复制代码
// vite-plugin-image-optimizer.ts
import type { PluginOption } from 'vite'
import fs from 'fs/promises'
import path from 'path'
import imagemin from 'imagemin'
import imageminPngquant from 'imagemin-pngquant'
import imageminJpegtran from 'imagemin-jpegtran'
import imageminSvgo from 'imagemin-svgo'

interface PluginOptions {
  pngQuality?: [number, number]
  jpegQuality?: number
  svgOptimize?: boolean
  include?: RegExp
  exclude?: RegExp
}

export default function viteImageOptimizer(
  options: PluginOptions = {}
): PluginOption {
  const {
    pngQuality = [0.6, 0.8],
    jpegQuality = 80,
    svgOptimize = true,
    include = /\.(png|jpe?g|svg)$/,
    exclude = /node_modules/
  } = options

  return {
    name: 'vite-plugin-image-optimizer',
    
    // 在构建结束时执行优化
    async closeBundle() {
      console.log('🖼️ 开始优化图片资源...')
      
      // 需要优化的文件目录
      const buildDir = path.resolve('dist/assets')
      
      try {
        await fs.access(buildDir)
      } catch {
        console.log('没有找到图片资源')
        return
      }
      
      const files = await imagemin([`${buildDir}/**/*`], {
        destination: buildDir,
        plugins: [
          // PNG 优化
          imageminPngquant({
            quality: pngQuality,
            speed: 4
          }),
          // JPEG 优化
          imageminJpegtran({
            progressive: true
          }),
          // SVG 优化
          ...(svgOptimize ? [imageminSvgo({
            plugins: [
              { name: 'removeViewBox', active: false },
              { name: 'cleanupIDs', active: true }
            ]
          })] : [])
        ]
      })
      
      console.log(`✅ 已优化 ${files.length} 个图片文件`)
      
      // 输出压缩统计
      let totalSaved = 0
      for (const file of files) {
        const originalPath = file.history[0]
        const originalSize = (await fs.stat(originalPath)).size
        const saved = originalSize - file.data.length
        totalSaved += saved
        
        if (saved > 0) {
          console.log(`   ${path.basename(originalPath)}: 减少 ${(saved / 1024).toFixed(2)}KB (${((saved / originalSize) * 100).toFixed(1)}%)`)
        }
      }
      
      console.log(`📊 总计节省: ${(totalSaved / 1024 / 1024).toFixed(2)}MB`)
    }
  }
}

插件配置与使用

javascript 复制代码
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import viteImageOptimizer from './vite-plugin-image-optimizer'

export default defineConfig({
  plugins: [
    vue(),
    viteImageOptimizer({
      pngQuality: [0.65, 0.8],  // PNG 压缩质量范围
      jpegQuality: 75,            // JPEG 压缩质量
      svgOptimize: true,          // 是否优化 SVG
      include: /\.(png|jpe?g|svg)$/,
      exclude: /node_modules|already-optimized/
    })
  ],
  
  build: {
    assetsInlineLimit: 8 * 1024,  // 8KB 以下内联
    rollupOptions: {
      output: {
        assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
      }
    }
  }
})

进阶功能:生成 WebP 副本

typescript 复制代码
// 扩展插件,添加 WebP 生成功能
import imageminWebp from 'imagemin-webp'

// 在 closeBundle 中添加
async function generateWebpCopies() {
  const files = await imagemin([`${buildDir}/**/*.{png,jpg,jpeg}`], {
    destination: buildDir,
    plugins: [
      imageminWebp({
        quality: 80,
        lossless: false
      })
    ]
  })
  
  console.log(`✅ 已生成 ${files.length} 个 WebP 副本`)
}

最佳实践清单

配置清单

  • 安装 vite-plugin-imagemin 并配置压缩参数
  • 配置 build.assetsInlineLimit 为 8KB
  • 使用 <picture> 实现 WebP/AVIF 降级
  • 小图标合并成雪碧图
  • 配置文件名哈希(Vite 默认已做)
  • 配置 CDN 长期缓存

优化策略矩阵

优化维度 推荐配置 收益
压缩算法 PNG: pngquant quality 65-80 JPEG: jpegtran progressive SVG: SVGO 减少 30-70% 体积
格式转换 同时生成 WebP + AVIF WebP 比 JPEG 小 30%
雪碧图 合并小图标,使用 <symbol> 请求数从 N 降到 1
内联阈值 8KB(HTTP/2 可适当提高) 减少 20-30% 请求
缓存策略 哈希命名 + max-age=1y 缓存命中率 90%+

快速落地清单

  1. 批量转换存量图片为 WebP/AVIF,保留原格式作为回退
  2. 配置 build.assetsInlineLimit 为 8KB(根据项目特点调整)
  3. 集成图片压缩插件,自动优化新增图片
  4. 合并小图标为雪碧图,减少请求数
  5. 配置 CDN 缓存:max-age=31536000 + immutable
  6. 通过 Lighthouse 审计,设置性能预算告警

结语

图片优化是投入产出比最高的性能优化手段。一个配置得当的 Vite 构建流程,可以在完全不改变开发体验的前提下,让图片加载耗时减少 40-60% ,首屏加载速度提升 30% 以上 。

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

相关推荐
Irene19911 小时前
Vue3 的 Proxy 与 Vue2 的 Object.defineProperty 的对比
vue.js·proxy·defineproperty
hashiqimiya1 小时前
vue项目组装-路由-文件修改地方
前端·javascript·vue.js
回到原点的码农2 小时前
TypeScript 与后端开发Node.js
javascript·typescript·node.js
Mike_jia2 小时前
ChatClaw:5 分钟打造你的个人 AI 智能体
前端
CodeSheep2 小时前
王自如公开招聘01号员工,这要求有多离谱?
前端·后端·程序员
亿元程序员2 小时前
“我要验牌”很火吗?我特意写了个Shader去验...
前端
@yanyu6662 小时前
04vue3基础
前端·javascript·vue.js
IT_陈寒2 小时前
JavaScript 闭包陷阱:90%开发者踩过的5个坑,你中招了吗?
前端·人工智能·后端
SuperEugene2 小时前
Vue3 Props 传参实战规范:必传校验 + 默认值 + 类型标注,避开 undefined / 类型混用坑|Vue 组件与模板规范篇
前端·javascript·vue.js·前端框架