# SwiftUI Performans: LazyVStack ve Karmaşık Listeleri Optimize Etme > LazyVStack ve SwiftUI listeleri için optimizasyon teknikleri. Bellek tüketimini azaltın, kaydırma performansını iyileştirin ve sık karşılaşılan tuzakları önleyin. - Published: 2026-03-06 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftui, ios, performance, lazyvstack, swift - Reading time: 14 min --- Listeler, iOS uygulamalarında en sık kullanılan bileşenlerden birini temsil eder. SwiftUI'nin `LazyVStack` ve `List` bileşenleri veri koleksiyonlarını göstermek için performanslı çözümler sunar, ancak yanlış kullanım kullanıcı deneyimini hızla bozabilir. Bu bileşenlerin iç mekanizmalarını anlamak, sık karşılaşılan tuzakları önlemeye ve akıcı arayüzler oluşturmaya yardımcı olur. > **Bu makalenin kapsamı** > > Bu makale SwiftUI listeleri için temel optimizasyon tekniklerini sunar: lazy loading, view geri dönüşümü, tanımlayıcı yönetimi ve büyük veri kümeleri için gelişmiş desenler. ## SwiftUI'de Lazy Loading'i Anlamak Lazy loading, view'ları yalnızca ekranda görünür hale geldiklerinde örneklendirir. Tüm alt view'ları hemen oluşturan `VStack`'in aksine, `LazyVStack` bu oluşturma işlemini erteler ve bellek tüketimini ile başlangıç render süresini önemli ölçüde azaltır. ```swift // LazyVStackComparison.swift import SwiftUI // ❌ Problem: VStack instantiates all 10,000 views immediately struct NonLazyListView: View { let items = (1...10000).map { "Item \($0)" } var body: some View { ScrollView { VStack { ForEach(items, id: \.self) { item in // Each view is created at launch ExpensiveRowView(title: item) } } } } } // ✅ Solution: LazyVStack creates views on demand struct LazyListView: View { let items = (1...10000).map { "Item \($0)" } var body: some View { ScrollView { LazyVStack { ForEach(items, id: \.self) { item in // Only visible views are created ExpensiveRowView(title: item) } } } } } ``` Performans farkı yalnızca birkaç yüz öğeyle bile belirgin hale gelir. 10.000 öğeyle `VStack` başlatılması birkaç saniye sürebilirken `LazyVStack` anında kalır. ### Lazy Loading Etkisini Ölçmek Instruments, bellek ve CPU kullanımını hassas bir şekilde ölçmeye olanak tanır. İşte farkı gösteren bir test view'i: ```swift // PerformanceMeasurement.swift struct ExpensiveRowView: View { let title: String // Simulating expensive initialization init(title: String) { self.title = title // Log to visualize when the view is created print("Creating row: \(title)") } var body: some View { HStack { // Image with processing Circle() .fill( LinearGradient( colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 50, height: 50) VStack(alignment: .leading) { Text(title) .font(.headline) Text("Subtitle with computation") .font(.caption) .foregroundStyle(.secondary) } Spacer() } .padding() } } ``` `VStack` ile çalıştırıldığında, 10.000 logun tamamı hemen görünür. `LazyVStack` ile, yalnızca görünür öğeler (ekran boyutuna bağlı olarak yaklaşık 15-20) başlangıçta loglanır ve kaydırma sırasında daha fazlası ortaya çıkar. > **Tutma davranışı** > > `LazyVStack`, oluşturulan view'ları göründükten sonra bellekte tutar. Hücreleri aktif olarak geri dönüştüren `List`'in aksine, `LazyVStack` içindeki view'lar üst bileşen yok edilene kadar kalır. ## Kararlı Tanımlayıcıların Önemi Tanımlayıcılar SwiftUI liste güncellemelerinin merkezi mekanizmasını oluşturur. Kararsız bir tanımlayıcı gereksiz view yeniden oluşturmalarına neden olur ve yanlış animasyonlar veya kaydırma konumu kaybı gibi görsel hatalara yol açabilir. ```swift // StableIdentifiers.swift // ❌ Problem: using index as identifier struct UnstableIdentifierView: View { @State private var items = ["A", "B", "C", "D"] var body: some View { List { // Index changes if an element is deleted ForEach(items.indices, id: \.self) { index in Text(items[index]) } } } } // ❌ Problem: using UUID() in ForEach struct RegeneratedIdentifierView: View { let items = ["A", "B", "C", "D"] var body: some View { List { // UUID() generates a new ID on each render ForEach(items, id: \.self) { item in // Subtle issue if items contain duplicates Text(item) } } } } // ✅ Solution: model with stable identifier struct Item: Identifiable { let id: UUID // Created once var name: String init(name: String) { self.id = UUID() self.name = name } } struct StableIdentifierView: View { @State private var items = [ Item(name: "A"), Item(name: "B"), Item(name: "C"), Item(name: "D") ] var body: some View { List { // id is stable for the item's lifetime ForEach(items) { item in Text(item.name) } } } } ``` Benzersiz ve kalıcı bir tanımlayıcı kullanmak, SwiftUI'nin güncellemeler, animasyonlar ve karşılaştırmalar sırasında öğeleri doğru şekilde ayırt edebilmesini garantiler. ## Equatable ile Hücre Optimizasyonu SwiftUI yeniden render gerekip gerekmediğini belirlemek için view'ları karşılaştırır. Varsayılan olarak bu karşılaştırma reflection kullanır ki bu da maliyetli olabilir. `Equatable` uygulamak optimize edilmiş ve açık karşılaştırma sağlar. ```swift // EquatableOptimization.swift // Data model struct Contact: Identifiable, Equatable { let id: UUID var name: String var email: String var avatarURL: URL? var lastActivity: Date // Custom comparison: ignore lastActivity // if other properties are identical static func == (lhs: Contact, rhs: Contact) -> Bool { lhs.id == rhs.id && lhs.name == rhs.name && lhs.email == rhs.email && lhs.avatarURL == rhs.avatarURL // lastActivity intentionally excluded } } // Optimized cell view struct ContactRow: View, Equatable { let contact: Contact // Explicit comparison to avoid unnecessary re-renders static func == (lhs: ContactRow, rhs: ContactRow) -> Bool { lhs.contact == rhs.contact } var body: some View { HStack(spacing: 12) { // Async avatar AsyncImage(url: contact.avatarURL) { phase in switch phase { case .success(let image): image .resizable() .aspectRatio(contentMode: .fill) case .failure: Image(systemName: "person.circle.fill") .foregroundStyle(.gray) default: ProgressView() } } .frame(width: 44, height: 44) .clipShape(Circle()) // Contact information VStack(alignment: .leading, spacing: 2) { Text(contact.name) .font(.body.weight(.medium)) Text(contact.email) .font(.caption) .foregroundStyle(.secondary) } Spacer() } .padding(.vertical, 4) } } // List using EquatableView struct ContactListView: View { let contacts: [Contact] var body: some View { List { ForEach(contacts) { contact in // EquatableView prevents re-renders if contact unchanged EquatableView(content: ContactRow(contact: contact)) } } } } ``` Bu optimizasyon, özellikle hesaplama veya görsel içeren hücrelerde hızlı kaydırma sırasında CPU yükünü önemli ölçüde azaltır. ## Asenkron Görsel Yüklemenin Yönetimi Görseller listelerde sıklıkla performans darboğazı haline gelir. Yanlış yönetim kaydırma takılmalarına ve aşırı bellek tüketimine neden olur. ```swift // ImageLoadingOptimization.swift import SwiftUI // Singleton image cache actor ImageCache { static let shared = ImageCache() private var cache = NSCache() private init() { // Memory limit: 50 MB cache.totalCostLimit = 50 * 1024 * 1024 } func image(for url: URL) -> UIImage? { cache.object(forKey: url.absoluteString as NSString) } func setImage(_ image: UIImage, for url: URL) { // Cost estimation: image bytes let cost = Int(image.size.width * image.size.height * 4) cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost) } } // Optimized image view with caching struct CachedAsyncImage: View { let url: URL? let size: CGSize @State private var image: UIImage? @State private var isLoading = false var body: some View { Group { if let image { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fill) } else if isLoading { Rectangle() .fill(Color.gray.opacity(0.2)) .overlay(ProgressView()) } else { Rectangle() .fill(Color.gray.opacity(0.2)) } } .frame(width: size.width, height: size.height) .clipped() .task(id: url) { await loadImage() } } private func loadImage() async { guard let url else { return } // Check cache if let cached = await ImageCache.shared.image(for: url) { self.image = cached return } isLoading = true defer { isLoading = false } // Download and resize do { let (data, _) = try await URLSession.shared.data(from: url) // Resize to save memory if let original = UIImage(data: data), let resized = await resizeImage(original, to: size) { await ImageCache.shared.setImage(resized, for: url) self.image = resized } } catch { // Handle error silently } } private func resizeImage(_ image: UIImage, to size: CGSize) async -> UIImage? { // Use screen scale let scale = await UIScreen.main.scale let targetSize = CGSize( width: size.width * scale, height: size.height * scale ) return await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { let renderer = UIGraphicsImageRenderer(size: targetSize) let resized = renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: targetSize)) } continuation.resume(returning: resized) } } } } ``` Bu uygulama, akıcı bir kaydırma deneyimi için bellek önbelleğini, önleyici yeniden boyutlandırmayı ve asenkron yüklemeyi birleştirir. ### Öngörü için Akıllı Prefetching Çok uzun listeler için prefetching görselleri görünür hale gelmeden önce yükler: ```swift // ImagePrefetching.swift // Prefetching coordinator @Observable final class ImagePrefetcher { private var prefetchTasks: [URL: Task] = [:] private let prefetchDistance = 10 // Number of items ahead func prefetchImages(for items: [Contact], visibleRange: Range) { // Calculate prefetch range let prefetchStart = max(0, visibleRange.lowerBound - prefetchDistance) let prefetchEnd = min(items.count, visibleRange.upperBound + prefetchDistance) // Launch prefetch for items in range for index in prefetchStart.., items: [Contact]) { let validURLs = Set( items[validRange].compactMap { $0.avatarURL } ) for (url, task) in prefetchTasks { if !validURLs.contains(url) { task.cancel() prefetchTasks.removeValue(forKey: url) } } } } ``` ## List ve LazyVStack Arasında Seçim `List` ile `LazyVStack` arasındaki seçim kullanım durumuna bağlıdır. Her bileşenin bilinmesi gereken kendine özgü avantajları vardır. ```swift // ListVsLazyVStack.swift // ✅ List: ideal for interactive content // - Automatic cell recycling // - Native swipe actions support // - Built-in separators and styles struct ContactsWithSwipeActions: View { @State private var contacts: [Contact] = [] var body: some View { List { ForEach(contacts) { contact in ContactRow(contact: contact) .swipeActions(edge: .trailing) { Button(role: .destructive) { deleteContact(contact) } label: { Label("Delete", systemImage: "trash") } } .swipeActions(edge: .leading) { Button { favoriteContact(contact) } label: { Label("Favorite", systemImage: "star") } .tint(.yellow) } } } .listStyle(.plain) } private func deleteContact(_ contact: Contact) { contacts.removeAll { $0.id == contact.id } } private func favoriteContact(_ contact: Contact) { // Favorite logic } } // ✅ LazyVStack: ideal for custom layouts // - Full control over spacing and padding // - No imposed styles // - Better performance for simple display struct CustomFeedView: View { let posts: [Post] var body: some View { ScrollView { LazyVStack(spacing: 16) { ForEach(posts) { post in PostCard(post: post) } } .padding(.horizontal) } } } // Post model for example struct Post: Identifiable { let id: UUID let author: String let content: String let imageURL: URL? } struct PostCard: View { let post: Post var body: some View { VStack(alignment: .leading, spacing: 12) { // Header HStack { Circle() .fill(Color.blue) .frame(width: 40, height: 40) Text(post.author) .font(.headline) Spacer() } // Content Text(post.content) // Optional image if let imageURL = post.imageURL { CachedAsyncImage(url: imageURL, size: CGSize(width: 300, height: 200)) .cornerRadius(12) } } .padding() .background(Color(.systemBackground)) .cornerRadius(16) .shadow(color: .black.opacity(0.1), radius: 4, y: 2) } } ``` > **Hücre geri dönüşümü** > > `List` hücreleri aktif olarak geri dönüştürür ve bu, yerel state (`@State`) ile sorunlara neden olabilir. `List` hücrelerindeki `@State` değerleri beklenmedik şekilde yeniden kullanılabilir. State'i veri modelinde veya bir ViewModel'de saklamak tercih edilmelidir. ## Optimize Edilmiş Bölümler ve Başlıklar İçeriği bölümler halinde organize etmek okunabilirliği artırır ancak kötü uygulanırsa performansı etkileyebilir. Sabitlenmiş başlıklar ve bölüm yönetimi özel dikkat gerektirir. ```swift // OptimizedSections.swift // Grouped data model struct GroupedContacts { let letter: String let contacts: [Contact] } // View with optimized sections struct SectionedContactList: View { let groupedContacts: [GroupedContacts] var body: some View { ScrollView { LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { ForEach(groupedContacts, id: \.letter) { group in Section { // Section content ForEach(group.contacts) { contact in ContactRow(contact: contact) .padding(.horizontal) .padding(.vertical, 8) // Custom separator if contact.id != group.contacts.last?.id { Divider() .padding(.leading, 68) } } } header: { // Optimized pinned header SectionHeader(title: group.letter) } } } } } } // Lightweight header for performance struct SectionHeader: View { let title: String var body: some View { Text(title) .font(.headline) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) .padding(.vertical, 8) .background(.ultraThinMaterial) } } // Optimized grouping function extension Array where Element == Contact { func groupedByFirstLetter() -> [GroupedContacts] { // Dictionary for O(n) grouping var groups: [String: [Contact]] = [:] for contact in self { let letter = String(contact.name.prefix(1)).uppercased() groups[letter, default: []].append(contact) } // Sort groups alphabetically return groups .map { GroupedContacts(letter: $0.key, contacts: $0.value) } .sorted { $0.letter < $1.letter } } } ``` Sabitlenmiş başlıklar (`pinnedViews: [.sectionHeaders]`) kaydırma sırasında görünür kalır ve uzun listelerde gezinmeyi iyileştirir. ## Sayfalama ve Sonsuz Kaydırma Büyük veri kümeleri için sayfalama tüm verileri belleğe yüklemekten kaçınır. Uygulama kullanıcı için şeffaf olmalıdır. ```swift // InfiniteScrolling.swift // ViewModel handling pagination @Observable final class PaginatedListViewModel { private(set) var items: [Contact] = [] private(set) var isLoading = false private(set) var hasMorePages = true private var currentPage = 0 private let pageSize = 20 private let dataService: ContactDataService init(dataService: ContactDataService) { self.dataService = dataService } func loadInitialData() async { guard items.isEmpty else { return } await loadNextPage() } func loadMoreIfNeeded(currentItem: Contact) async { // Trigger loading when approaching the end guard let index = items.firstIndex(where: { $0.id == currentItem.id }) else { return } // Load 5 items before the end let thresholdIndex = items.count - 5 if index >= thresholdIndex { await loadNextPage() } } private func loadNextPage() async { guard !isLoading, hasMorePages else { return } isLoading = true defer { isLoading = false } do { let newItems = try await dataService.fetchContacts( page: currentPage, limit: pageSize ) items.append(contentsOf: newItems) currentPage += 1 hasMorePages = newItems.count == pageSize } catch { // Handle error } } } // View with infinite scroll struct InfiniteContactList: View { @State private var viewModel: PaginatedListViewModel init(dataService: ContactDataService) { _viewModel = State(initialValue: PaginatedListViewModel(dataService: dataService)) } var body: some View { List { ForEach(viewModel.items) { contact in ContactRow(contact: contact) .task { // Check if more loading needed await viewModel.loadMoreIfNeeded(currentItem: contact) } } // Loading indicator at end of list if viewModel.isLoading { HStack { Spacer() ProgressView() Spacer() } .padding() } } .task { await viewModel.loadInitialData() } } } // Protocol for data service protocol ContactDataService { func fetchContacts(page: Int, limit: Int) async throws -> [Contact] } ``` Bu desen, arayüzü engellemeden akıcı yükleme sağlar ve verimli bellek yönetimine olanak tanır. ## Instruments ile Profilleme Performans sorunlarını tanımlamak hassas ölçüm araçları gerektirir. Instruments, SwiftUI'ye uygun çeşitli şablonlar sunar. ```swift // ProfilingHelpers.swift // Measurement points for debugging struct PerformanceMonitor { // Measure view creation time static func measureViewCreation( _ name: String, @ViewBuilder content: () -> T ) -> T { let start = CFAbsoluteTimeGetCurrent() let view = content() let elapsed = CFAbsoluteTimeGetCurrent() - start #if DEBUG if elapsed > 0.016 { // More than 16ms = frame drop print("⚠️ [\(name)] View creation took \(elapsed * 1000)ms") } #endif return view } } // Extension to trace renders extension View { func debugRender(_ label: String) -> some View { #if DEBUG let _ = Self._printChanges() print("🔄 Rendering: \(label)") #endif return self } func measureRender(_ label: String) -> some View { modifier(RenderMeasureModifier(label: label)) } } struct RenderMeasureModifier: ViewModifier { let label: String @State private var renderCount = 0 func body(content: Content) -> some View { content .onAppear { renderCount += 1 #if DEBUG print("📊 [\(label)] Render count: \(renderCount)") #endif } } } ``` ### Instruments Optimizasyon Kontrol Listesi Etkili SwiftUI liste profillemesi için: 1. **Time Profiler**: en çok CPU tüketen fonksiyonları tanımlayın 2. **Allocations**: kaydırma sırasında bellek artışını doğrulayın 3. **SwiftUI Instrument**: body değerlendirmelerini görselleştirin 4. **Core Animation**: kare düşüşlerini tespit edin ```swift // InstrumentsExample.swift // Instrumented view for profiling struct ProfiledContactList: View { let contacts: [Contact] var body: some View { let _ = Self._printChanges() // Shows changes triggering re-render List { ForEach(contacts) { contact in ContactRow(contact: contact) .measureRender("ContactRow-\(contact.id)") } } } } ``` > **Self._printChanges()** > > Bu SwiftUI hata ayıklama API'si, hangi özelliklerin değiştiğini ve body yeniden değerlendirmesini tetiklediğini konsola yazdırır. Gereksiz yeniden render'ları tanımlamak için vazgeçilmezdir. ## drawingGroup ile İleri Düzey Optimizasyonlar Birçok görsel efekt içeren karmaşık view'lar için `drawingGroup()`, view'ı bir Metal katmanına rasterize ederek performansı önemli ölçüde artırabilir. ```swift // DrawingGroupOptimization.swift // Cell with complex visual effects struct ComplexVisualRow: View { let item: Item var body: some View { HStack(spacing: 16) { // Circle with gradient and shadow Circle() .fill( RadialGradient( colors: [.blue, .purple, .pink], center: .center, startRadius: 0, endRadius: 25 ) ) .frame(width: 50, height: 50) .shadow(color: .purple.opacity(0.5), radius: 8, y: 4) VStack(alignment: .leading, spacing: 4) { Text(item.name) .font(.headline) // Progress bar with gradient GeometryReader { geometry in Capsule() .fill(Color.gray.opacity(0.2)) .overlay(alignment: .leading) { Capsule() .fill( LinearGradient( colors: [.green, .yellow, .orange], startPoint: .leading, endPoint: .trailing ) ) .frame(width: geometry.size.width * item.progress) } } .frame(height: 8) } } .padding() // Rasterization for performance .drawingGroup() } } // List using optimized cells struct OptimizedComplexList: View { let items: [Item] var body: some View { ScrollView { LazyVStack(spacing: 8) { ForEach(items) { item in ComplexVisualRow(item: item) } } .padding() } } } struct Item: Identifiable { let id: UUID let name: String let progress: Double } ``` `drawingGroup()`, gradyanlar, gölgeler ve bulanıklık efektlerini birleştiren view'larda özellikle etkilidir. ## Sonuç SwiftUI liste optimizasyonu, lazy loading, geri dönüşüm ve view karşılaştırma mekanizmalarının derin bir anlayışına dayanır. Sunulan teknikler, 60 FPS'de akıcı kaydırmayı korurken binlerce öğeyi işleyebilen arayüzler oluşturmaya olanak tanır. ### SwiftUI Performans Kontrol Listesi - ✅ Koleksiyonlar için `VStack` yerine `LazyVStack` veya `List` kullanın - ✅ Kararlı, benzersiz ID'lerle `Identifiable` uygulayın - ✅ Karmaşık hücreler için `Equatable` benimseyin - ✅ Görselleri görüntülemeden önce önbelleğe alın ve yeniden boyutlandırın - ✅ Akıllı prefetching ile verileri önceden yükleyin - ✅ Etkileşimler (swipe) için `List` veya özel düzenler için `LazyVStack` seçin - ✅ Bölüm başlıkları için `pinnedViews` kullanın - ✅ Büyük veri kümeleri için sayfalama uygulayın - ✅ Düzenli olarak Instruments ile profilleyin - ✅ Karmaşık görsel efektlere sahip view'lara `drawingGroup()` uygulayın --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/tr/blog/ios/swiftui-performance-lazyvstack-complex-lists