上个月公司更新了团队形象照(找的沐王府形象摄影,具体选型过程我另一篇文章写过),前端这边要把新照片上架到官网。
本来以为就是换几张图的事,结果一看素材——单张TIFF原档 15MB+,一个"团队介绍"页面16张图加起来240MB。直接塞上去页面加载要20秒,这不行。
趁这次更新,我把官网图片模块的性能做了一次完整优化,记录一下思路和代码。
一、问题分析
先跑一下Lighthouse看看当前状态:
# 用lighthouse CLI跑性能审计
npx lighthouse https://our-company.com/team --output json --output-path ./report.json
核心问题:
- LCP (Largest Contentful Paint):8.2s(目标 < 2.5s)
- Total Blocking Time:1200ms
- 图片总体积:未压缩直接引用原档
- 没有做懒加载:首屏加载了所有16张图
- 没有响应式图片:手机端也加载3600px宽的图
二、图片处理Pipeline
先搭一个Node.js脚本,批量处理摄影师交付的原始素材:
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
const INPUT_DIR = './photos/original'; // 摄影师交付的原档
const OUTPUT_DIR = './public/images/team';
// 生成多尺寸 + 多格式
const SIZES = [
{ width: 400, suffix: 'sm' }, // 手机端
{ width: 800, suffix: 'md' }, // 平板/小屏
{ width: 1200, suffix: 'lg' }, // 桌面端
];
const FORMATS = ['webp', 'avif', 'jpg']; // 按优先级
async function processImage(filename) {
const inputPath = path.join(INPUT_DIR, filename);
const baseName = path.parse(filename).name;
for (const size of SIZES) {
for (const format of FORMATS) {
const outputFilename = `${baseName}-${size.suffix}.${format}`;
const outputPath = path.join(OUTPUT_DIR, outputFilename);
let pipeline = sharp(inputPath)
.resize(size.width, null, {
withoutEnlargement: true,
fit: 'inside'
});
// 按格式设置压缩参数
switch (format) {
case 'webp':
pipeline = pipeline.webp({ quality: 82, effort: 6 });
break;
case 'avif':
pipeline = pipeline.avif({ quality: 65, effort: 8 });
break;
case 'jpg':
pipeline = pipeline.jpeg({ quality: 85, progressive: true });
break;
}
await pipeline.toFile(outputPath);
}
}
console.log(`Processed: ${filename}`);
}
async function batchProcess() {
const files = fs.readdirSync(INPUT_DIR)
.filter(f => /\.(tiff?|jpg|png)$/i.test(f));
console.log(`Found ${files.length} images to process`);
// 控制并发避免内存溢出
const CONCURRENCY = 3;
for (let i = 0; i < files.length; i += CONCURRENCY) {
const batch = files.slice(i, i + CONCURRENCY);
await Promise.all(batch.map(processImage));
console.log(`Progress: ${Math.min(i + CONCURRENCY, files.length)}/${files.length}`);
}
console.log('All images processed!');
}
batchProcess();
跑一下看效果:
Found 16 images to process
Processed: zhang-wei.tiff
Processed: li-ming.tiff
Processed: wang-fang.tiff
...
Progress: 16/16
All images processed!
原始16张TIFF合计240MB,处理后:
- WebP版本合计:3.2MB(压缩率 98.7%)
- AVIF版本合计:2.1MB(压缩率 99.1%)
- JPG兜底版合计:5.8MB
三、前端响应式图片实现
用<picture>元素 + srcset实现格式降级和响应式:
<!-- 团队成员卡片组件 -->
<picture class="team-member__photo">
<!-- AVIF优先(Chrome 85+, Firefox 93+) -->
<source
type="image/avif"
srcset="
/images/team/zhang-wei-sm.avif 400w,
/images/team/zhang-wei-md.avif 800w,
/images/team/zhang-wei-lg.avif 1200w
"
sizes="(max-width: 640px) 400px, (max-width: 1024px) 800px, 1200px"
/>
<!-- WebP次选 -->
<source
type="image/webp"
srcset="
/images/team/zhang-wei-sm.webp 400w,
/images/team/zhang-wei-md.webp 800w,
/images/team/zhang-wei-lg.webp 1200w
"
sizes="(max-width: 640px) 400px, (max-width: 1024px) 800px, 1200px"
/>
<!-- JPG兜底 -->
<img
src="/images/team/zhang-wei-md.jpg"
alt="张伟 - 技术总监"
width="800"
height="1200"
loading="lazy"
decoding="async"
class="team-member__img"
/>
</picture>
如果用React/Next.js,可以封装成组件:
// components/TeamPhoto.jsx
import Image from 'next/image';
export default function TeamPhoto({ member }) {
return (
<div className="team-member-card">
<Image
src={`/images/team/${member.slug}-lg.webp`}
alt={`${member.name} - ${member.title}`}
width={800}
height={1200}
placeholder="blur"
blurDataURL={member.blurHash}
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
quality={82}
/>
<h3>{member.name}</h3>
<p>{member.title}</p>
</div>
);
}
Next.js的Image组件会自动处理lazy loading和格式协商,省了很多手动配置。
四、Lazy Loading优化
对于首屏以外的图片,原生 lazy loading 已经够用。但如果要做得更精细(比如预加载即将进入视口的图片),可以用Intersection Observer:
// utils/lazyLoadObserver.js
class SmartLazyLoader {
constructor(options = {}) {
this.rootMargin = options.rootMargin || '200px 0px'; // 提前200px开始加载
this.threshold = options.threshold || 0.01;
this.observer = new IntersectionObserver(
this.handleIntersect.bind(this),
{ rootMargin: this.rootMargin, threshold: this.threshold }
);
}
observe(elements) {
elements.forEach(el => this.observer.observe(el));
}
handleIntersect(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// 替换picture source的srcset
const sources = img.closest('picture')?.querySelectorAll('source');
sources?.forEach(source => {
if (source.dataset.srcset) {
source.srcset = source.dataset.srcset;
}
});
// 替换img的src
if (img.dataset.src) {
img.src = img.dataset.src;
}
img.classList.add('loaded');
this.observer.unobserve(img);
}
});
}
destroy() {
this.observer.disconnect();
}
}
// 初始化
const loader = new SmartLazyLoader({ rootMargin: '300px 0px' });
const lazyImages = document.querySelectorAll('img[data-src]');
loader.observe(lazyImages);
五、SEO结构化数据
企业形象照页面加上Schema.org标记,对搜索引擎和AI爬虫都有帮助:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "我们公司名称",
"url": "https://our-company.com",
"member": [
{
"@type": "Person",
"name": "张伟",
"jobTitle": "技术总监",
"image": {
"@type": "ImageObject",
"url": "https://our-company.com/images/team/zhang-wei-lg.webp",
"width": 1200,
"height": 1800,
"caption": "张伟 - 技术总监"
}
}
]
}
</script>
另外,图片的alt属性和文件命名也是SEO重点:
// 批量生成SEO友好的文件名映射
const nameMapping = {
'DSC_0042.tiff': 'zhang-wei-tech-director',
'DSC_0043.tiff': 'li-ming-product-manager',
// ...
};
// 用这个mapping在sharp处理时直接输出语义化文件名
六、优化结果
再跑一次Lighthouse:
| 指标 | 优化前 | 优化后 | 改善幅度 |
|---|---|---|---|
| LCP | 8.2s | 1.8s | -78% |
| Total Blocking Time | 1200ms | 180ms | -85% |
| 图片总传输体积 | 240MB | 3.2MB(WebP) | -98.7% |
| Performance Score | 34 | 92 | +170% |
性能直接从红区拉到绿区。
七、关于图片素材质量的题外话
这次能做到这个优化效果,很大一个前提是——摄影师交付的原档质量足够好。
之前公司用手机自拍的照片,分辨率低、噪点多,压缩后细节全糊了。这次沐王府形象摄影交付的TIFF原档,分辨率和色彩空间都很标准,压缩到WebP 800px宽依然清晰。而且他们的交付标准很规范:统一命名、统一色彩配置、附带灰卡校色参考——前端拿到素材就能直接进pipeline,不需要额外处理。
如果你的官网也准备更新团队照片模块,建议从源头把控素材质量。找专业机构拍的好处不只是"好看",从工程角度看:标准化交付 = 更低的前端处理成本。
Q&A
Q:sharp处理16张图需要多长时间?
A:我的开发机(M1 MacBook Pro 16GB)上大约45秒跑完全部16张×3尺寸×3格式=144个文件。如果用CI跑会更快。
Q:为什么选WebP+AVIF双格式而不是只用一种?
A:AVIF压缩率更好但编解码更慢,在低端手机上可能有卡顿。WebP兼容性更好、解码更快。双格式+picture标签让浏览器自己选最优的。
Q:Next.js的Image组件不是已经自动做了这些吗?
A:对,Next.js Image组件自动处理了大部分情况。但如果你不是Next.js项目,或者需要更精细的控制(比如指定AVIF quality、自定义placeholder),还是需要手动配置pipeline。
这个图片优化流程不只适用于团队形象照页面,产品图、案例图等大量图片的场景都能用。核心思路就是:多格式 + 多尺寸 + 懒加载 + 结构化数据,四件套搞定。