From fb26db3bf04680519ce9934967c3f1ff27730e84 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 30 Sep 2025 14:50:03 +0200 Subject: [PATCH] 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. --- .../voice/VoiceNotePlaybackController.swift | 13 ++++++++----- bitchat/Views/Media/VoiceNoteView.swift | 11 +++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/bitchat/Features/voice/VoiceNotePlaybackController.swift b/bitchat/Features/voice/VoiceNotePlaybackController.swift index de475e51..1ce463f7 100644 --- a/bitchat/Features/voice/VoiceNotePlaybackController.swift +++ b/bitchat/Features/voice/VoiceNotePlaybackController.swift @@ -16,12 +16,14 @@ final class VoiceNotePlaybackController: NSObject, ObservableObject, AVAudioPlay init(url: URL) { self.url = url super.init() - // Load duration asynchronously to avoid blocking main thread - loadDurationAsync() + // Don't load anything eagerly - wait until user interaction or view is fully displayed } - private func loadDurationAsync() { - DispatchQueue.global(qos: .userInitiated).async { [weak self] in + func loadDuration() { + // 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 } do { let player = try AVAudioPlayer(contentsOf: self.url) @@ -44,7 +46,8 @@ final class VoiceNotePlaybackController: NSObject, ObservableObject, AVAudioPlay stop() self.url = url player = nil - loadDurationAsync() + duration = 0 + // Duration will be loaded on demand when needed } func togglePlayback() { diff --git a/bitchat/Views/Media/VoiceNoteView.swift b/bitchat/Views/Media/VoiceNoteView.swift index cba1ba5b..4e0b2e5b 100644 --- a/bitchat/Views/Media/VoiceNoteView.swift +++ b/bitchat/Views/Media/VoiceNoteView.swift @@ -100,10 +100,13 @@ struct VoiceNoteView: View { .stroke(borderColor, lineWidth: 1) ) .onAppear { - WaveformCache.shared.waveform(for: url, completion: { bins in - self.waveform = bins - }) - // No need to call playback.replaceURL - already initialized with correct URL + // Defer both duration and waveform loading to let UI settle after message appears + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + playback.loadDuration() + WaveformCache.shared.waveform(for: url, completion: { bins in + self.waveform = bins + }) + } } .onChange(of: url) { newValue in WaveformCache.shared.waveform(for: newValue, completion: { bins in