17 नव॰ 2025·8 मिनट पढ़ने में

SwiftUI में लंबी सूचियों के प्रदर्शन का अनुकूलन: व्यावहारिक समाधान

SwiftUI में लंबी सूचियों के लिए प्रदर्शन अनुकूलन: री-रेंडर, स्थिर रो पहचान, पेजिंग, इमेज लोडिंग और पुराने iPhone पर स्मूद स्क्रोलिंग के व्यावहारिक समाधान।

SwiftUI में लंबी सूचियों के प्रदर्शन का अनुकूलन: व्यावहारिक समाधान

असली SwiftUI ऐप्स में "धीमी सूचियाँ" कैसी दिखती हैं

SwiftUI में एक "धीमी सूची" सामान्यतः कोई बग नहीं होती। यह वह पल है जब आपकी UI आपकी उंगली के साथ नहीं चल पाती। आप स्क्रोल करते हुए इसको महसूस करते हैं: सूची हिचकिचाती है, फ्रेम गिरते हैं, और सब भारी महसूस होता है।

आम संकेत:

  • स्क्रोलिंग स्टटर करती है, खासकर पुराने डिवाइस पर
  • रो झिलमिलाते हैं या थोड़ी देर के लिए गलत सामग्री दिखाते हैं
  • टैप देरी से रिस्पॉन्ड करते हैं, या स्वाइप एक्शन्स लेट होते हैं
  • फोन गर्म हो जाता है और बैटरी तेज़ी से घटती है
  • मेमोरी स्क्रॉल जितना बढ़ता जाता है उतनी बढ़ती रहती है

लंबी सूचियाँ तब भी धीमी लग सकती हैं जब हर रो "छोटी" दिखे, क्योंकि लागत केवल पिक्सल ड्रॉ करने की नहीं होती। SwiftUI को हर रो की पहचान करनी होती है, लेआउट कम्प्यूट करना होता है, फोंट और इमेजेस रिज़ॉल्व करने होते हैं, आपका फॉर्मैटिंग कोड चलाना होता है, और डेटा बदलने पर अपडेट्स का डिफ़ निकालना होता है। अगर इनमें से कोई काम बार-बार होता है, तो सूची हॉटस्पॉट बन जाती है।

यह भी मदद करता है दो विचारों को अलग करना। SwiftUI में एक "re-render" अक्सर उस दृश्य की body का फिर से कम्प्यूट होना होता है। वह हिस्सा आम तौर पर सस्ता होता है। महँगी चीज़ वह है जो उस कम्प्यूटेशन को ट्रिगर करती है: भारी लेआउट, इमेज डिकोडिंग, टेक्स्ट मापन, या कई रो का फिर से बनना क्योंकि SwiftUI ने उनकी पहचान बदलती देखी।

सोचिए 2,000 संदेश वाली एक चैट। हर सेकंड नए संदेश आते हैं, और हर रो टाइमस्टैम्प फॉर्मैट करता है, मल्टी-लाइन टेक्स्ट नापता है, और अवतार लोड करता है। भले ही आप केवल एक आइटम जोड़ें, एक खराब स्कोप्ड स्टेट चेंज कई रो को फिर से मूल्यांकन करा सकता है, और उनमें से कुछ को फिर से ड्रॉ भी करवा सकता है।

लक्ष्य माइक्रो-ऑप्टिमाइज़ेशन नहीं है। आप स्मूद स्क्रोलिंग, इंस्टेंट टैप्स और केवल उन्हीं रो को अपडेट करना चाहते हैं जो वास्तव में बदले हैं। नीचे दिए गए फिक्स स्थिर पहचान, सस्ते रो, अनावश्यक अपडेट कम करना, और नियंत्रित लोडिंग पर ध्यान देते हैं।

मुख्य कारण: पहचान, प्रति-रो काम, और अपडेट स्टॉर्म

जब SwiftUI लिस्ट धीमी लगती है, तो आमतौर पर कारण "बहुत सारी रो" नहीं होते, बल्कि स्क्रोल करते समय होने वाला अतिरिक्त काम होता है: रो का री-बिल्ड, लेआउट का फिर से कैलकुलेशन, या बार-बार इमेजेस का री-लोड होना।

ज्यादातर रूट कारण तीन बकेट में आते हैं:

  • अस्थिर पहचान: रो का कोई सुसंगत id नहीं है, या आप \\.self का उपयोग कर रहे हैं उन वैल्यूज़ के लिए जो बदल सकती हैं। SwiftUI पुराने रो को नए रो से मैच नहीं कर पाता और ज़रूरत से ज़्यादा री-बिल्ड कर देता है।
  • प्रति-रो बहुत ज्यादा काम: तारीख फॉर्मैटिंग, फ़िल्टरिंग, इमेज रिसाइज़, या रो के अंदर नेटवर्क/डिस्क काम।
  • अपडेट स्टॉर्म: एक बदलाव (टाइपिंग, टाइमर टिक, प्रोग्रेस) बार-बार स्टेट अपडेट्स ट्रिगर करता है और सूची बार-बार रिफ्रेश होती है।

उदाहरण: आपके पास 2,000 ऑर्डर हैं। हर रो मुद्रा फॉर्मैट करता है, एट्रिब्यूटेड स्ट्रिंग बनाता है, और इमेज फेच शुरू करता है। साथ में, पैरेंट व्यू में एक "last synced" टाइमर हर सेकंड अपडेट होता है। भले ही ऑर्डर डेटा न बदले, वह टाइमर लिस्ट को इतना अक्सर इनवैलिडेट कर सकता है कि स्क्रोलिंग चॉकी लगे।

क्यों List और LazyVStack अलग महसूस कर सकते हैं

List सिर्फ एक स्क्रोल व्यू से ज़्यादा है। यह टेबल/कलेक्शन व्यवहार और सिस्टम ऑप्टिमाइज़ेशन के आसपास डिज़ाइन किया गया है। यह अक्सर बड़े डेटासेट्स को कम मेमोरी के साथ हैंडल करता है, पर यह पहचान और बार-बार अपडेट्स के प्रति संवेदनशील हो सकता है।

ScrollView + LazyVStack आपको लेआउट और विज़ुअल्स पर ज़्यादा नियंत्रण देता है, पर यह भी आसान बनाता है कि आप अनजाने में एक्स्ट्रा लेआउट काम करवा दें या महँगे अपडेट्स ट्रिगर कर दें। पुराने डिवाइस पर वह अतिरिक्त काम जल्दी दिखता है।

UI को री-राइट करने से पहले मापें। छोटी-छोटी फिक्स — स्थिर IDs, रो से काम हटाना, और स्टेट चर्न घटाना — अक्सर कंटेनर बदले बिना समस्या सुलझा देते हैं।

रो पहचान ठीक करें ताकि SwiftUI प्रभावी रूप से डिफ कर सके

जब लंबी सूची जंगी महसूस होती है, तो पहचान अक्सर दोषी होती है। SwiftUI यह तय करता है कि कौन सी रो पुन: उपयोग की जा सकती है IDs की तुलना करके। अगर वे IDs बदलती हैं, SwiftUI रो को नया समझकर पुराने को फेंक देता है और ज़रूरत से ज़्यादा री-बिल्ड कर देता है। इससे रैंडम री-रेंडर्स, स्क्रोल पोजीशन खो जाना, या बिना कारण एनीमेशन चलना दिख सकता है।

सबसे सरल जीत: प्रत्येक रो का id स्थिर रखें और अपने डेटा स्रोत से जोड़ें।

एक आम गलती है व्यू के अंदर पहचान जेनरेट करना:

ForEach(items) { item in
  Row(item: item)
    .id(UUID())
}

यह हर रेंडर पर नया ID बनाता है, इसलिए हर रो हर बार "भिन्न" बन जाती है।

उन IDs को प्राथमिकता दें जो पहले से आपके मॉडल में मौजूद हों, जैसे डेटाबेस प्राइमरी की, सर्वर ID, या एक स्थिर स्लग। अगर आपके पास ऐसा नहीं है, तो मॉडल बनते वक्त एक बार ID बनाएं — न कि व्यू के अंदर।

struct Item: Identifiable {
  let id: Int
  let title: String
}

List(items) { item in
  Row(item: item)
}

इंडिसेस के साथ सावधान रहें। ForEach(items.indices, id: \\.self) पहचान को पोजीशन से जोड़ता है। अगर आप insert, delete, या sort करते हैं, तो रो "खिसक" सकती है और SwiftUI गलत व्यू का पुन: उपयोग कर सकता है। इंडिसेस केवल तब उपयोग करें जब ऐरे सचमुच स्टैटिक हो।

यदि आप id: \\.self का उपयोग करते हैं, तो सुनिश्चित करें कि एलिमेंट का Hashable मान समय के साथ स्थिर रहे। अगर हैश किसी फील्ड के अपडेट होने पर बदलता है, तो रो की पहचान भी बदल जाएगी। Equatable और Hashable के लिए सुरक्षित नियम: उन्हें एक सिंगल, स्थिर ID पर आधारित रखें, न कि सम्पादन योग्य प्रॉपर्टीज़ जैसे name या isSelected पर।

सैनिटी चेक्स:

  • IDs डेटा स्रोत से आते हैं (व्यू के अंदर UUID() नहीं)
  • IDs तब भी नहीं बदलते जब रो की सामग्री बदलती है
  • पहचान ऐरे पोजीशन पर निर्भर न हो जब तक सूची कभी रीरॉयर्ड न हो

री-रेंडर्स घटाएँ — रो व्यू को सस्ता बनाएं

लंबी सूची अक्सर इसलिए धीमी लगती है क्योंकि हर बार SwiftUI body का पुन: मूल्यांकन होने पर हर रो बहुत ज़्यादा काम कर देती है। लक्ष्य सरल है: हर रो को फिर से बनाना सस्ता रखें।

एक छुपी हुई लागत बड़े वैल्यूज़ को रो में पास करने से होती है। बड़े structs, गहरे नेस्टेड मॉडल, या भारी कम्प्यूटेड प्रॉपर्टीज़ तब भी अतिरिक्त काम करवा सकती हैं जब UI अपरिवर्तित दिखे। आप बार-बार स्ट्रिंग्स बना रहे होंगे, तारीखें पार्स कर रहे होंगे, इमेजेस रिसाइज़ कर रहे होंगे, या जटिल लेआउट ट्री बना रहे होंगे — और शायद आप इसे महसूस भी नहीं कर रहे।

महँगा काम body से बाहर ले जाएँ

अगर कुछ धीमा है, तो उसे रो body के अंदर बार-बार न बनाएं। जब डेटा आए तब उसे प्रीकम्प्यूट करें, अपने व्यू-मॉडल में कैश करें, या छोटे हेलीपर में मेमोइज़ करें।

रो-लेवल लागत जो तेजी से बढ़ जाती हैं:

  • प्रति-रो नया DateFormatter या NumberFormatter बनाना
  • body में भारी स्ट्रिंग फॉर्मैटिंग (joins, regex, markdown parsing)
  • body में .map या .filter के साथ व्युत्पन्न अर्रे बनाना
  • व्यू में बड़ा ब्लॉब पढ़ना और कनवर्ट करना (जैसे JSON डीकोड करना)
  • कई नेस्टेड स्टैक्स और कंडीशनल्स के साथ ओवरली कॉम्प्लेक्स लेआउट

सरल उदाहरण: फॉर्मैटर्स को स्टैटिक रखें, और रो में पहले से फॉर्मैटेड स्ट्रिंग पास करें।

enum Formatters {
    static let shortDate: DateFormatter = {
        let f = DateFormatter()
        f.dateStyle = .medium
        f.timeStyle = .none
        return f
    }()
}

struct OrderRow: View {
    let title: String
    let dateText: String

    var body: some View {
        HStack {
            Text(title)
            Spacer()
            Text(dateText).foregroundStyle(.secondary)
        }
    }
}

रो को अलग करें और जहाँ फ़िट हो Equatable का उपयोग करें

अगर केवल एक छोटा हिस्सा बदलता है (जैसे बैज काउंट), तो उसे एक सबव्यू में अलग रखें ताकि बाकी रो स्थिर बनी रहे।

वैल्यू-ड्रिवन UI के लिए, किसी सबव्यू को Equatable बनाना (या EquatableView से रैप करना) SwiftUI को तब काम स्किप करने में मदद कर सकता है जब इनपुट बदलें नहीं। परन्तु equatable इनपुट्स छोटे और विशिष्ट रखें — पूरे मॉडल पर नहीं।

उन स्टेट अपडेट्स को कंट्रोल करें जो पूरी लिस्ट रिफ्रेश कराते हैं

Reduce update storms at the source
विजुअल बिजनेस प्रोसेस के साथ वर्कफ़्लो ऑटोमेट करें और अपनी ऐप लॉजिक हर जगह सुसंगत रखें।
प्रोजेक्ट शुरू करें

कभी-कभी रो ठीक होते हैं, पर कुछ चीज़ें बार-बार SwiftUI को पूरी सूची रिफ्रेश करने को कहती रहती हैं। स्क्रोलिंग के दौरान भी छोटे-छोटे एक्स्ट्रा अपडेट्स स्टटर में बदल सकते हैं, खासकर पुराने डिवाइस पर।

एक आम कारण मॉडल को बार-बार फिर से बनाना है। अगर पैरेंट व्यू हर बार री-बिल्ड होता है और आपने उस व्यू के मालिकाना मॉडल के लिए @ObservedObject का उपयोग किया हुआ है, तो SwiftUI उसे फिर से बना सकता है, सब्सक्रिप्शन्स रीसेट कर सकता है, और नए publishes ट्रिगर कर सकता है। अगर व्यू मॉडल का मालिक है तो @StateObject का उपयोग करें ताकि वह एक बार बनाए और स्थिर रहे। बाहर से इंजेक्ट किए जाने पर @ObservedObject इस्तेेमाल करें।

एक और चुपचाप प्रदर्शन मारने वाली चीज़ बहुत बार प्रकाशित होना है। टाइमर्स, Combine पाइपलाइन्स, और प्रोग्रेस अपडेट्स सेकंड में कई बार फायर कर सकते हैं। अगर कोई प्रकाशित प्रॉपर्टी सूची को प्रभावित करती है (या स्क्रीन द्वारा उपयोग किए जा रहे साझा ObservableObject पर है), तो हर टिक सूची को इनवैलिडेट कर सकती है।

उदाहरण: आपके पास एक सर्च फ़ील्ड है जो हर कीस्ट्रोक पर query अपडेट करता है, और फिर 5,000 आइटम्स को फ़िल्टर करता है। यदि आप तुरंत फ़िल्टर करते हैं, तो यूज़र टाइप करते समय सूची लगातार रीडिफ़ होती रहेगी। क्वेरी को डीबॉन्स करें, और थोड़ी देरी के बाद फ़िल्टरेड अर्रे अपडेट करें।

सहायक पैटर्न:

  • तेज़-बदलने वाली वैल्यूज़ को उस ऑब्जेक्ट से बाहर रखें जो लिस्ट को ड्राइव करता है (छोटे ऑब्जेक्ट्स या लोकल @State का उपयोग करें)
  • सर्च और फ़िल्टरिंग को डीबॉन्स करें ताकि सूची टाइपिंग के रुकने पर ही अपडेट हो
  • हाई-फ्रिक्वेंसी टाइमर पब्लिशेस से बचें; कम बार अपडेट करें या तभी अपडेट करें जब वैल्यू वास्तव में बदले
  • प्रति-रो स्टेट लोकल रखें (जैसे रो में @State) बजाय एक ग्लोबल वैल्यू के जो लगातार बदलता हो
  • बड़े मॉडल को बाँटें: एक ObservableObject सूची डेटा के लिए, दूसरा स्क्रीन-लेवल UI स्टेट के लिए

आइडिया सरल है: स्क्रोलिंग का समय शांत रखें। अगर कुछ महत्वपूर्ण नहीं बदला, तो सूची को काम करने के लिए न कहें।

सही कंटेनर चुनें: List बनाम LazyVStack

आप जो कंटेनर चुनते हैं वह iOS कितना काम आपके लिए करता है, उसे प्रभावित करता है।

List आम तौर पर सुरक्षित विकल्प है जब आपकी UI एक मानक टेबल जैसी दिखती है: टेक्स्ट, इमेज, स्वाइप एक्शन्स, सिलेक्शन, सेपरेटर, एडिट मोड, और पहुँचनीयता। अंदरूनी तौर पर, यह वर्षों से प्लेटफ़ॉर्म ऑप्टिमाइज़ेशन्स का लाभ उठाता है।

ScrollView के साथ LazyVStack तब अच्छा है जब आपको कस्टम लेआउट चाहिए: कार्ड, मिक्स्ड सामग्री ब्लॉक्स, स्पेशल हेडर्स, या फ़ीड-स्टाइल डिज़ाइन। “Lazy” का मतलब है कि यह स्क्रीन पर आने पर रो बनाता है, पर यह हर केस में List जैसा व्यवहार नहीं देता। बहुत बड़े डेटासेट्स के साथ यह अधिक मेमोरी उपयोग और पुराने डिवाइस पर ज़्यादा चॉप्पी स्क्रोलिंग दिखा सकता है।

सरल निर्णय नियम:

  • क्लासिक टेबल स्क्रीन (settings, inboxes, orders, admin lists) के लिए List का उपयोग करें
  • कस्टम लेआउट और मिक्स्ड कंटेंट के लिए ScrollView + LazyVStack का उपयोग करें
  • अगर हजारों आइटम हैं और आपको केवल टेबल चाहिए, तो List से शुरू करें
  • अगर पिक्सल-परफेक्ट नियंत्रण चाहिए तो LazyVStack आज़माएँ और फिर मेमोरी और फ्रेम ड्रॉप्स मापें

यह भी ध्यान रखें कि स्टाइलिंग जो चुपचाप स्क्रोलिंग धीमा कर दे सकती है। प्रति-रो इफेक्ट्स जैसे शैडो, ब्लर, और जटिल ओवरले अतिरिक्त रेंडरिंग काम करवा सकते हैं। अगर आप डेप्थ चाहते हैं, तो भारी इफेक्ट्स को छोटे एलिमेंट्स (जैसे आइकन) पर लागू करें बजाय पूरे रो के।

कंक्रीट उदाहरण: 5,000 रो वाला "Orders" स्क्रीन अक्सर List में स्मूद रहता है क्योंकि रो रिप्रोयोग होते हैं। अगर आप LazyVStack में कार्ड-स्टाइल रो बनाते हैं जिनमें बड़े शैडो और कई ओवरले होते हैं, तो आप जंक देख सकते हैं भले ही कोड क्लीन लगे।

पेजिंग जिससे स्मूद लगे और मेमोरी स्पाइक्स न आएँ

Iterate without technical debt
मैसेजिंग और AI इंटीग्रेशन्स को बिना री-राइट किए जोड़ें और तकनीकी कर्ज़ के बिना इटरेट करें।
प्रोटोटाइप बनाएं

पेजिंग लंबी सूचियों को तेज़ रखती है क्योंकि आप कम रो रेंडर करते हैं, कम मॉडल मेमोरी में रखते हैं, और SwiftUI को कम डिफिंग काम मिलता है।

एक स्पष्ट पेजिंग कॉन्ट्रैक्ट से शुरू करें: एक फिक्स्ड पेज साईज़ (जैसे 30–60 आइटम), एक "कोई और परिणाम नहीं" फ़्लैग, और एक लोडिंग रो जो केवल तब दिखे जब आप फेट्च कर रहे हों।

एक आम जाल आखिरी रो दिखने पर ही अगला पेज ट्रिगर करना है। वह अक्सर बहुत देर से होता है, इसलिए उपयोगकर्ता अंत में पहुँचता है और रुकावट देखता है। इसके बजाय, अंतिम कुछ रो में से किसी एक के दिखते ही लोड करना शुरू करें।

सरल पैटर्न:

@State private var items: [Item] = []
@State private var isLoading = false
@State private var reachedEnd = false

func loadNextPageIfNeeded(currentIndex: Int) {
    guard !isLoading, !reachedEnd else { return }
    let threshold = max(items.count - 5, 0)
    guard currentIndex >= threshold else { return }

    isLoading = true
    Task {
        let page = try await api.fetchPage(after: items.last?.id)
        await MainActor.run {
            let newUnique = page.filter { p in !items.contains(where: { $0.id == p.id }) }
            items.append(contentsOf: newUnique)
            reachedEnd = page.isEmpty
            isLoading = false
        }
    }
}

यह डुप्लिकेट रो (ओवरलैपिंग API रिज़ल्ट्स), कई onAppear कॉल्स से रेस कंडीशन्स, और बहुत अधिक लोड करने जैसी सामान्य समस्याओं से बचता है।

अगर आपकी सूची पुल-टू-रिफ्रेश सपोर्ट करती है, तो पेजिंग स्टेट को सावधानी से रीसेट करें (items क्लियर करें, reachedEnd रीसेट करें, इन-फ्लाइट टास्क्स को संभव हो तो कैंसिल करें)। अगर आप बैकएंड को नियंत्रित करते हैं, तो स्थिर IDs और करसर-आधारित पेजिंग UI को स्पष्ट रूप से स्मूद बनाते हैं।

इमेजेस, टेक्स्ट, और लेआउट: रो रेंडरिंग को हल्का रखें

Keep row work off the UI
फॉर्मैटिंग और बिजनेस नियम बैकएंड लॉजिक में रखें ताकि डिवाइस पर रोws हल्के रहें।
ऐप बनाएं

लंबी सूचियाँ आम तौर पर कंटेनर की वजह से कम और रो की वजह से ज़्यादा धीमी लगती हैं। इमेजेस सामान्यतः सबसे बड़ा दोषी होते हैं: डिकोडिंग, रिसाइज़िंग, और ड्रॉइंग आपकी स्क्रोल स्पीड से तेज़ हो सकते हैं, खासकर पुराने डिवाइस पर।

यदि आप रिमोट इमेज लोड करते हैं, तो सुनिश्चित करें कि भारी काम स्क्रोल के दौरान मेन थ्रेड पर न हो। साथ ही पूर्ण-रिज़ॉल्यूशन असेट्स को 44–80pt थंबनेल के लिए डाउनलोड करने से बचें।

उदाहरण: अवतार्स वाली एक "Messages" स्क्रीन लें। अगर हर रो 2000x2000 इमेज डाउनलोड करती है, उसे स्केल करती है, और उस पर ब्लर या शैडो लगाती है, तो सूची स्टटर करेगी भले ही आपका डेटा मॉडल सरल हो।

इमेज काम को अनुमान्य रखें

प्रभावी आदतें:

  • सर्वर-साइड या प्री-जनरेटेड थंबनेल का उपयोग करें जो दिखाए जाने वाले साइज के करीब हों
  • जहां संभव हो, मेन थ्रेड के बाहर डिकोड और रिसाइज़ करें
  • थंबनेल्स को कैश करें ताकि तेज़ स्क्रोलिंग पर री-फेच या री-डिकोड न हो
  • ऐसा प्लेसहोल्डर उपयोग करें जो फाइनल साइज से मेल खाता हो ताकि फ्लिकर और लेआउट जंप से बचा जा सके
  • रो में इमेज पर भारी मॉडिफायर्स (भारी शैडोज़, मास्क, ब्लर) से बचें

लेआउट को स्थिर रखें ताकि थ्रैश न हो

अगर रो की हाइट लगातार बदलती रहे, तो SwiftUI मापने में अधिक समय बिताता है बनाम ड्रॉ करने में। रो को अनुमान्य रखें: थंबनेल के लिए फिक्स्ड फ्रेम, स्थिर लाइन लिमिट्स, और स्थिर स्पेसिंग। अगर टेक्स्ट बढ़ सकता है तो उसे कैप करें (उदा. 1–2 लाइनें) ताकि एक अपडेट पूरे मापन को फिर से न करवाए।

प्लेसहोल्डर भी मायने रखते हैं। एक ग्रे सर्कल जो बाद में अवतार बनता है उसे वही फ्रेम देना चाहिए ताकि रो स्क्रोल के बीच में फिर से फ्लो न करे।

मापने का तरीका: Instruments चेक्स जो असली बॉटलनेक्स दिखाते हैं

केवल "ऐसा लग रहा है" पर निर्णय लेना परफॉर्मेंस वर्क को गेसिंग बना देता है। Instruments आपको दिखाता है कि क्या मेन थ्रेड पर चलता है, तेज़ स्क्रोल के दौरान क्या अलोकेट होता है, और कौन सी चीज़ ड्रॉप फ्रेम्स कारण बनती है।

रियल डिवाइस (यदि आप उसे सपोर्ट करते हैं तो एक पुराना डिवाइस) पर बेसलाइन परिभाषित करें। एक दोहराने योग्य क्रिया करें: स्क्रीन खोलें, तेज़ी से ऊपर से नीचे स्क्रोल करें, एक बार लोड-मोर ट्रिगर करें, फिर वापस ऊपर स्क्रोल करें। सबसे खराब हिच-पॉइंट्स, मेमोरी पीक, और क्या UI रिस्पॉन्सिव रहता है नोट करें।

तीन Instruments व्यूज़ जो असरदार हैं

इन्हें साथ में उपयोग करें:

  • Time Profiler: स्क्रोल के दौरान मेन-थ्रेड स्पाइक्स देखें। लेआउट, टेक्स्ट मापन, JSON पार्सिंग, और इमेज डिकोडिंग यहाँ अक्सर हिच बताते हैं।
  • Allocations: तेज़ स्क्रोल के दौरान अस्थायी ऑब्जेक्ट्स के सर्ज पर नज़र रखें। यह अक्सर बार-बार फॉर्मैटिंग, नए एट्रिब्यूटेड स्ट्रिंग्स, या प्रति-रो मॉडल्स के फिर से बनने की तरफ़ इशारा करता है।
  • Core Animation: ड्रॉप फ्रेम्स और लंबे फ्रेम टाइम्स की पुष्टि करें। यह रेंडरिंग प्रेशर को धीमे डाटा काम से अलग करने में मदद करता है।

जब आप स्पाइक मिलते हैं, तो कॉल ट्री में क्लिक करें और पूछें: क्या यह स्क्रीन पर एक बार हो रहा है, या हर रो, हर स्क्रोल पर? दूसरी श्रेणी वही है जो स्मूद स्क्रोलिंग तोड़ती है।

स्क्रोल और पेजिनेशन इवेंट्स के लिए साइनपोस्ट जोड़ें

कई ऐप्स स्क्रोल करते समय एक्स्ट्रा काम करते हैं (इमेज लोड्स, पेजिनेशन, फ़िल्टरिंग)। साइनपोस्ट्स आपको टाइमलाइन पर उन क्षणों को दिखाने में मदद करते हैं।

import os
let log = OSLog(subsystem: "com.yourapp", category: "list")
os_signpost(.begin, log: log, name: "LoadMore")
// fetch next page
os_signpost(.end, log: log, name: "LoadMore")

हर बदल के बाद फिर से टेस्ट करें, एक-एक करके। अगर FPS सुधरता है पर Allocations बढ़ जाते हैं, तो आपने शायद स्टटर के बदले मेमोरी दबाव लिया है। बेसलाइन नोट रखें और केवल उन चेंजेस को रखें जो नंबर सही दिशा में ले जाते हैं।

सामान्य गलतियाँ जो चुपचाप लिस्ट परफॉर्मेंस को मार देती हैं

Get identity right from day one
SwiftUI UI के पहले ही स्थिर IDs और PostgreSQL टेबल विजुअली डिज़ाइन करें।
प्रोजेक्ट बनाएं

कुछ समस्याएँ स्पष्ट हैं (बड़ी इमेजेस, विशाल डेटासेट)। अन्य केवल तब दिखती हैं जब डेटा बढ़े, खासकर पुराने डिवाइस पर।

1) अस्थिर रो IDs

एक क्लासिक गलती है व्यू के अंदर IDs बनाना, जैसे id: \\.self को रेफरेंस टाइप्स के लिए उपयोग करना, या UUID() को रो बॉडी में बनाना। SwiftUI अपडेट्स को डिफ करने के लिए पहचान का उपयोग करता है। अगर ID बदलती है, SwiftUI रो को नया समझेगा, उसे री-बिल्ड करेगा, और कभी-कभी कैश्ड लेआउट फेंक देगा।

अपने मॉडल से एक स्थिर ID (डेटाबेस की, सर्वर ID, या जब आइटम बनाया गया हो तब स्टोर किया गया UUID) उपयोग करें। अगर आपके पास नहीं है तो जोड़ें।

2) onAppear के अंदर भारी काम

onAppear उतना ही बार चलता है जितना लोग उम्मीद नहीं करते क्योंकि रो स्क्रोल के दौरान आते और जाते रहते हैं। अगर हर रो onAppear में इमेज डिकोडिंग, JSON पार्सिंग, या DB लुकअप शुरू करती है, तो आपको बार-बार स्पाइक्स मिलेंगे।

भारी काम रो से बाहर ले जाएँ। डेटा लोड होते ही प्रीकम्प्यूट करें, नतीजों को कैश करें, और onAppear को सस्ते एक्शन्स तक सीमित रखें (जैसे अंतिम के पास पेजिनेशन ट्रिगर करना)।

3) पूरी सूची को रो एडिट्स से बाँधना

जब हर रो को बड़ी ऐरे में @Binding मिलती है, एक छोटा एडिट बड़ा चेंज लग सकता है। इससे कई रो फिर से मूल्यांकन कर सकती हैं, और कभी-कभी पूरी सूची रिफ्रेश हो सकती है।

रो में इम्युटेबल वैल्यूज़ पास करें और परिवर्तनों को हल्के एक्शन के तौर पर वापस भेजें (उदा. "toggle favorite for id")। केवल वही स्टेट रो में रखें जो सचमुच वहीं होना चाहिए।

4) स्क्रोलिंग के दौरान बहुत सारी एनीमेशन

सूची में एनीमेशन महंगा हो सकता है क्योंकि वे अतिरिक्त लेआउट पास ट्रिगर कर सकते हैं। ऊँचे स्तर पर animation(.default, value:) या बार-बार बदलने वाली वैल्यूज़ पर इम्प्लिसिट एनीमेशन स्क्रोलिंग को चिपकाऊ बना सकती हैं।

सरल रखें:

  • एनीमेशन को केवल उस एक रो तक सीमित रखें जो बदलती है
  • तेज़ स्क्रॉल के दौरान एनिमेट करने से बचें (खासकर सिलेक्शन/हाइलाइट के लिए)
  • बार-बार बदलने वाली वैल्यूज़ पर इम्प्लिसिट एनीमेशन से सावधान रहें
  • जटिल सम्मिलन के बजाय सरल ट्रांज़िशन्स को प्राथमिकता दें

एक असली उदाहरण: एक चैट-स्टाइल लिस्ट जहाँ हर रो onAppear में नेटवर्क फेच शुरू करे, UUID() को id के रूप में उपयोग करे, और "seen" स्टेट परिवर्तन को एनीमेट करे। यह संयोजन निरंतर रो चर्न पैदा करता है। पहचान ठीक करने, काम कैश करने, और एनीमेशन सीमित करने से वही UI तुरंत स्मूद महसूस कर सकता है।

त्वरित चेकलिस्ट, एक सरल उदाहरण, और अगले कदम

अगर आप केवल कुछ ही चीजें करेंगे, तो यहाँ से शुरू करें:

  • प्रत्येक रो के लिए स्थिर, यूनिक id का उपयोग करें (ना ऐरे इंडेक्स, ना हर बार बनता UUID)
  • रो का काम छोटा रखें: भारी फॉर्मैटिंग, बड़े व्यू ट्री, और महँगी कम्प्यूटेड प्रॉपर्टीज़ body में टालें
  • पब्लिशेस को नियंत्रित करें: तेज़-बदलने वाली स्टेट (timers, टाइपिंग, नेटवर्क प्रोग्रेस) को पूरी सूची को इनवैलिडेट न करने दें
  • पेजिंग और प्रीफ़ेचिंग से मेमोरी फ्लैट रखें
  • Instruments से पहले और बाद में मापें ताकि आप अंदाज़ पर न रहें

एक सपोर्ट इनबॉक्स का कल्पना करें जिसमें 20,000 कन्वर्सेशन हों। हर रो सब्जेक्ट, आखिरी संदेश प्रिव्यू, टाइमस्टैम्प, अनरीड बैज, और अवतार दिखाती है। उपयोगकर्ता सर्च कर सकते हैं, और नए संदेश आते रहते हैं। धीमा वर्शन आमतौर पर एक साथ कई गलतियाँ करता है: यह हर कीस्ट्रोक पर रो को फिर से बनाता है, टेक्स्ट को बार-बार मापता है, और बहुत सारी इमेजेस पहले ही फेच कर लेता है।

एक व्यावहारिक योजना जो पूरे कोडबेस को तोड़े बिना काम करे:

  • बेसलाइन: Instruments (Time Profiler + Core Animation) में एक छोटा स्क्रोल और सर्च सत्र रिकॉर्ड करें।
  • पहचान ठीक करें: सुनिश्चित करें कि आपका मॉडल एक असली id रखता है और ForEach उसे लगातार उपयोग करे।
  • पेजिंग जोड़ें: सबसे पहले नवीनतम 50–100 आइटम लोड करें, फिर उपयोगकर्ता के नजदीक पहुँचने पर और लोड करें।
  • इमेज को ऑप्टिमाइज़ करें: छोटे थंबनेल्स का उपयोग करें, रिज़ल्ट्स कैश करें, और मेन थ्रेड पर डिकोडिंग से बचें।
  • फिर से मापें: सुनिश्चित करें कि लेआउट पास कम हुए हैं, व्यू अपडेट्स कम हुए हैं, और पुराने डिवाइस पर फ्रेम टाइम्स स्थिर हुए हैं।

यदि आप एक पूरा प्रोडक्ट बना रहे हैं (iOS ऐप प्लस बैकएंड और वेब एडमिन पैनल), तो डेटा मॉडल और पेजिंग कॉन्ट्रैक्ट को जल्दी डिज़ाइन करना भी मदद करता है। प्लेटफ़ॉर्म जैसे AppMaster (appmaster.io) उस फुल-स्टैक वर्कफ़्लो के लिए बनाए गए हैं: आप विजुअली डेटा और बिजनेस लॉजिक परिभाषित कर सकते हैं, और फिर वास्तविक सोर्स कोड जनरेट या सेल्फ-होस्ट कर सकते हैं।

सामान्य प्रश्न

मेरी SwiftUI लिस्ट जब स्टटर करती है तो सबसे तेज़ फिक्स क्या है?

सबसे पहले रो पहचान (row identity) ठीक करें। अपने मॉडल से स्थिर id का उपयोग करें और व्यू के अंदर IDs जनरेट करने से बचें, क्योंकि बदलती IDs SwiftUI को हर बार रो को नया समझने पर मजबूर करती हैं और ज़रूरत से ज़्यादा री-बिल्ड करवा देती हैं।

क्या SwiftUI इसलिए धीमा है क्योंकि यह "बहुत ज़्यादा रेंडर" करता है?

body का फिर से गणना होना आम तौर पर महंगा नहीं होता; महंगा वह काम है जो उस गणना से ट्रिगर होता है। भारी लेआउट, टेक्स्ट मापना, इमेज डिकोडिंग, और अस्थिर पहचान की वजह से कई रो का री-बिल्ड ही आमतौर पर फ्रेम ड्रॉप्स का कारण बनता है।

`ForEach` और `List` के लिए स्थिर `id` कैसे चुनूं?

रो या ForEach के लिए id के रूप में UUID() का उपयोग न करें और यदि डेटा insert/delete/reorder हो सकता है तो ऐरे इंडेक्स पर निर्भर न करें। सर्वर/डाटाबेस ID या मॉडल बनाते समय स्टोर किया गया UUID बेहतर विकल्प है ताकि ID अपडेट्स के बीच स्थिर रहे।

`id: .self` लिस्ट परफॉर्मेंस खराब कर सकता है?

id: .self तब समस्या कर सकता है जब उस वैल्यू का हैश ऐसे फ़ील्ड्स पर निर्भर करे जो बदले जा सकते हैं। बेहतर है कि Hashable/Equatable किसी एक स्थिर पहचान पर आधारित हों, न कि संपादन योग्य प्रॉपर्टीज़ जैसे name या isSelected पर।

रो के `body` के अंदर मुझे क्या नहीं करना चाहिए?

महँगा काम body के अंदर न रखें। तारीखें और नंबर पहले से फॉर्मेट करें, प्रति-रो नया Formatter बनाना टालें, और बड़े map/filter जैसी व्युत्पन्न अर्रे बिल्डिंग को मॉडल/व्यू-मॉडल में करें और रो को डिस्प्ले-रेडी छोटे वैल्यू दें।

मेरी लम्बी लिस्ट में `onAppear` इतनी बार क्यों चल रहा है?

onAppear स्क्रॉल के दौरान अक्सर फ़ायर होता है क्योंकि रो स्क्रीन पर आते और जाते रहते हैं। अगर हर रो में onAppear पर भारी काम (इमेज डीकोडिंग, DB पढ़ना, पार्सिंग) शुरू होता है तो बार-बार स्पाइक्स मिलेंगे; onAppear को केवल सस्ती कार्रवाइयों तक सीमित रखें जैसे पेजिनेशन ट्रिगर करना।

कौन सी चीज़ें "अपडेट स्टॉर्म" पैदा करती हैं जो स्क्रोलिंग को चिपकन जैसा बना देती हैं?

कोई भी तेज़-तर्रार प्रकाशित वैल्यू जो लिस्ट के साथ शेयर होती है, बार-बार उसे इनवैलिडेट कर सकती है। टाइमर्स, टाइपिंग स्टेट, और प्रोग्रेस अपडेट्स को मुख्य ऑब्जेक्ट से अलग रखें, सर्च को डीबॉन्स करें, और बड़े ObservableObject को छोटे-छोटे हिस्सों में बाँटें।

बड़े डेटा सेट के लिए `List` vs `LazyVStack` कब चुनूँ?

List तब उपयोग करें जब आपकी UI क्लासिक टेबल जैसी हो (टेक्स्ट, इमेज, स्वाइप एक्शन्स, सिलेक्शन)। कस्टम लेआउट और मिक्स्ड कंटेंट के लिए ScrollView + LazyVStack बेहतर है, पर उसे प्रयोग करने से पहले मेमोरी और फ्रेम ड्रॉप्स नापें क्योंकि वह अधिक काम करवा सकता है।

एक स्मूद पेजिंग का सरल तरीका क्या है?

बहुत आखिरी रो दिखने पर पेज लोड करना अक्सर देर हो जाता है—वाले उपयोगकर्ता रुकावट देखेंगे। इसके बजाय आखिरी कुछ रोमें पहुँचने पर अगला पेज लोड करना शुरू करें, isLoading और reachedEnd जैसी गार्ड्स रखें, और स्थिर IDs से डुप्लिकेट्स को फ़िल्टर करें।

मैं अपने SwiftUI लिस्ट की असली बॉटलनेक्स कैसे मापूँ?

रियल डिवाइस पर बेसलाइन रिकॉर्ड करें और Instruments (Time Profiler, Allocations, Core Animation) से मुख्य-थ्रेड स्पाइक्स, एलोकेशन सर्जेस, और ड्रॉप फ्रेम्स देखें। इससे पता चलेगा कि समस्या रेंडरिंग की है या डाटा/काम की।

शुरू करना आसान
कुछ बनाएं अद्भुत

फ्री प्लान के साथ ऐपमास्टर के साथ प्रयोग करें।
जब आप तैयार होंगे तब आप उचित सदस्यता चुन सकते हैं।

शुरू हो जाओ