阅读视图

发现新文章,点击刷新页面。

Next项目中静态资源的压缩、优化

前言

几个月前从0到1搭建落地了现在Next框架的项目,做了很多项目处理,现在想整理一下整个过程,这一次主要是对Next项目中静态资源(图片、打包的静态css、js)的压缩、优化,做个记录

实现了什么

  1. 存放在public(图片)在commit的时候,会通过tinify进行压缩之后再提交
  2. 压缩后的public(静态资源),会在jenkins打包的过程统一上传至CF的桶中形成CDN链接url
  3. url通过配置next.config自定义loaderFile映射到Image组件src

过程

commit进行tinify压缩(实现1)

npm包:husky、lint-staged、tinify

  • 触发点:.husky/pre-commit 调用 npx lint-staged,所以每次 git commit 都会跑 lint-staged。

38a8ecd4-9f15-41bd-b913-209fb22b4c34.png

  • 匹配规则:package.json 里的 lint-staged 配置,当前匹配 *.png, *.jpg, *.jpeg

image.png

  • 压缩逻辑:使用 TinyPNG(tinify),读取 .env 的 TINIFY_KEY,对传入的文件(由 lint-staged 提供的已暂存文件列表)逐个压缩并覆盖原文件。

image.png

直接覆盖原文件 image.png

  • 完整代码,需要在 .env 设置 TINIFY_KEY
import fs from "fs"
import path from "path"

import dotenv from "dotenv"
import tinify from "tinify"

// 读取 .env 文件中的 TINIFY_KEY
dotenv.config()

tinify.key = process.env.TINIFY_KEY

function formatFileSize(bytes) {
  if (bytes < 1024) return bytes + " B"
  else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB"
  else return (bytes / (1024 * 1024)).toFixed(2) + " MB"
}

async function compressImage(filePath) {
  const ext = path.extname(filePath).toLowerCase()
  const originalSize = fs.statSync(filePath).size

  try {
    // 只处理 jpg/jpeg/png/webp
    if (!/\.(jpe?g|png|webp)$/i.test(ext)) {
      console.log(`跳过不支持的文件类型: ${filePath}`)
      return
    }
    await tinify.fromFile(filePath).toFile(filePath)
    const compressedSize = fs.statSync(filePath).size

    if (compressedSize < originalSize) {
      const savings = originalSize - compressedSize
      const savingsPercent = ((savings / originalSize) * 100).toFixed(2)
      console.log(
        `✅ ${path.basename(filePath)} - 原始: ${formatFileSize(originalSize)} → ` +
          `压缩后: ${formatFileSize(compressedSize)} (减少: ${formatFileSize(savings)}, ${savingsPercent}%)`
      )
    } else {
      console.log(
        `⚠️  ${path.basename(filePath)} - 压缩后变大,已跳过 ` + `(原始: ${formatFileSize(originalSize)}${formatFileSize(compressedSize)})`
      )
    }
  } catch (err) {
    console.error(`❌ 压缩失败: ${filePath}`, err.message)
  }
}

// 获取 lint-staged 传入的文件列表
const files = process.argv.slice(2).filter(f => /\.(jpe?g|png|webp)$/i.test(f))

if (files.length === 0) {
  console.log("ℹ️ 没有需要压缩的图片文件")
  process.exit(0)
}

Promise.all(files.map(compressImage)).then(() => {
  console.log("✨ 图片压缩完成")
})

jenkins上传CDN

准备:Jenkins、@aws-sdk/client-s3

  • 构建前去选择是否需要上传,毕竟上传过一次之后就不用在上传了

image.png

  • 在Build Steps的时候写一个shell脚本去执行打包,大致就是在build后之后,去执行自己在项目中写到upload.js,js主要去上传库中public文件夹下的所有静态资源
  • 其实也不止这种资源性的,我们打包后的static文件夹下的也可以上传CDN,我们在nginx做一个映射就可以

image.png

  • 实现代码
  1. 初始化 S3 / CloudFront 与策略控制,增量上传还是全量上传,对比远端 Metadata["file-hash"] 与本地 md5,未变则跳过上传 image.png

  2. 遍历与过滤:递归 public/,跳过 public/pwa/,将 bar.png 上传为 bar.png。上传时用 PutObject,设置 ContentType(mime-types 识别)和元数据 file-hash 为 MD5。

image.png

image.png

上传static

image.png

  1. CDN 刷新:收集本次处理的 key 列表,若 ONLY_REFRESH_IMAGES=true 则只保留图片后缀(png/jpg/jpeg/gif/webp/svg/bmp/ico)。为每批(≤3000)调用 CloudFront CreateInvalidation,路径前加 /,callerReference 用时间戳+批次保证唯一。

image.png

Image的loader

那么接下来让我们的url上自动拼接上CND域名前缀到Image组件上,Image本身用法还是不变

<Image src='/a/b.png' alt='online' ... />

最终networl看到的
<Image src='https://xx.com/a/b.png' alt='xx' ... />

next.config image.png

imageLoader.ts 具体咋用可以看文档,功能就是帮你拼接前缀到Image到src上,仍然保留w={width}&q={quality || 75},前提是你的CDN支持配置

image.png

总结

  • 无,还有好多可以写hh
❌