perf: optimize site images
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import sharp from 'sharp';
|
||||
import { glob } from 'glob';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
const SOURCE_DIR = 'image-sources';
|
||||
const PUBLIC_DIR = 'src/.vuepress/public';
|
||||
|
||||
async function getFileSize(filePath) {
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
return stats.size;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
async function optimizeImage(inputPath, outputPath, options = {}) {
|
||||
const { width, quality = 80, outputExt = '.webp' } = options;
|
||||
const originalSize = await getFileSize(inputPath);
|
||||
|
||||
let pipeline = sharp(inputPath);
|
||||
|
||||
if (width) {
|
||||
pipeline = pipeline.resize({ width, withoutEnlargement: true });
|
||||
}
|
||||
|
||||
if (outputExt === '.webp') {
|
||||
pipeline = pipeline.webp({ quality, lossless: false, force: true });
|
||||
} else if (outputExt === '.png') {
|
||||
pipeline = pipeline.png({ quality, compressionLevel: 9, force: true });
|
||||
}
|
||||
|
||||
const buffer = await pipeline.toBuffer();
|
||||
|
||||
// For standard images, if webp is larger, we'll keep it if it's the specific target like bgImage
|
||||
// but for general article images we might skip.
|
||||
const isTargeted = inputPath.includes(SOURCE_DIR);
|
||||
if (!isTargeted && buffer.length >= originalSize && outputExt === '.webp') {
|
||||
console.log(`Skipping ${path.basename(inputPath)}: Optimized size (${formatBytes(buffer.length)}) >= original (${formatBytes(originalSize)})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, buffer);
|
||||
const optimizedSize = buffer.length;
|
||||
|
||||
return {
|
||||
inputPath,
|
||||
outputPath,
|
||||
name: path.basename(inputPath),
|
||||
outputName: path.basename(outputPath),
|
||||
originalSize,
|
||||
optimizedSize,
|
||||
reduction: ((originalSize - optimizedSize) / originalSize * 100).toFixed(2) + '%'
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting reorganized image optimization...');
|
||||
const results = [];
|
||||
|
||||
// 1. Optimize Background from Source
|
||||
const bgSource = path.join(SOURCE_DIR, 'bg/bgImage.jpg');
|
||||
if (await fs.access(bgSource).then(() => true).catch(() => false)) {
|
||||
const res = await optimizeImage(bgSource, path.join(PUBLIC_DIR, 'bg/bgImage.webp'), { width: 1920, quality: 78 });
|
||||
if (res) results.push(res);
|
||||
}
|
||||
|
||||
// 2. Optimize Logo from Source
|
||||
const logoSource = path.join(SOURCE_DIR, 'logo/transparentLogo.png');
|
||||
if (await fs.access(logoSource).then(() => true).catch(() => false)) {
|
||||
const res = await optimizeImage(logoSource, path.join(PUBLIC_DIR, 'logo/transparentLogo.webp'), { width: 256, quality: 82 });
|
||||
if (res) results.push(res);
|
||||
}
|
||||
|
||||
// 3. Optimize Favicon from Source (output to public/favicon.png)
|
||||
const favSource = path.join(SOURCE_DIR, 'favicon-source.ico');
|
||||
if (await fs.access(favSource).then(() => true).catch(() => false)) {
|
||||
const res = await optimizeImage(favSource, path.join(PUBLIC_DIR, 'favicon.png'), { width: 32, outputExt: '.png' });
|
||||
if (res) results.push(res);
|
||||
}
|
||||
|
||||
// 4. Optimize Article Images (In-place WebP generation)
|
||||
const articleImages = await glob('src/**/*.{jpg,jpeg,png}', {
|
||||
ignore: ['src/.vuepress/dist/**', 'src/.vuepress/public/**']
|
||||
});
|
||||
|
||||
console.log(`Found ${articleImages.length} article images to process...`);
|
||||
for (const img of articleImages) {
|
||||
const metadata = await sharp(img).metadata();
|
||||
const outputWebp = img.replace(path.extname(img), '.webp');
|
||||
const options = { quality: 80 };
|
||||
if (metadata.width > 1600) {
|
||||
options.width = 1600;
|
||||
}
|
||||
const res = await optimizeImage(img, outputWebp, options);
|
||||
if (res) results.push(res);
|
||||
}
|
||||
|
||||
// Print Report
|
||||
console.log('\nOptimization Report:');
|
||||
console.table(results.map(r => ({
|
||||
File: r.name,
|
||||
Source: r.inputPath.startsWith(SOURCE_DIR) ? 'Source Folder' : 'Article Folder',
|
||||
Original: formatBytes(r.originalSize),
|
||||
Optimized: formatBytes(r.optimizedSize),
|
||||
Reduction: r.reduction
|
||||
})));
|
||||
|
||||
// 5. Update Markdown references
|
||||
console.log('\nUpdating Markdown references...');
|
||||
const mdFiles = await glob('src/**/*.md');
|
||||
for (const res of results) {
|
||||
// Only update for article images (not the core theme images which are already updated)
|
||||
if (!res.inputPath.includes(SOURCE_DIR)) {
|
||||
const oldName = res.name;
|
||||
const newName = res.outputName;
|
||||
for (const mdFile of mdFiles) {
|
||||
let content = await fs.readFile(mdFile, 'utf-8');
|
||||
if (content.includes(oldName)) {
|
||||
const newContent = content.replaceAll(oldName, newName);
|
||||
if (newContent !== content) {
|
||||
await fs.writeFile(mdFile, newContent);
|
||||
console.log(`Updated ${oldName} -> ${newName} in ${mdFile}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalOriginal = results.reduce((sum, r) => sum + r.originalSize, 0);
|
||||
const totalOptimized = results.reduce((sum, r) => sum + r.optimizedSize, 0);
|
||||
console.log(`\nTotal Reduction: ${formatBytes(totalOriginal - totalOptimized)} (${((totalOriginal - totalOptimized) / totalOriginal * 100).toFixed(2)}%)`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user