Make voice note loading completely lazy with deferred initialization

Aggressive performance optimization to prevent UI freezes:

Problem: Even with async loading, creating 10+ VoiceNotePlaybackController
instances simultaneously (when scrolling past multiple voice notes) spawned
20+ concurrent background tasks, potentially starving main thread.

Solution - Ultra-lazy loading:
1. VoiceNotePlaybackController.init() now does ZERO work
   - No duration loading
   - No player creation
   - Instant initialization

2. Duration loaded on-demand via public loadDuration() method
   - Called from VoiceNoteView.onAppear after 150ms delay
   - Reduced priority: .utility instead of .userInitiated
   - Guard prevents duplicate loading

3. Waveform loading also deferred 150ms
   - Gives UI time to settle after message appears
   - Prevents task storms when multiple voice notes appear

This spreads the work over time instead of all at once.
This commit is contained in:
jack
2025-10-15 17:37:19 +01:00
committed by islam
parent 46caded099
commit 3df0aa0cc4
2 changed files with 15 additions and 9 deletions
@@ -16,12 +16,14 @@ final class VoiceNotePlaybackController: NSObject, ObservableObject, AVAudioPlay
init(url: URL) { init(url: URL) {
self.url = url self.url = url
super.init() super.init()
// Load duration asynchronously to avoid blocking main thread // Don't load anything eagerly - wait until user interaction or view is fully displayed
loadDurationAsync()
} }
private func loadDurationAsync() { func loadDuration() {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in // Only load if not already loaded
guard duration == 0 else { return }
DispatchQueue.global(qos: .utility).async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
do { do {
let player = try AVAudioPlayer(contentsOf: self.url) let player = try AVAudioPlayer(contentsOf: self.url)
@@ -44,7 +46,8 @@ final class VoiceNotePlaybackController: NSObject, ObservableObject, AVAudioPlay
stop() stop()
self.url = url self.url = url
player = nil player = nil
loadDurationAsync() duration = 0
// Duration will be loaded on demand when needed
} }
func togglePlayback() { func togglePlayback() {
+7 -4
View File
@@ -100,10 +100,13 @@ struct VoiceNoteView: View {
.stroke(borderColor, lineWidth: 1) .stroke(borderColor, lineWidth: 1)
) )
.onAppear { .onAppear {
WaveformCache.shared.waveform(for: url, completion: { bins in // Defer both duration and waveform loading to let UI settle after message appears
self.waveform = bins DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
}) playback.loadDuration()
// No need to call playback.replaceURL - already initialized with correct URL WaveformCache.shared.waveform(for: url, completion: { bins in
self.waveform = bins
})
}
} }
.onChange(of: url) { newValue in .onChange(of: url) { newValue in
WaveformCache.shared.waveform(for: newValue, completion: { bins in WaveformCache.shared.waveform(for: newValue, completion: { bins in