रिएक्ट एप्लिकेशन डेवलपमेंट में एक व्यापक दृष्टिकोण मेमोइज़ेशन के साथ सब कुछ कवर करना है। कई डेवलपर्स इस ऑप्टिमाइज़ेशन तकनीक को उदारतापूर्वक लागू करते हैं, अनावश्यक री-रेंडर को रोकने के लिए घटकों को मेमोइज़ेशन में लपेटते हैं। सतह पर, यह प्रदर्शन को बढ़ाने के लिए एक मूर्खतापूर्ण रणनीति की तरह लगता है। हालाँकि, सोशल डिस्कवरी ग्रुप टीम ने पाया है कि जब गलत तरीके से लागू किया जाता है, तो मेमोइज़ेशन वास्तव में अप्रत्याशित तरीकों से टूट सकता है, जिससे प्रदर्शन संबंधी समस्याएँ हो सकती हैं। इस लेख में, हम उन आश्चर्यजनक स्थानों का पता लगाएंगे जहाँ मेमोइज़ेशन लड़खड़ा सकता है और अपने रिएक्ट एप्लिकेशन में इन छिपे हुए जाल से कैसे बचें।
मेमोइज़ेशन प्रोग्रामिंग में एक अनुकूलन तकनीक है जिसमें महंगे ऑपरेशन के परिणामों को सहेजना और उन्हें फिर से उपयोग करना शामिल है जब वही इनपुट फिर से सामने आते हैं। मेमोइज़ेशन का सार एक ही इनपुट डेटा के लिए अनावश्यक गणनाओं से बचना है। यह विवरण वास्तव में पारंपरिक मेमोइज़ेशन के लिए सही है। आप कोड उदाहरण में देख सकते हैं कि सभी गणनाएँ कैश की गई हैं।
const cache = { } function calculate (a) { if (Object.hasOwn(cache, a)) { return cache[a] } cache[a] = a * a return cache[a] }
मेमोइज़ेशन कई लाभ प्रदान करता है, जिसमें प्रदर्शन में सुधार, संसाधन की बचत और परिणाम कैशिंग शामिल है। हालाँकि, रिएक्ट का मेमोइज़ेशन तब तक काम करता है जब तक कि नए प्रॉप्स नहीं आते हैं, जिसका अर्थ है कि केवल अंतिम कॉल का परिणाम सहेजा जाता है।
const prev = { value: null, result: null } function calculate(a) { if (prev.value === a) { return prev.result } prev.value = a prev.result = a * a return prev.result }
React लाइब्रेरी हमें मेमोइज़ेशन के लिए कई उपकरण प्रदान करती है। ये हैं HOC React.memo, हुक useCallback, useMemo, और useEvent, साथ ही React.PureComponent और क्लास कॉम्पोनेंट की लाइफ़साइकल विधि shouldComponentUpdate। आइए पहले तीन मेमोइज़ेशन टूल की जाँच करें और React में उनके उद्देश्यों और उपयोग का पता लगाएँ।
रिएक्ट.मेमो
यह हायर-ऑर्डर कंपोनेंट (HOC) अपने पहले तर्क के रूप में एक घटक और दूसरे के रूप में एक वैकल्पिक तुलना फ़ंक्शन स्वीकार करता है। तुलना फ़ंक्शन पिछले और वर्तमान प्रॉप्स की मैन्युअल तुलना की अनुमति देता है। जब कोई तुलना फ़ंक्शन प्रदान नहीं किया जाता है, तो React शैलो इक्वलिटी को डिफ़ॉल्ट करता है। यह समझना महत्वपूर्ण है कि शैलो इक्वलिटी केवल सतह-स्तर की तुलना करती है। नतीजतन, यदि प्रॉप्स में गैर-स्थिर संदर्भों वाले संदर्भ प्रकार होते हैं, तो React घटक के पुनः-रेंडर को ट्रिगर करेगा।
const Button = memo((props) => { return ( <button onClick={props.onClick}> {props.title} </button> ) }, (prevProps, props) => { return props.title === prevProps.title })
React.useCallback
useCallback हुक हमें शुरुआती रेंडर के दौरान पास किए गए फ़ंक्शन के संदर्भ को सुरक्षित रखने की अनुमति देता है। बाद के रेंडर पर, React हुक की निर्भरता सरणी में मानों की तुलना करेगा, और यदि कोई भी निर्भरता नहीं बदली है, तो यह पिछली बार की तरह ही कैश्ड फ़ंक्शन संदर्भ लौटाएगा। दूसरे शब्दों में, useCallback रेंडर के बीच फ़ंक्शन के संदर्भ को तब तक कैश करता है जब तक कि इसकी निर्भरताएँ बदल न जाएँ।
const callback = useCallback(() => { // do something }, [a, b, c])
React.useMemo
useMemo हुक आपको रेंडर के बीच गणना के परिणाम को कैश करने की अनुमति देता है। आम तौर पर, useMemo का उपयोग महंगी गणनाओं को कैश करने के लिए किया जाता है, साथ ही मेमो HOC में लिपटे अन्य घटकों को या हुक में निर्भरता के रूप में किसी ऑब्जेक्ट को पास करते समय उसके संदर्भ को संग्रहीत करने के लिए भी किया जाता है।
const value = useMemo(() => { return [1, 2, 3, 4, 5].filter(it => it % 2 === 0) }, [])
रिएक्ट डेवलपमेंट टीमों में, व्यापक मेमोइज़ेशन एक व्यापक अभ्यास है। इस दृष्टिकोण में आम तौर पर शामिल हैं:
हालांकि, डेवलपर्स हमेशा इस रणनीति की पेचीदगियों को नहीं समझ पाते हैं और यह नहीं समझ पाते हैं कि मेमोइज़ेशन से कितनी आसानी से समझौता किया जा सकता है। मेमो HOC में लिपटे घटकों का अप्रत्याशित रूप से फिर से रेंडर होना असामान्य नहीं है। सोशल डिस्कवरी ग्रुप में, हमने सहकर्मियों को यह दावा करते सुना है, "सब कुछ मेमोइज़ करना बिल्कुल भी मेमोइज़ न करने से ज़्यादा महंगा नहीं है।"
हमने देखा है कि हर कोई मेमोइज़ेशन के एक महत्वपूर्ण पहलू को पूरी तरह से नहीं समझता है: यह बिना किसी अतिरिक्त बदलाव के तभी बेहतर ढंग से काम करता है जब प्रिमिटिव्स को मेमोइज़ किए गए घटक में पास कर दिया जाता है।
ऐसे मामलों में, घटक केवल तभी पुनः प्रस्तुत होगा जब प्रॉप मान वास्तव में बदल गए हों। प्रॉप्स में प्राइमिटिव्स अच्छे हैं।
दूसरा बिंदु तब होता है जब हम प्रॉप्स में संदर्भ प्रकार पास करते हैं। यह याद रखना और समझना महत्वपूर्ण है कि रिएक्ट में कोई जादू नहीं है - यह एक जावास्क्रिप्ट लाइब्रेरी है जो जावास्क्रिप्ट के नियमों के अनुसार काम करती है। प्रॉप्स (फ़ंक्शन, ऑब्जेक्ट, एरे) में संदर्भ प्रकार खतरनाक हैं।
उदाहरण के लिए:
const a = { c: 1 } const b = { c: 1 } a === b // false First call: MemoComponent(a) Second call: MemoComponent(b) const MemoComponent = memo(({object}) => { return <div /> }, (prevProps, props) => (prevProps.object === props.object)) // false
यदि आप कोई ऑब्जेक्ट बनाते हैं, तो समान गुणों और मूल्यों वाला दूसरा ऑब्जेक्ट पहले वाले के बराबर नहीं होगा, क्योंकि उनके संदर्भ भिन्न होंगे।
यदि हम घटक के बाद के कॉल पर समान प्रतीत होने वाली वस्तु को पास करते हैं, लेकिन यह वास्तव में एक अलग वस्तु है (क्योंकि इसका संदर्भ अलग है), तो React द्वारा उपयोग की जाने वाली उथली तुलना इन वस्तुओं को अलग-अलग के रूप में पहचान लेगी। यह मेमो में लिपटे घटक के पुनः-रेंडर को ट्रिगर करेगा, इस प्रकार उस घटक का मेमोइज़ेशन टूट जाएगा।
मेमोइज़्ड घटकों के साथ सुरक्षित संचालन सुनिश्चित करने के लिए, मेमो, useCallback और useMemo के संयोजन का उपयोग करना महत्वपूर्ण है। इस तरह, सभी संदर्भ प्रकारों में निरंतर संदर्भ होंगे।
टीमवर्क: मेमो, useCallback, useMemo
ऊपर वर्णित सब कुछ तार्किक और सरल लगता है, लेकिन आइए इस दृष्टिकोण के साथ काम करते समय की जाने वाली सबसे आम गलतियों पर एक साथ नज़र डालें। उनमें से कुछ सूक्ष्म हो सकते हैं, और कुछ थोड़े ज़्यादा हो सकते हैं, लेकिन हमें उनके बारे में पता होना चाहिए और सबसे महत्वपूर्ण बात यह है कि उन्हें समझना चाहिए ताकि यह सुनिश्चित हो सके कि हम पूर्ण मेमोइज़ेशन के साथ जो तर्क लागू करते हैं वह टूट न जाए।
इनलाइनिंग से मेमोइज़ेशन
आइए एक क्लासिक गलती से शुरू करें, जहां पैरेंट घटक के प्रत्येक बाद के रेंडर पर, मेमोइज्ड घटक MemoComponent लगातार पुनः रेंडर होगा क्योंकि params में पास किए गए ऑब्जेक्ट का संदर्भ हमेशा नया होगा।
const Parent = () => { return ( <MemoComponent params={[1, 2 ,3]} /> ) }
इस समस्या को हल करने के लिए, पहले बताए गए useMemo हुक का उपयोग करना पर्याप्त है। अब, हमारे ऑब्जेक्ट का संदर्भ हमेशा स्थिर रहेगा।
const Parent = () => { const params = useMemo(() => { return [1, 2 ,3] ), []) return ( <MemoComponent params={params} /> ) }
वैकल्पिक रूप से, यदि सारणी में हमेशा स्थैतिक डेटा होता है, तो आप इसे घटक के बाहर स्थिरांक में स्थानांतरित कर सकते हैं।
const params = [1, 2 ,3] const Parent = () => { return ( <MemoComponent params={params} /> ) }
इस उदाहरण में एक समान स्थिति बिना मेमोइज़ेशन के फ़ंक्शन को पास करना है। इस मामले में, पिछले उदाहरण की तरह, मेमोकंपोनेंट का मेमोइज़ेशन इसे एक फ़ंक्शन पास करके तोड़ा जाएगा, जिसमें पैरेंट घटक के प्रत्येक रेंडर पर एक नया संदर्भ होगा। नतीजतन, मेमोकंपोनेंट नए सिरे से रेंडर करेगा जैसे कि इसे याद नहीं किया गया था।
const Parent = () => { return ( <MemoComponent onClick={() => {}} /> ) }
यहां, हम पैरेंट घटक के रेंडर के बीच पास किए गए फ़ंक्शन के संदर्भ को संरक्षित करने के लिए useCallback हुक का उपयोग कर सकते हैं।
const Parent = () => { const handleClick = useCallback(() => console.log('click') }, []) return ( <MemoComponent onClick={handleClick} /> ) }
नोट कर लिया गया.
इसके अलावा, useCallback में, आपको किसी ऐसे फ़ंक्शन को पास करने से कोई नहीं रोकता जो किसी दूसरे फ़ंक्शन को लौटाता है। हालाँकि, यह याद रखना महत्वपूर्ण है कि इस दृष्टिकोण में, फ़ंक्शन `someFunction` को हर रेंडर पर कॉल किया जाएगा। `someFunction` के अंदर जटिल गणनाओं से बचना महत्वपूर्ण है।
function someFunction() { // expensive calculations (?) ... return () => {} } .............................. const Parent = () => { const cachedFunction = useCallback(someFunction(), []) return ( <MemoComponent onClick={cachedFunction} /> ) }
अगली आम स्थिति प्रॉप्स का फैलना है। कल्पना करें कि आपके पास घटकों की एक श्रृंखला है। आप कितनी बार इस बात पर विचार करते हैं कि InitialComponent से पास किया गया डेटा प्रॉप कितनी दूर तक यात्रा कर सकता है, जो इस श्रृंखला में कुछ घटकों के लिए संभावित रूप से अनावश्यक है? इस उदाहरण में, यह प्रॉप ChildMemo घटक में मेमोइज़ेशन को तोड़ देगा क्योंकि InitialComponent के प्रत्येक रेंडर पर, इसका मान हमेशा बदलता रहेगा। एक वास्तविक प्रोजेक्ट में, जहाँ मेमोइज़ किए गए घटकों की श्रृंखला लंबी हो सकती है, सभी मेमोइज़ेशन टूट जाएँगे क्योंकि लगातार बदलते मूल्यों वाले अनावश्यक प्रॉप्स उन्हें पास किए जाते हैं:
const Child = () => {} const ChildMemo = React.memo(Child) const Component = (props) => { return <ChildMemo {...props} /> } const InitialComponent = (props) => { // The only component that has state and can trigger a re-render return ( <Component {...props} data={Math.random()} /> ) }
अपनी सुरक्षा के लिए, सुनिश्चित करें कि मेमोइज़्ड घटक में केवल आवश्यक मान ही पास किए जाएँ। इसके बजाय:
const Component = (props) => { return <ChildMemo {...props} /> }
उपयोग करें (केवल आवश्यक प्रॉप्स पास करें):
const Component = (props) => { return ( <ChildMemo firstProp={prop.firstProp} secondProp={props.secondProp} /> ) )
आइए निम्नलिखित उदाहरण पर विचार करें। एक परिचित स्थिति तब होती है जब हम एक घटक लिखते हैं जो JSX को बच्चों के रूप में स्वीकार करता है।
const ChildMemo = React.memo(Child) const Component = () => { return ( <ChildMemo> <div>Text</div> </ChildMemo> ) }
पहली नज़र में, यह हानिरहित लगता है, लेकिन वास्तव में, ऐसा नहीं है। आइए उस कोड पर करीब से नज़र डालें जहाँ हम JSX को मेमोइज़्ड कॉम्पोनेंट में बच्चों के रूप में पास करते हैं। यह सिंटैक्स कुछ और नहीं बल्कि इस `div` को `children` नामक प्रॉप के रूप में पास करने के लिए सिंटैक्टिक शुगर है।
बच्चे किसी भी अन्य प्रॉप से अलग नहीं हैं जिसे हम किसी घटक को पास करते हैं। हमारे मामले में, हम JSX पास कर रहे हैं, और JSX, बदले में, `createElement` विधि के लिए वाक्यविन्यास शर्करा है, इसलिए अनिवार्य रूप से, हम `div` प्रकार के साथ एक नियमित ऑब्जेक्ट पास कर रहे हैं। और यहाँ, एक मेमोइज़्ड घटक के लिए सामान्य नियम लागू होता है: यदि एक गैर-मेमोइज़्ड ऑब्जेक्ट प्रॉप्स में पास किया जाता है, तो घटक को फिर से रेंडर किया जाएगा जब उसका पैरेंट रेंडर किया जाएगा, क्योंकि हर बार इस ऑब्जेक्ट का संदर्भ नया होगा।
इस तरह की समस्या के समाधान पर कुछ सार तत्वों में पहले ही चर्चा की जा चुकी है, गैर-मेमोकृत ऑब्जेक्ट के पासिंग से संबंधित ब्लॉक में। इसलिए, यहाँ, पास की जा रही सामग्री को useMemo का उपयोग करके मेमोकृत किया जा सकता है, और इसे चाइल्डमेमो में बच्चों के रूप में पास करने से इस घटक का मेमोकरण नहीं टूटेगा।
const Component = () => { const childrenContent = useMemo( () => <div>Text</div>, [], ) return ( <ChildMemo> {childrenContent} </ChildMemo> ) }
आइये एक और दिलचस्प उदाहरण पर विचार करें।
const ParentMemo = React.memo(Parent) const ChildMemo = React.memo(Child) const App = () => { return ( <ParentMemo> <ChildMemo /> </ParentMemo> ) }
पहली नज़र में, यह हानिरहित लगता है: हमारे पास दो घटक हैं, जिनमें से दोनों को याद किया जाता है। हालाँकि, इस उदाहरण में, ParentMemo इस तरह व्यवहार करेगा जैसे कि यह मेमो में लपेटा नहीं गया है क्योंकि इसके बच्चे, ChildMemo, याद नहीं हैं। ChildMemo घटक का परिणाम JSX होगा, और JSX केवल React.createElement के लिए वाक्यविन्यास शर्करा है, जो एक ऑब्जेक्ट लौटाता है। इसलिए, React.createElement विधि निष्पादित होने के बाद, ParentMemo और ChildMemo नियमित JavaScript ऑब्जेक्ट बन जाएंगे, और इन ऑब्जेक्ट को किसी भी तरह से याद नहीं किया जाएगा।
पेरेंटमेमो | चाइल्डमेमो |
---|---|
| |
परिणामस्वरूप, हम एक गैर-मेमोइज़्ड ऑब्जेक्ट को प्रॉप्स में पास करते हैं, जिससे पैरेंट घटक का मेमोइज़ेशन टूट जाता है।
const ParentMemo = React.memo(Parent) const ChildMemo = React.memo(Child) const App = () => { return ( <ParentMemo children={<ChildMemo />} /> ) }
इस समस्या को हल करने के लिए, पास किए गए चाइल्ड को मेमोइज़ करना पर्याप्त है, यह सुनिश्चित करते हुए कि पैरेंट ऐप घटक के रेंडर के दौरान इसका संदर्भ स्थिर रहता है।
const App = () => { const child = useMemo(() => { return <ChildMemo /> }, []); return ( <ParentMemo> {child} </ParentMemo> ) }
एक और खतरनाक और अंतर्निहित क्षेत्र कस्टम हुक है। कस्टम हुक हमें हमारे घटकों से तर्क निकालने में मदद करते हैं, जिससे कोड अधिक पठनीय हो जाता है और जटिल तर्क छिप जाता है। हालाँकि, वे हमसे यह भी छिपाते हैं कि उनके डेटा और फ़ंक्शन में निरंतर संदर्भ हैं या नहीं। मेरे उदाहरण में, सबमिट फ़ंक्शन का कार्यान्वयन कस्टम हुक useForm में छिपा हुआ है, और पैरेंट घटक के प्रत्येक रेंडर पर, हुक को फिर से निष्पादित किया जाएगा।
const Parent = () => { const { submit } = useForm() return <ComponentMemo onChange={submit} /> };
क्या हम कोड से समझ सकते हैं कि मेमोइज्ड कंपोनेंट ComponentMemo में प्रॉप के रूप में सबमिट मेथड को पास करना सुरक्षित है या नहीं? बिल्कुल नहीं। और सबसे खराब स्थिति में, कस्टम हुक का कार्यान्वयन इस तरह दिख सकता है:
const Parent = () => { const { submit } = useForm() return <ComponentMemo onChange={submit} /> }; const useForm = () => { const submit = () => {} return { submit } }
मेमोइज्ड कॉम्पोनेंट में सबमिट मेथड पास करके, हम मेमोइजेशन को तोड़ देंगे क्योंकि पेरेंट कॉम्पोनेंट के हर रेंडर के साथ सबमिट मेथड का संदर्भ नया होगा। इस समस्या को हल करने के लिए, आप useCallback हुक का उपयोग कर सकते हैं। लेकिन मुख्य बिंदु जिस पर मैं जोर देना चाहता था वह यह है कि यदि आप अपने द्वारा लागू किए गए मेमोइजेशन को तोड़ना नहीं चाहते हैं तो आपको कस्टम हुक से डेटा को मेमोइज्ड कॉम्पोनेंट में पास करने के लिए आँख मूंदकर उपयोग नहीं करना चाहिए।
const Parent = () => { const { submit } = useForm() return <ComponentMemo onChange={submit} /> }; const useForm = () => { const submit = useCallback(() => {}, []) return { submit } }
किसी भी दृष्टिकोण की तरह, पूर्ण मेमोइज़ेशन का उपयोग सोच-समझकर किया जाना चाहिए, और अत्यधिक मेमोइज़ेशन से बचने का प्रयास करना चाहिए। आइए निम्नलिखित उदाहरण पर विचार करें:
export function App() { const [state, setState] = useState('') const handleChange = (e) => { setState(e.target.value) } return ( <Form> <Input value={state} onChange={handleChange}/> </Form> ) } export const Input = memo((props) => (<input {...props} />))
इस उदाहरण में, handleChange विधि को useCallback में लपेटना आकर्षक है क्योंकि handleChange को उसके वर्तमान स्वरूप में पास करने से Input घटक का मेमोइज़ेशन टूट जाएगा क्योंकि handleChange का संदर्भ हमेशा नया रहेगा। हालाँकि, जब स्थिति बदलती है, तो Input घटक को वैसे भी फिर से रेंडर किया जाएगा क्योंकि value prop में इसे एक नया मान पास किया जाएगा। इसलिए, यह तथ्य कि हमने handleChange को useCallback में लपेटा नहीं है, Input घटक को लगातार फिर से रेंडर करने से नहीं रोकेगा। इस मामले में, useCallback का उपयोग करना अत्यधिक होगा। इसके बाद, मैं कोड समीक्षा के दौरान देखे गए वास्तविक कोड के कुछ उदाहरण देना चाहूँगा।
const activeQuestionNumber = useMemo(() => { return activeQuestionIndex + 1 }, [activeQuestionIndex]) const userAnswerImage = useMemo(() => { return `/i/call/quiz/${quizQuestionAnswer.userAnswer}.png` }, [quizQuestionAnswer.userAnswer])
यह देखते हुए कि दो संख्याओं को जोड़ने या स्ट्रिंग को संयोजित करने का ऑपरेशन कितना सरल है, और इन उदाहरणों में हमें आउटपुट के रूप में प्राइमिटिव्स मिलते हैं, यह स्पष्ट है कि यहाँ useMemo का उपयोग करने का कोई मतलब नहीं है। यहाँ इसी तरह के उदाहरण दिए गए हैं।
const cta = useMemo(() => { return activeOverlayName === 'photos' ? 'gallery' : 'profile' }, [activeOverlayName]) const attendeeId = useMemo(() => { return userId === senderId ? recipientId : senderId }, [userId, recipientId, senderId])
useMemo में निर्भरता के आधार पर, अलग-अलग परिणाम प्राप्त किए जा सकते हैं, लेकिन फिर से, वे आदिम हैं और अंदर कोई जटिल गणना नहीं है। घटक के प्रत्येक रेंडर पर इनमें से किसी भी स्थिति को निष्पादित करना useMemo का उपयोग करने से सस्ता है।
हम आपके एप्लिकेशन कोड के प्रदर्शन को मापने के लिए इनमें से कम से कम 4 टूल की अनुशंसा कर सकते हैं।
रिएक्ट.प्रोफाइलर
साथ
रिएक्ट डेवलपर टूल्स
रिएक्ट रेंडर ट्रैकर
एक और दिलचस्प उपकरण है
स्टोरीबुक-एडऑन-परफॉर्मेंस ऐडऑन के साथ स्टोरीबुक।
इसके अलावा, स्टोरीबुक में, आप एक दिलचस्प प्लगइन स्थापित कर सकते हैं जिसका नाम है
** सोशल डिस्कवरी ग्रुप के वरिष्ठ सॉफ्टवेयर इंजीनियर सर्गेई लेवकोविच द्वारा लिखित