perf(assets): compress oversized images to reduce load and bundle size
- Shrink heavy backgrounds and icon source assets - Reduce login background from ~8.1MB to ~0.43MB - Add reusable tool script for repeatable asset optimization
|
Before Width: | Height: | Size: 561 KiB After Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 561 KiB After Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 8.7 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 8.7 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 561 KiB After Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 7.9 MiB After Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 7.9 MiB After Width: | Height: | Size: 433 KiB |
75
tool/optimize_assets.dart
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import 'dart:io';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
class _AssetRule {
|
||||
final String path;
|
||||
final int maxDimension;
|
||||
final int jpgQuality;
|
||||
|
||||
const _AssetRule(this.path, {required this.maxDimension, this.jpgQuality = 86});
|
||||
}
|
||||
|
||||
const _rules = <_AssetRule>[
|
||||
_AssetRule('assets/img/login_bg.jpg', maxDimension: 1920, jpgQuality: 84),
|
||||
_AssetRule('assets/img/loadingScreenMainScreen.png', maxDimension: 1920),
|
||||
_AssetRule('assets/img/icon.png', maxDimension: 1024),
|
||||
_AssetRule('assets/img/icon_bg_removed.png', maxDimension: 1024),
|
||||
_AssetRule('assets/icon/app_icon.png', maxDimension: 1024),
|
||||
_AssetRule('assets/icon/app_icon_ios.png', maxDimension: 1024),
|
||||
_AssetRule('assets/icon/app_icon_foreground.png', maxDimension: 1024),
|
||||
];
|
||||
|
||||
void main() {
|
||||
for (final rule in _rules) {
|
||||
final file = File(rule.path);
|
||||
if (!file.existsSync()) {
|
||||
stdout.writeln('SKIP ${rule.path} (missing)');
|
||||
continue;
|
||||
}
|
||||
|
||||
final beforeBytes = file.lengthSync();
|
||||
final decoded = img.decodeImage(file.readAsBytesSync());
|
||||
if (decoded == null) {
|
||||
stdout.writeln('SKIP ${rule.path} (decode failed)');
|
||||
continue;
|
||||
}
|
||||
|
||||
img.Image working = decoded;
|
||||
final maxSide = working.width > working.height ? working.width : working.height;
|
||||
|
||||
if (maxSide > rule.maxDimension) {
|
||||
final scale = rule.maxDimension / maxSide;
|
||||
final newWidth = (working.width * scale).round();
|
||||
final newHeight = (working.height * scale).round();
|
||||
working = img.copyResize(
|
||||
working,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
interpolation: img.Interpolation.average,
|
||||
);
|
||||
}
|
||||
|
||||
final lower = rule.path.toLowerCase();
|
||||
late final List<int> encoded;
|
||||
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) {
|
||||
encoded = img.encodeJpg(working, quality: rule.jpgQuality);
|
||||
} else if (lower.endsWith('.png')) {
|
||||
encoded = img.encodePng(working, level: 6);
|
||||
} else {
|
||||
stdout.writeln('SKIP ${rule.path} (unsupported type)');
|
||||
continue;
|
||||
}
|
||||
|
||||
file.writeAsBytesSync(encoded, flush: true);
|
||||
final afterBytes = file.lengthSync();
|
||||
final saved = beforeBytes - afterBytes;
|
||||
final pct = beforeBytes == 0 ? 0 : (saved / beforeBytes * 100);
|
||||
|
||||
stdout.writeln(
|
||||
'DONE ${rule.path} '
|
||||
'${(beforeBytes / 1024).toStringAsFixed(1)}KB -> '
|
||||
'${(afterBytes / 1024).toStringAsFixed(1)}KB '
|
||||
'(${pct >= 0 ? '-' : '+'}${pct.abs().toStringAsFixed(1)}%)',
|
||||
);
|
||||
}
|
||||
}
|
||||