fix: performance
This commit is contained in:
@@ -1,150 +0,0 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const distAssetRoots = [
|
||||
path.join(process.cwd(), 'dist', 'assets'),
|
||||
path.join(process.cwd(), 'dist', 'browser', 'assets'),
|
||||
];
|
||||
|
||||
const losslessTransformers = {
|
||||
'.jpg': (pipeline) =>
|
||||
pipeline.jpeg({
|
||||
quality: 90,
|
||||
mozjpeg: true,
|
||||
chromaSubsampling: '4:4:4',
|
||||
optimizeScans: true,
|
||||
}),
|
||||
'.jpeg': (pipeline) =>
|
||||
pipeline.jpeg({
|
||||
quality: 90,
|
||||
mozjpeg: true,
|
||||
chromaSubsampling: '4:4:4',
|
||||
optimizeScans: true,
|
||||
}),
|
||||
'.png': (pipeline) => pipeline.png({ compressionLevel: 9, effort: 10 }),
|
||||
'.gif': (pipeline) => pipeline.gif({ effort: 10, colours: 256 }),
|
||||
'.webp': (pipeline) => pipeline.webp({ lossless: true, effort: 6 }),
|
||||
'.avif': (pipeline) => pipeline.avif({ lossless: true, effort: 9 }),
|
||||
'.tif': (pipeline) => pipeline.tiff({ compression: 'lzw' }),
|
||||
'.tiff': (pipeline) => pipeline.tiff({ compression: 'lzw' }),
|
||||
};
|
||||
|
||||
const imageExtensions = new Set([
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.webp',
|
||||
'.avif',
|
||||
'.gif',
|
||||
'.svg',
|
||||
'.tif',
|
||||
'.tiff',
|
||||
'.bmp',
|
||||
]);
|
||||
|
||||
const skippedNoLosslessEncoder = new Set(['.svg', '.bmp']);
|
||||
|
||||
function getAllFilesRecursively(directory) {
|
||||
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
||||
const files = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...getAllFilesRecursively(fullPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function compressLossless(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const transform = losslessTransformers[ext];
|
||||
|
||||
if (!transform) {
|
||||
return { status: 'unsupported', beforeBytes: 0, afterBytes: 0 };
|
||||
}
|
||||
|
||||
const beforeBuffer = fs.readFileSync(filePath);
|
||||
const beforeBytes = beforeBuffer.byteLength;
|
||||
const optimizedBuffer = await transform(sharp(beforeBuffer)).toBuffer();
|
||||
const afterBytes = optimizedBuffer.byteLength;
|
||||
|
||||
if (afterBytes < beforeBytes) {
|
||||
fs.writeFileSync(filePath, optimizedBuffer);
|
||||
return { status: 'optimized', beforeBytes, afterBytes };
|
||||
}
|
||||
|
||||
return { status: 'unchanged', beforeBytes, afterBytes: beforeBytes };
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const existingRoots = distAssetRoots
|
||||
.filter((rootPath) => fs.existsSync(rootPath))
|
||||
.filter((rootPath, index, list) => list.indexOf(rootPath) === index);
|
||||
|
||||
if (existingRoots.length === 0) {
|
||||
console.log('Asset compression skipped: no dist asset directory found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const imageFiles = existingRoots
|
||||
.flatMap((rootPath) => getAllFilesRecursively(rootPath))
|
||||
.filter((filePath) => imageExtensions.has(path.extname(filePath).toLowerCase()));
|
||||
|
||||
let optimizedCount = 0;
|
||||
let unchangedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let totalSavedBytes = 0;
|
||||
|
||||
for (const filePath of imageFiles) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (skippedNoLosslessEncoder.has(ext)) {
|
||||
skippedCount += 1;
|
||||
console.log(
|
||||
`Skip (no lossless encoder configured): ${path.relative(process.cwd(), filePath)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await compressLossless(filePath);
|
||||
if (result.status === 'optimized') {
|
||||
optimizedCount += 1;
|
||||
const savedBytes = result.beforeBytes - result.afterBytes;
|
||||
totalSavedBytes += savedBytes;
|
||||
console.log(`Optimized: ${path.relative(process.cwd(), filePath)} (-${savedBytes} bytes)`);
|
||||
} else if (result.status === 'unchanged') {
|
||||
unchangedCount += 1;
|
||||
console.log(`Unchanged: ${path.relative(process.cwd(), filePath)}`);
|
||||
} else {
|
||||
skippedCount += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
skippedCount += 1;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Skip (error): ${path.relative(process.cwd(), filePath)} (${message})`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Asset compression complete. Optimized: ${optimizedCount}, unchanged: ${unchangedCount}, skipped: ${skippedCount}, saved: ${totalSavedBytes} bytes.`,
|
||||
);
|
||||
}
|
||||
|
||||
run()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||
console.error(`Asset compression failed: ${message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const sourceRoot = path.join(process.cwd(), 'src', 'assets', 'unoptimized');
|
||||
const targetRoot = path.join(process.cwd(), 'src', 'assets', 'optimized');
|
||||
|
||||
const rasterExtensions = new Set(['.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif', '.tif', '.tiff']);
|
||||
const passthroughExtensions = new Set(['.svg']);
|
||||
|
||||
function collectFilesRecursively(directoryPath) {
|
||||
if (!fs.existsSync(directoryPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
|
||||
const files = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(directoryPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectFilesRecursively(fullPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function ensureCleanDirectory(directoryPath) {
|
||||
fs.rmSync(directoryPath, { recursive: true, force: true });
|
||||
fs.mkdirSync(directoryPath, { recursive: true });
|
||||
}
|
||||
|
||||
function getProfile(relativePath) {
|
||||
const normalizedRelativePath = relativePath.split(path.sep).join('/').toLowerCase();
|
||||
|
||||
if (normalizedRelativePath === 'background.jpg' || normalizedRelativePath === 'background.jpeg') {
|
||||
return { maxWidth: 1920, quality: 82, alphaQuality: 100 };
|
||||
}
|
||||
|
||||
if (normalizedRelativePath.startsWith('logos/')) {
|
||||
return { maxWidth: 640, quality: 90, alphaQuality: 92, nearLossless: true };
|
||||
}
|
||||
|
||||
if (normalizedRelativePath.startsWith('portrait/')) {
|
||||
return { maxWidth: 1600, quality: 84, alphaQuality: 92 };
|
||||
}
|
||||
|
||||
return { maxWidth: 1600, quality: 82, alphaQuality: 92 };
|
||||
}
|
||||
|
||||
async function writeOptimizedAsset(sourceFilePath) {
|
||||
const relativePath = path.relative(sourceRoot, sourceFilePath);
|
||||
const extension = path.extname(sourceFilePath).toLowerCase();
|
||||
const targetRelativePath = passthroughExtensions.has(extension)
|
||||
? relativePath
|
||||
: relativePath.replace(/\.[^.]+$/, '.webp');
|
||||
const targetFilePath = path.join(targetRoot, targetRelativePath);
|
||||
|
||||
fs.mkdirSync(path.dirname(targetFilePath), { recursive: true });
|
||||
|
||||
if (passthroughExtensions.has(extension)) {
|
||||
fs.copyFileSync(sourceFilePath, targetFilePath);
|
||||
return {
|
||||
sourceRelativePath: relativePath,
|
||||
targetRelativePath,
|
||||
beforeBytes: fs.statSync(sourceFilePath).size,
|
||||
afterBytes: fs.statSync(targetFilePath).size,
|
||||
mode: 'copied',
|
||||
};
|
||||
}
|
||||
|
||||
const profile = getProfile(relativePath);
|
||||
const inputBuffer = fs.readFileSync(sourceFilePath);
|
||||
const inputBytes = inputBuffer.byteLength;
|
||||
const image = sharp(inputBuffer, { animated: false }).rotate();
|
||||
const metadata = await image.metadata();
|
||||
|
||||
let pipeline = image;
|
||||
if ((metadata.width ?? 0) > profile.maxWidth) {
|
||||
pipeline = pipeline.resize({
|
||||
width: profile.maxWidth,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
});
|
||||
}
|
||||
|
||||
const outputBuffer = await pipeline
|
||||
.webp({
|
||||
quality: profile.quality,
|
||||
alphaQuality: profile.alphaQuality,
|
||||
effort: 6,
|
||||
smartSubsample: true,
|
||||
nearLossless: profile.nearLossless ?? false,
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
fs.writeFileSync(targetFilePath, outputBuffer);
|
||||
|
||||
return {
|
||||
sourceRelativePath: relativePath,
|
||||
targetRelativePath,
|
||||
beforeBytes: inputBytes,
|
||||
afterBytes: outputBuffer.byteLength,
|
||||
mode: 'converted',
|
||||
};
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (!fs.existsSync(sourceRoot)) {
|
||||
console.log('Optimized asset generation skipped: no unoptimized asset directory found.');
|
||||
return;
|
||||
}
|
||||
|
||||
ensureCleanDirectory(targetRoot);
|
||||
|
||||
const assetFiles = collectFilesRecursively(sourceRoot).filter((filePath) => {
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
return rasterExtensions.has(extension) || passthroughExtensions.has(extension);
|
||||
});
|
||||
|
||||
let convertedCount = 0;
|
||||
let copiedCount = 0;
|
||||
let totalSavedBytes = 0;
|
||||
|
||||
for (const assetFile of assetFiles) {
|
||||
const result = await writeOptimizedAsset(assetFile);
|
||||
const savedBytes = result.beforeBytes - result.afterBytes;
|
||||
totalSavedBytes += savedBytes;
|
||||
|
||||
if (result.mode === 'copied') {
|
||||
copiedCount += 1;
|
||||
console.log(
|
||||
`Copied: ${result.sourceRelativePath} -> ${result.targetRelativePath} (${result.afterBytes} bytes)`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
convertedCount += 1;
|
||||
console.log(
|
||||
`Optimized: ${result.sourceRelativePath} -> ${result.targetRelativePath} (${savedBytes >= 0 ? `-${savedBytes}` : `+${Math.abs(savedBytes)}`} bytes)`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Optimized asset generation complete. Converted: ${convertedCount}, copied: ${copiedCount}, net saved: ${totalSavedBytes} bytes.`,
|
||||
);
|
||||
}
|
||||
|
||||
run()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||
console.error(`Optimized asset generation failed: ${message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user