5.1 KiB
PLAN: Service Image Optimization
Problem
Full-size S3 images (up to 3-4MB each) are downloaded for 120px thumbnails in the Discover tab. With 10 cards visible, that's 30-40MB of unnecessary data per page load, causing slow scroll performance and excessive bandwidth usage.
Current Flow
Provider uploads photo → Spatie stores FULL SIZE in S3 → API returns raw S3 URL → App downloads FULL image for 120px card
Image Stats (DB)
- 6,101 active services with image attachments
- Images stored in
main-bucket-ays.s3.ap-southeast-1.amazonaws.com - No thumbnails or resized variants generated
Options Analysis
| Option | Effort | Impact | Cost |
|---|---|---|---|
| A. Backend thumbnail on upload | Medium | High | Free (server CPU) |
| B. On-the-fly resize via API | Low | High | Free (server CPU per request) |
| C. Flutter-side only (memCache) | Very Low | Medium | None |
| D. CloudFront + Lambda@Edge | High | Highest | AWS costs |
Recommended: Option A + C (Hybrid)
Why?
- A generates thumbnails once on upload — no repeated CPU cost
- C optimizes Flutter memory immediately — quick win, no backend needed
- Together they solve both bandwidth AND memory issues
Phase 1: Flutter Quick Win (No backend change needed)
[MODIFY] cached_image_widget.dart
- Cap
memCacheWidth/memCacheHeightto reasonable limits based on display size- Card thumbnails (120px height):
memCacheHeight: 360(3x for retina) - Provider avatars (16px):
memCacheHeight: 48
- Card thumbnails (120px height):
- Set
fadeInDurationto 150ms for snappier feel - Add
maxWidthDiskCache/maxHeightDiskCacheto limit disk cache bloat
CachedNetworkImage(
imageUrl: url,
memCacheHeight: (height * 3).toInt().clamp(0, 720),
memCacheWidth: null, // Let aspect ratio be preserved
maxHeightDiskCache: (height * 3).toInt().clamp(0, 720),
fadeInDuration: Duration(milliseconds: 150),
...
)
Impact: Images still download full-size but decode to smaller in-memory, reducing RAM by ~70%. Scroll performance improves immediately.
Phase 2: Backend Thumbnail Generation
[MODIFY] helper.php — storeMediaFile()
Add thumbnail generation using Spatie Media Library conversions:
// In Service model, register conversions:
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('thumb')
->width(400)
->height(300)
->sharpen(10)
->quality(80)
->performOnCollections('service_attachment')
->nonQueued(); // or ->queued() for async
}
[MODIFY] ServiceResource.php — Return thumbnail URLs
Add attchments_thumb field alongside existing attchments:
'attchments_thumb' => $this->getMedia('service_attachment')
->map(fn($m) => $m->getUrl('thumb'))
->filter()
->values()
->all(),
[MODIFY] Flutter service_data_model.dart
Parse the new attchments_thumb field:
List<String>? attachmentsThumbnail;
// In fromJson:
attachmentsThumbnail = json['attchments_thumb'] != null
? List<String>.from(json['attchments_thumb'])
: null;
[MODIFY] discover_service_card.dart
Prefer thumbnail URL for card display:
final imageUrl = data.attachmentsThumbnail?.isNotEmpty == true
? data.attachmentsThumbnail!.first
: (data.attachments?.isNotEmpty == true ? data.attachments!.first : '');
Phase 3: Backfill Existing Images (One-time)
[NEW] Artisan command to regenerate thumbnails
// php artisan media-library:regenerate --only-missing
Spatie's built-in command processes all existing media and creates missing conversions. For 6,000+ images this may take 30-60 minutes.
Verification Plan
Phase 1 (Flutter-only)
- Profile RAM usage before/after with DevTools
- Compare scroll FPS in Discover tab
- Confirm images still display correctly
Phase 2 (Backend)
- Upload a new service → verify thumbnail created in S3
- API response includes
attchments_thumbwith valid URL - Flutter card loads thumbnail (~30-50KB) instead of original (~2-4MB)
- Compare network tab: bandwidth reduction should be ~90%
Phase 3 (Backfill)
- Run regeneration command on staging first
- Verify all existing services have thumbnails
- Deploy to production
Estimated Savings
| Metric | Before | After (Phase 2) |
|---|---|---|
| Image per card | ~2-4 MB | ~30-50 KB |
| 10 cards loaded | ~30 MB | ~400 KB |
| Bandwidth reduction | — | ~95% |
| Memory per card | ~12 MB decoded | ~1 MB decoded |
Risk & Notes
Important
Phase 1 is a safe, backend-free change — can deploy immediately. Phase 2 requires backend deployment + S3 write permissions for thumbnails.
Warning
Phase 3 (backfill) will generate new S3 objects for all 6,000+ services. Estimate storage increase: ~6,000 × 50KB = ~300 MB additional S3 storage.
Note
The Spatie Media Library conversion is the industry-standard approach for Laravel. No additional packages needed — it's already a dependency.