optimize-large-images.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import sharp from 'sharp'
  2. import { readdir, stat, rename, unlink } from 'fs/promises'
  3. import { join, extname } from 'path'
  4. const IMAGE_DIR = 'src/assets/images'
  5. const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg']
  6. const SIZE_THRESHOLD = 500 * 1024 // 500KB
  7. // 计算文件大小(KB)
  8. function formatSize(bytes) {
  9. return (bytes / 1024).toFixed(2) + ' KB'
  10. }
  11. // 压缩单个图片 - 更激进的压缩策略
  12. async function compressImage(fileName) {
  13. const ext = extname(fileName).toLowerCase()
  14. const inputPath = join(IMAGE_DIR, fileName)
  15. const tempPath = join(IMAGE_DIR, 'temp_' + fileName)
  16. try {
  17. const inputStats = await stat(inputPath)
  18. const inputSize = inputStats.size
  19. // 如果文件小于 500KB,跳过
  20. if (inputSize < SIZE_THRESHOLD) {
  21. console.log(`⏭️ ${fileName} (${formatSize(inputSize)}) - 跳过`)
  22. return null
  23. }
  24. let pipeline = sharp(inputPath)
  25. // 首先调整尺寸(如果图片太大)
  26. const metadata = await pipeline.metadata()
  27. const maxWidth = 1920
  28. const maxHeight = 1080
  29. if (metadata.width > maxWidth || metadata.height > maxHeight) {
  30. pipeline = pipeline.resize(maxWidth, maxHeight, {
  31. fit: 'inside',
  32. withoutEnlargement: true
  33. })
  34. }
  35. // 根据图片类型设置更激进的压缩参数
  36. if (ext === '.png') {
  37. // PNG 转为 PNG 但使用更激进的设置
  38. pipeline = pipeline.png({
  39. compressionLevel: 9,
  40. palette: true,
  41. quality: 60,
  42. dither: 1
  43. })
  44. } else if (ext === '.jpg' || ext === '.jpeg') {
  45. pipeline = pipeline.jpeg({
  46. quality: 70,
  47. mozjpeg: true,
  48. progressive: true
  49. })
  50. }
  51. // 执行压缩到临时文件
  52. await pipeline.toFile(tempPath)
  53. const outputStats = await stat(tempPath)
  54. const outputSize = outputStats.size
  55. // 如果压缩后仍然很大,考虑转为 WebP
  56. if (outputSize > SIZE_THRESHOLD && ext === '.png') {
  57. console.log(`🔄 ${fileName} - 转为 WebP 格式`)
  58. await pipeline.webp({ quality: 75 }).toFile(tempPath)
  59. const webpStats = await stat(tempPath)
  60. console.log(` 原始:${formatSize(inputSize)} → WebP: ${formatSize(webpStats.size)}`)
  61. }
  62. // 删除原文件并重命名临时文件
  63. await unlink(inputPath)
  64. await rename(tempPath, inputPath)
  65. const finalStats = await stat(inputPath)
  66. const finalSize = finalStats.size
  67. const savings = ((inputSize - finalSize) / inputSize * 100).toFixed(1)
  68. console.log(`✅ ${fileName}`)
  69. console.log(` 原始:${formatSize(inputSize)} → 压缩后:${formatSize(finalSize)} (节省 ${savings}%)`)
  70. return { inputSize, outputSize: finalSize, savings }
  71. } catch (error) {
  72. console.error(`❌ 压缩失败 ${fileName}:`, error.message)
  73. // 清理临时文件(如果存在)
  74. try {
  75. await unlink(tempPath)
  76. } catch (e) {
  77. // 忽略
  78. }
  79. return null
  80. }
  81. }
  82. // 主函数
  83. async function main() {
  84. console.log('🚀 开始优化大图片(>500KB)...\n')
  85. const files = await readdir(IMAGE_DIR)
  86. const imageFiles = files.filter(file =>
  87. IMAGE_EXTENSIONS.includes(extname(file).toLowerCase())
  88. )
  89. if (imageFiles.length === 0) {
  90. console.log('⚠️ 未找到图片')
  91. return
  92. }
  93. // 先找出所有大于 500KB 的图片
  94. const largeImages = []
  95. for (const file of imageFiles) {
  96. const stats = await stat(join(IMAGE_DIR, file))
  97. if (stats.size > SIZE_THRESHOLD) {
  98. largeImages.push({ name: file, size: stats.size })
  99. }
  100. }
  101. if (largeImages.length === 0) {
  102. console.log('✅ 所有图片都已小于 500KB,无需进一步优化!')
  103. return
  104. }
  105. console.log(`📁 找到 ${largeImages.length} 张大图片需要优化:\n`)
  106. largeImages.forEach(img => {
  107. console.log(` - ${img.name}: ${formatSize(img.size)}`)
  108. })
  109. console.log()
  110. let totalInput = 0
  111. let totalOutput = 0
  112. let successCount = 0
  113. for (const file of imageFiles) {
  114. const result = await compressImage(file)
  115. if (result) {
  116. totalInput += result.inputSize
  117. totalOutput += result.outputSize
  118. successCount++
  119. console.log()
  120. }
  121. }
  122. if (successCount === 0) {
  123. console.log('\n✅ 没有需要优化的大图片')
  124. return
  125. }
  126. const totalSavings = ((totalInput - totalOutput) / totalInput * 100).toFixed(1)
  127. console.log('='.repeat(50))
  128. console.log(`📊 优化完成!`)
  129. console.log(` 成功:${successCount} 张`)
  130. console.log(` 原始总大小:${formatSize(totalInput)}`)
  131. console.log(` 优化后总大小:${formatSize(totalOutput)}`)
  132. console.log(` 总计节省:${totalSavings}% (${formatSize(totalInput - totalOutput)})`)
  133. console.log('='.repeat(50))
  134. }
  135. // 运行
  136. main().catch(console.error)