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
This commit is contained in:
Lukas Müllner 2026-03-05 08:15:14 +01:00
parent 4f0c98dc44
commit f3d1454cdc
8 changed files with 75 additions and 0 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 561 KiB

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 561 KiB

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 561 KiB

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 MiB

After

Width:  |  Height:  |  Size: 433 KiB

75
tool/optimize_assets.dart Normal file
View 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)}%)',
);
}
}