Skip to content

Commit dfd7e6b

Browse files
feat: add automated image optimization pipeline with Sharp
1 parent 152ffa3 commit dfd7e6b

2 files changed

Lines changed: 159 additions & 2 deletions

File tree

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@
1111
"react-dom": "^19.2.3",
1212
"react-icons": "^5.5.0"
1313
},
14+
"devDependencies": {
15+
"sharp": "^0.34.5"
16+
},
1417
"scripts": {
1518
"dev": "next dev",
19+
"prebuild": "npm run optimize-images",
1620
"build": "next build",
1721
"start": "next start",
18-
"lint": "next lint"
22+
"lint": "next lint",
23+
"optimize-images": "node scripts/optimize-images.js"
1924
}
20-
}
25+
}

scripts/optimize-images.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Image Optimization Script
3+
*
4+
* Uses Sharp to compress PNG images and generate WebP versions.
5+
* Run with: npm run optimize-images
6+
*/
7+
8+
const sharp = require('sharp');
9+
const fs = require('fs');
10+
const path = require('path');
11+
12+
const ASSETS_DIR = path.join(__dirname, '..', 'app', 'assets');
13+
const PNG_QUALITY = 80;
14+
const WEBP_QUALITY = 80;
15+
16+
// Images to optimize (large PNG files)
17+
const IMAGES_TO_OPTIMIZE = [
18+
'resonate_app.png',
19+
'Vector.png',
20+
'Group.png',
21+
'createrooms.png',
22+
'roomscreen.png',
23+
'pairchat.png',
24+
'chatscreen.png',
25+
'aossie_logo.png',
26+
'PlayStore.png',
27+
];
28+
29+
async function getFileSize(filePath) {
30+
try {
31+
const stats = await fs.promises.stat(filePath);
32+
return stats.size;
33+
} catch (error) {
34+
// File doesn't exist or is inaccessible
35+
return 0;
36+
}
37+
}
38+
39+
async function fileExists(filePath) {
40+
try {
41+
await fs.promises.access(filePath, fs.constants.F_OK);
42+
return true;
43+
} catch {
44+
return false;
45+
}
46+
}
47+
48+
function formatBytes(bytes) {
49+
if (bytes === 0) return '0 Bytes';
50+
const k = 1024;
51+
const sizes = ['Bytes', 'KB', 'MB'];
52+
const i = Math.floor(Math.log(bytes) / Math.log(k));
53+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
54+
}
55+
56+
async function optimizeImage(filename) {
57+
const inputPath = path.join(ASSETS_DIR, filename);
58+
const tempPath = path.join(ASSETS_DIR, `temp_${filename}`);
59+
const webpPath = path.join(ASSETS_DIR, filename.replace('.png', '.webp'));
60+
61+
// Check if file exists
62+
if (!await fileExists(inputPath)) {
63+
console.log(`Skipping ${filename} - file not found`);
64+
return null;
65+
}
66+
67+
const originalSize = await getFileSize(inputPath);
68+
69+
try {
70+
// Compress PNG
71+
await sharp(inputPath)
72+
.png({
73+
quality: PNG_QUALITY,
74+
compressionLevel: 9
75+
})
76+
.toFile(tempPath);
77+
78+
// Atomically replace original with compressed version
79+
// rename() overwrites the destination if it exists (atomic on same filesystem)
80+
await fs.promises.rename(tempPath, inputPath);
81+
82+
const compressedSize = await getFileSize(inputPath);
83+
84+
// Generate WebP version
85+
await sharp(inputPath)
86+
.webp({ quality: WEBP_QUALITY })
87+
.toFile(webpPath);
88+
89+
const webpSize = await getFileSize(webpPath);
90+
91+
return {
92+
filename,
93+
originalSize,
94+
compressedSize,
95+
webpSize,
96+
savings: originalSize - compressedSize,
97+
savingsPercent: originalSize > 0
98+
? ((originalSize - compressedSize) / originalSize * 100).toFixed(1)
99+
: '0.0'
100+
};
101+
} catch (error) {
102+
console.error(`Error optimizing ${filename}:`, error.message);
103+
// Clean up temp file if it exists
104+
if (await fileExists(tempPath)) {
105+
await fs.promises.unlink(tempPath);
106+
}
107+
return null;
108+
}
109+
}
110+
111+
async function main() {
112+
console.log('\nImage Optimization Script\n');
113+
console.log('='.repeat(60));
114+
115+
const results = [];
116+
let totalOriginal = 0;
117+
let totalCompressed = 0;
118+
119+
for (const filename of IMAGES_TO_OPTIMIZE) {
120+
process.stdout.write(`Processing ${filename}... `);
121+
const result = await optimizeImage(filename);
122+
123+
if (result) {
124+
results.push(result);
125+
totalOriginal += result.originalSize;
126+
totalCompressed += result.compressedSize;
127+
console.log(`Done - Saved ${result.savingsPercent}%`);
128+
}
129+
}
130+
131+
console.log('\n' + '='.repeat(60));
132+
console.log('\nOptimization Results:\n');
133+
console.log('| Image | Original | Compressed | WebP | Savings |');
134+
console.log('|-------|----------|------------|------|---------|');
135+
136+
for (const r of results) {
137+
console.log(`| ${r.filename.substring(0, 20).padEnd(20)} | ${formatBytes(r.originalSize).padEnd(8)} | ${formatBytes(r.compressedSize).padEnd(10)} | ${formatBytes(r.webpSize).padEnd(6)} | ${r.savingsPercent}% |`);
138+
}
139+
140+
console.log('\n' + '='.repeat(60));
141+
142+
// Prevent division by zero if no images were processed
143+
if (totalOriginal === 0) {
144+
console.log('\nNo images were processed.\n');
145+
return;
146+
}
147+
148+
console.log(`\nTotal: ${formatBytes(totalOriginal)} -> ${formatBytes(totalCompressed)}`);
149+
console.log(`Saved: ${formatBytes(totalOriginal - totalCompressed)} (${((totalOriginal - totalCompressed) / totalOriginal * 100).toFixed(1)}%)\n`);
150+
}
151+
152+
main().catch(console.error);

0 commit comments

Comments
 (0)