文章 · 2026-03-02

给相册瘦个身:构建时缩略图预处理实战

我的个人网站 yuxu.ge 有一个我很喜欢的相册页面,用来记录和分享生活中的一些摄影作品。随着照片越攒越多,我渐渐发现一个尴尬的问题:相册页面变得越来越慢,尤其是对于网络环境不好的用户,简直是一场灾难。

最近,我终于下定决心解决这个性能瓶颈。通过一套简单的构建时预处理流水线,我成功将相册首屏的加载量从约 8.5MB 剧降到 0.7MB,性能提升了惊人的 91%。

问题在哪?病根是"大图小用"

诊断性能问题,我通常会打开 Chrome DevTools 的 Network 面板。不看不知道,一看吓一跳。在我的相册页面,虽然用户看到的是一个由许多小方格组成的图片墙,但每个方格加载的,竟然都是一张原始尺寸的图片。

我们来算一笔账:

这是一个非常恐怖的数字。用户在屏幕上看到的,可能只是一个宽度 300px 左右的缩略图格子,浏览器却在背后吭哧吭哧地下载一张 2000px 宽的"高清大图"。这完全是"杀鸡用牛刀",浪费了用户的流量和时间。

更糟糕的是,我的首页有一个"生活瞬间"的胶片滚动条,里面的图片尺寸更小,大约只有 120×80px,但它们同样在加载原始大图。这让我的首页也背上了沉重的负担。

解决方案:构建时生成缩略图

解决方案是在网站构建阶段生成缩略图。对于静态网站,无需依赖动态图片服务或复杂的服务端设置,所有处理都可以在构建时完成。

我的目标很简单:

  1. 网格视图(Grid View):加载并显示小尺寸、低文件大小的缩略图。
  2. 灯箱视图(Lightbox View):当用户点击缩略图时,再加载并显示完整尺寸的原始图片。

整个实现的核心思路是 "构建时处理" (Build-time Processing)。我的网站是静态生成的,这意味着我可以在网站构建的环节,提前把所有需要的缩略图都生成好。这样做的好处是:

我选择了强大的命令行工具 ImageMagick 来完成这个任务。具体的优化参数设定如下:

经过这番处理,效果立竿见影:

现在,让我们看看具体是怎么实现的。

实现第一步:升级 compress-gallery.sh 脚本

我的项目里原本就有一个 compress-gallery.sh 脚本,用于将相机直出的原始图片(通常很大)压缩到 2000px 宽、85% 质量,作为网站上使用的"原始大图"。我需要做的,就是在这个脚本里增加一个生成缩略图的步骤。

这是新增的核心代码:

# Thumbnail settings
THUMB_WIDTH=600
THUMB_QUALITY=75

# ... a loop over each image file `img` ...

# Naming: insert -thumb before extension
# e.g., A7C00748.JPG → A7C00748-thumb.JPG
ext="${img##*.}"
base="${img%.*}"
thumb_file="${base}-thumb.${ext}"

magick "$img" \
    -auto-orient \
    -resize "${THUMB_WIDTH}x>" \
    -quality "$THUMB_QUALITY" \
    -strip \
    -interlace Plane \
    "$thumb_file"

我们来逐行解析一下 magick 这个命令:

为了让这个脚本在日常使用中更友好,我还加入了几个辅助功能:

实现第二步:更新 build-gallery-json.js 数据源

我的相册页面不是直接读取文件目录,而是通过一个 gallery.json 文件来获取图片列表。这个 JSON 文件由一个 Node.js 脚本 build-gallery-json.js 在构建时生成。

我需要修改这个脚本,让它在生成 JSON 的时候,不仅包含原始图片的路径,也包含对应缩略图的路径。

修改后的逻辑大致如下:

// Thumbnail path construction
groupThumbs = groupFiles.map(img => {
    const dot = img.lastIndexOf('.');
    // Manually construct the thumb path to preserve extension case
    return `/_content/gallery/${year}/${event}/${img.substring(0, dot)}-thumb.${img.substring(dot + 1)}`;
});

最终生成的 gallery.json 结构从这样:

{
  "images": ["/_content/gallery/2026/.../A7C00748.JPG"]
}

变成了这样:

{
  "images": ["/_content/gallery/2026/.../A7C00748.JPG"],
  "thumbs": ["/_content/gallery/2026/.../A7C00748-thumb.JPG"]
}

这样,前端组件就能同时拿到原始图和缩略图的地址了。

实现第三步:改造前端代码

万事俱备,只欠东风。最后一步就是修改前端代码,让它真正用上我们新生成的缩略图。

相册页面 (gallery/index.html)

在渲染图片网格时,我修改了 <img> 标签的 src 属性,让它指向缩略图。同时,为了增加健壮性,我添加了一个 onerror 的 fallback 机制。

<img src="${thumb}" alt="${group.location}" loading="lazy"
     onerror="this.onerror=null;this.src='${encodedImg}'">

这里的 onerror 属性是一个很棒的保险措施。它的意思是:"如果 src 指定的缩略图加载失败(比如因为某些原因没有生成成功),那么就不要再尝试加载了(this.onerror=null),并且将 src 切换到原始大图的地址。"

这样一来,即使缩略图缺失,页面也不会显示一个破碎的图片图标,而是会优雅地降级去加载原始图片,保证了功能的完整性。

首页胶片滚动条

首页的"生活瞬间"组件也同样得到了优化。它的数据处理逻辑被修改为优先使用缩略图:

let allPairs = photos.flatMap(p =>
    p.images.map((img, i) => ({
        thumb: p.thumbs && p.thumbs[i] ? p.thumbs[i] : img,
        original: img
    }))
);

这段代码为每张图片创建了一个 {thumb, original} 对象。如果 p.thumbs[i] 存在,就用它;如果不存在,就用原始图片 img 作为 thumb 的备选。对于只有 120px 宽的胶片格子,600px 的缩略图绰绰有余,加载速度却快了几十倍。

意外之喜:头像优化

在优化完相册后,我突然想到,我的个人头像图片是不是也可以用同样的方式处理?

网站导航栏和关于页面的头像其实显示尺寸很小,但原始文件 avatar.jpg 却有 352KB。我立刻用 ImageMagick 为它生成了一个缩略图版本 avatar-thumb.jpg,大小只有 32KB,体积减少了 91%

成果展示:数据不会说谎

经过这一系列操作,我的网站性能得到了质的飞跃。让我们用数据说话:

场景 优化前 优化后 性能提升
相册首屏 (约18张图) ~8.5MB ~0.7MB ~91%
单张图片平均大小 ~469KB ~25KB ~94.7%
首页胶片条 (30张图) ~14MB ~0.75MB ~94.6%
个人头像 352KB 32KB 91%

加载时间从肉眼可见的"等待"变成了"瞬间打开",尤其是在移动设备上,体验提升极为明显。

踩坑记录:一个关于文件扩展名大小写的 Bug

build-gallery-json.js 脚本中,我最初使用了 Node.js 的 path 模块来构建缩略图路径,代码类似这样:

// The buggy version
const path = require('path');
const ext = path.extname(img);   // .JPG -> .jpg (!)
const base = path.basename(img, ext);
const thumbFile = `${base}-thumb${ext}`;

这段代码在我的 macOS(默认文件系统不区分大小写)上运行得很好。但当我把网站部署到 Linux 服务器(区分大小写)上时,问题就来了。我的原始文件名是 A7C00748.JPG(大写),但 path.extname 会返回小写的 .jpg。这导致脚本生成的缩略图路径是 ...-thumb.jpg,而实际文件是 ...-thumb.JPG,路径不匹配,导致所有缩略图 404 Not Found。

最终,我放弃了 path 模块,改用简单的字符串操作来解决:

const dot = img.lastIndexOf('.');
return `.../${img.substring(0, dot)}-thumb.${img.substring(dot + 1)}`;

这个方法能完美保留原始文件扩展名的大小写,从而解决了跨平台部署的问题。这是一个小细节,但足以让整个功能失效,值得警惕。

总结

构建时缩略图预处理是我个人网站中投入产出比最高的性能优化。图片处理完全前置到构建阶段,消除了任何运行时开销。核心代码和前端修改都很简单,onerror fallback 机制确保了容错能力。同样的模式可以应用于任何包含大量图片的静态网站。

© 2026 Yuxu Ge ·