paint-brush
React'te Notlandırma: Güçlü Araç mı, Yoksa Gizli Tuzak mı?ile@socialdiscoverygroup
690 okumalar
690 okumalar

React'te Notlandırma: Güçlü Araç mı, Yoksa Gizli Tuzak mı?

ile Social Discovery Group15m2024/07/01
Read on Terminal Reader

Çok uzun; Okumak

React uygulama geliştirmede yaygın bir yaklaşım her şeyin ezberle kapsanmasıdır. Social Discovery Group ekibi, React uygulamalarında notlandırmanın aşırı kullanımının performans sorunlarına nasıl yol açabileceğini keşfetti. Nerede başarısız olduğunu ve gelişiminizdeki bu gizli tuzaklardan nasıl kaçınacağınızı öğrenin.
featured image - React'te Notlandırma: Güçlü Araç mı, Yoksa Gizli Tuzak mı?
Social Discovery Group HackerNoon profile picture
0-item


React uygulama geliştirmede yaygın bir yaklaşım, her şeyi not alma ile kapsamaktır. Birçok geliştirici, gereksiz yeniden oluşturmaları önlemek için bileşenleri notlandırmaya sararak bu optimizasyon tekniğini özgürce uygular. Görünüşte performansı artırmaya yönelik kusursuz bir strateji gibi görünüyor. Ancak Social Discovery Group ekibi, notlandırmanın yanlış uygulandığında aslında beklenmedik şekillerde bozulabileceğini ve performans sorunlarına yol açabileceğini keşfetti. Bu makalede, not almanın aksayabileceği şaşırtıcı yerleri ve React uygulamalarınızda bu gizli tuzaklardan nasıl kaçınabileceğinizi inceleyeceğiz.


Memoizasyon Nedir?

Notlandırma, pahalı işlemlerin sonuçlarının kaydedilmesini ve aynı girdilerle tekrar karşılaşıldığında bunların yeniden kullanılmasını içeren bir programlama optimizasyon tekniğidir. Not almanın özü, aynı giriş verileri için gereksiz hesaplamalardan kaçınmaktır. Bu açıklama aslında geleneksel notlandırma için de geçerlidir. Kod örneğinde tüm hesaplamaların önbelleğe alındığını görebilirsiniz.


 const cache = { } function calculate (a) { if (Object.hasOwn(cache, a)) { return cache[a] } cache[a] = a * a return cache[a] }


Notlandırma, performans iyileştirmesi, kaynak tasarrufu ve sonuçların önbelleğe alınması gibi çeşitli avantajlar sunar. Ancak React'ın notlandırması yeni donanımlar gelene kadar çalışır, yani yalnızca son çağrının sonucu kaydedilir.


 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'ta Notlandırma Araçları

React kütüphanesi bize not alma için çeşitli araçlar sağlar. Bunlar HOC React.memo, useCallback, useMemo ve useEvent kancalarının yanı sıra React.PureComponent ve sınıf bileşenlerinin yaşam döngüsü yöntemi olan ShouldComponentUpdate'tir. İlk üç not alma aracını inceleyelim ve bunların React'teki amaçlarını ve kullanımlarını inceleyelim.


React.memo

Bu Yüksek Dereceli Bileşen (HOC), bir bileşeni ilk argümanı olarak ve isteğe bağlı bir karşılaştırma fonksiyonunu ikinci argümanı olarak kabul eder. Karşılaştırma işlevi, önceki ve mevcut donanımların manuel olarak karşılaştırılmasına olanak tanır. Hiçbir karşılaştırma işlevi sağlanmadığında, React varsayılan olarak sığ eşitliği kullanır. Sığ eşitliğin yalnızca yüzey düzeyinde bir karşılaştırma yaptığını anlamak çok önemlidir. Sonuç olarak, prop'lar sabit olmayan referanslara sahip referans türleri içeriyorsa React, bileşenin yeniden oluşturulmasını tetikleyecektir.


 const Button = memo((props) => { return ( <button onClick={props.onClick}> {props.title} </button> ) }, (prevProps, props) => { return props.title === prevProps.title })


React.useGeri arama

UseCallback kancası, ilk oluşturma sırasında iletilen bir işleve yapılan referansı korumamıza olanak tanır. Sonraki oluşturmalarda React, kancanın bağımlılık dizisindeki değerleri karşılaştıracak ve bağımlılıklardan hiçbiri değişmediyse, geçen seferkiyle aynı önbelleğe alınmış işlev referansını döndürecektir. Başka bir deyişle, useCallback, bağımlılıkları değişene kadar işlemeler arasında işlevin referansını önbelleğe alır.


 const callback = useCallback(() => { // do something }, [a, b, c])


React.useMemo

UseMemo kancası, işlemeler arasında bir hesaplamanın sonucunu önbelleğe almanıza olanak tanır. UseMemo genellikle pahalı hesaplamaları önbelleğe almak ve bir nesneyi HOC notuna sarılmış diğer bileşenlere veya kancalara bağımlılık olarak geçirirken ona yapılan bir referansı depolamak için kullanılır.


 const value = useMemo(() => { return [1, 2, 3, 4, 5].filter(it => it % 2 === 0) }, [])


Bir projenin tam olarak not edilmesi

React geliştirme ekiplerinde yaygın bir uygulama kapsamlı notlandırmadır. Bu yaklaşım genellikle şunları içerir:

  • React.memo'daki tüm bileşenleri sarmalama
  • Diğer bileşenlere aktarılan tüm işlevler için useCallback'in kullanılması
  • UseMemo ile hesaplamaları ve referans türlerini önbelleğe alma


Ancak geliştiriciler bu stratejinin inceliklerini ve notlandırmanın ne kadar kolay tehlikeye atılabileceğini her zaman kavrayamıyorlar. HOC notuna sarılmış bileşenlerin beklenmedik bir şekilde yeniden işlenmesi alışılmadık bir durum değildir. Social Discovery Group'ta meslektaşlarımızın şunu iddia ettiğini duyduk: "Her şeyi not etmek, hiç not almamaktan çok daha pahalı değil."


Herkesin not almanın çok önemli bir yönünü tam olarak kavramadığını fark ettik: yalnızca ilkel öğeler not alınan bileşene aktarıldığında ek ince ayarlar olmadan en iyi şekilde çalışır.


  1. Bu gibi durumlarda bileşen yalnızca özellik değerleri gerçekten değiştiyse yeniden oluşturulur. Sahne dekorlarındaki ilkeller iyidir.

  2. İkinci nokta ise referans tiplerini props'ta ilettiğimiz zamandır. React'ta sihir olmadığını hatırlamak ve anlamak önemlidir; React, JavaScript kurallarına göre çalışan bir JavaScript kütüphanesidir. Desteklerdeki referans türleri (işlevler, nesneler, diziler) tehlikelidir.


    Örneğin:


 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


Bir nesne oluşturduğunuzda, aynı özelliklere ve değerlere sahip başka bir nesne, farklı referanslara sahip olduğundan ilkine eşit değildir.


Aynı nesne gibi görünen bir şeyi bileşenin sonraki çağrısında iletirsek, ancak bu aslında farklı bir nesneyse (referansı farklı olduğundan), React'in kullandığı yüzeysel karşılaştırma bu nesneleri farklı olarak tanıyacaktır. Bu, nota sarılmış bileşenin yeniden oluşturulmasını tetikleyecek ve böylece o bileşenin hafızaya alınması bozulacaktır.


Notlandırılmış bileşenlerle güvenli çalışmayı sağlamak için not, useCallback ve useMemo kombinasyonunun kullanılması önemlidir. Bu şekilde tüm referans türlerinin sabit referansları olacaktır.

Ekip çalışması: not, useCallback, useMemo

Haydi notu keselim, olur mu?

Yukarıda anlatılanların hepsi kulağa mantıklı ve basit geliyor ancak gelin bu yaklaşımla çalışırken yapılabilecek en yaygın hatalara birlikte göz atalım. Bunlardan bazıları incelikli, bazıları ise biraz abartılı olabilir ancak tam not almayla uyguladığımız mantığın bozulmamasını sağlamak için bunların farkında olmamız ve en önemlisi onları anlamamız gerekiyor.


Notlandırmaya satır içi ekleme

Klasik bir hatayla başlayalım; Parent bileşeninin sonraki her oluşturmasında, params'da iletilen nesneye yapılan referans her zaman yeni olacağından, not alınan MemoComponent bileşeni sürekli olarak yeniden oluşturulacaktır.


 const Parent = () => { return ( <MemoComponent params={[1, 2 ,3]} /> ) }


Bu sorunu çözmek için daha önce bahsettiğimiz useMemo kancasını kullanmak yeterlidir. Artık nesnemize yapılan referans her zaman sabit olacaktır.


 const Parent = () => { const params = useMemo(() => { return [1, 2 ,3] ), []) return ( <MemoComponent params={params} /> ) }


Alternatif olarak, eğer dizi her zaman statik veriler içeriyorsa, bunu bileşenin dışındaki bir sabite taşıyabilirsiniz.


 const params = [1, 2 ,3] const Parent = () => { return ( <MemoComponent params={params} /> ) }


Bu örnekte de benzer bir durum, bir fonksiyonun hafızaya alınmadan geçirilmesidir. Bu durumda, önceki örnekte olduğu gibi, MemoComponent'in hafızaya alınması, Parent bileşeninin her oluşturulmasında yeni bir referansa sahip olacak bir fonksiyonun kendisine iletilmesiyle bozulacaktır. Sonuç olarak, MemoComponent sanki ezberlenmemiş gibi yeniden oluşturulacaktır.


 const Parent = () => { return ( <MemoComponent onClick={() => {}} /> ) }


Burada, Parent bileşeninin render'ları arasında iletilen fonksiyona yapılan referansı korumak için useCallback kancasını kullanabiliriz.


 const Parent = () => { const handleClick = useCallback(() => console.log('click') }, []) return ( <MemoComponent onClick={handleClick} /> ) }


Not alındı.

Ayrıca useCallback'te, başka bir işlev döndüren bir işlevi iletmenizi engelleyen hiçbir şey yoktur. Ancak, bu yaklaşımda her renderda `someFunction` fonksiyonunun çağrılacağını unutmamak önemlidir. 'someFunction' içinde karmaşık hesaplamalardan kaçınmak çok önemlidir.


 function someFunction() { // expensive calculations (?) ... return () => {} } .............................. const Parent = () => { const cachedFunction = useCallback(someFunction(), []) return ( <MemoComponent onClick={cachedFunction} /> ) }


Sahne yayılıyor

Bir sonraki yaygın durum, sahne malzemelerinin yayılmasıdır. Bir bileşenler zinciriniz olduğunu hayal edin. İnitialComponent'ten iletilen veri desteğinin bu zincirdeki bazı bileşenler için potansiyel olarak gereksiz olacak şekilde ne kadar uzağa gidebileceğini ne sıklıkla düşünüyorsunuz? Bu örnekte, bu pervane ChildMemo bileşenindeki notlandırmayı bozacaktır çünkü İlk Bileşenin her oluşturulmasında değeri her zaman değişecektir. Notlandırılmış bileşenlerin zincirinin uzun olabileceği gerçek bir projede, sürekli değişen değerlere sahip gereksiz desteklerin onlara iletilmesi nedeniyle tüm notlandırma bozulacaktır:


 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()} /> ) }


Kendinizi korumak için not edilen bileşene yalnızca gerekli değerlerin aktarıldığından emin olun. Bunun yerine:


 const Component = (props) => { return <ChildMemo {...props} /> }


Kullanın (yalnızca gerekli malzemeleri iletin):


 const Component = (props) => { return ( <ChildMemo firstProp={prop.firstProp} secondProp={props.secondProp} /> ) )


Memo ve çocuklar

Aşağıdaki örneği ele alalım. JSX'i çocuk olarak kabul eden bir bileşen yazmamız tanıdık bir durumdur.


 const ChildMemo = React.memo(Child) const Component = () => { return ( <ChildMemo> <div>Text</div> </ChildMemo> ) }


İlk bakışta zararsız gibi görünse de gerçekte öyle değil. Çocukken JSX'i not edilmiş bir bileşene aktardığımız koda daha yakından bakalım. Bu sözdizimi, bu "div"i "çocuklar" adlı bir destek olarak geçirmek için kullanılan sözdizimsel şekerden başka bir şey değildir.


Çocuklar, bir bileşene aktardığımız diğer herhangi bir destekten farklı değildir. Bizim durumumuzda, JSX'i geçiyoruz ve JSX de 'createElement' yöntemi için sözdizimsel bir şeker, dolayısıyla aslında 'div' tipinde normal bir nesneyi geçiyoruz. Ve burada, notlandırılmış bir bileşen için genel kural geçerlidir: Notlandırılmamış bir nesne sahne donanımına aktarılırsa, bu nesneye yapılan referans her defasında yeni olacağından, ebeveyni oluşturulduğunda bileşen yeniden oluşturulacaktır.



Böyle bir sorunun çözümü, notlandırılmamış bir nesnenin iletilmesiyle ilgili blokta birkaç özet önce tartışılmıştı. Yani burada aktarılan içerik useMemo kullanılarak not edilebilir ve bunu alt öğeler olarak ChildMemo'ya aktarmak bu bileşenin ezberlenmesini bozmaz.


 const Component = () => { const childrenContent = useMemo( () => <div>Text</div>, [], ) return ( <ChildMemo> {childrenContent} </ChildMemo> ) }


ParentMemo ve ChildMemo

Daha ilginç bir örneği ele alalım.


 const ParentMemo = React.memo(Parent) const ChildMemo = React.memo(Child) const App = () => { return ( <ParentMemo> <ChildMemo /> </ParentMemo> ) }


İlk bakışta zararsız görünüyor: Her ikisi de ezberlenmiş iki bileşenimiz var. Ancak bu örnekte ParentMemo, alt öğeleri ChildMemo ezberlenmediği için nota sarılmamış gibi davranacaktır. ChildMemo bileşeninin sonucu JSX olacaktır ve JSX, bir nesneyi döndüren React.createElement için yalnızca sözdizimsel bir şekerdir. Dolayısıyla, React.createElement yöntemi yürütüldükten sonra ParentMemo ve ChildMemo normal JavaScript nesneleri haline gelecek ve bu nesneler hiçbir şekilde not edilmeyecektir.

Ebeveyn Notu

ÇocukMemo

{`` type: {`` ...`` $$typeof: Symbol(react.memo),`` type: {`` name: "Parent"`` }`` },`` ...``}

{`` type: {`` ...`` $$typeof: Symbol(react.memo),`` type: {`` name: "Child"`` }`` },`` ...``}


Sonuç olarak, notlandırılmamış bir nesneyi sahne donanımına aktarırız, böylece Ana bileşenin ezberlenmesini bozarız.


 const ParentMemo = React.memo(Parent) const ChildMemo = React.memo(Child) const App = () => { return ( <ParentMemo children={<ChildMemo />} /> ) }


Bu sorunu çözmek için, aktarılan alt öğeyi not etmek ve ana Uygulama bileşeninin oluşturulması sırasında referansının sabit kalmasını sağlamak yeterlidir.


 const App = () => { const child = useMemo(() => { return <ChildMemo /> }, []); return ( <ParentMemo> {child} </ParentMemo> ) }


Özel kancalardan ilkel olmayanlar

Bir diğer tehlikeli ve örtülü alan ise özel kancalardır. Özel kancalar, bileşenlerimizden mantık çıkarmamıza yardımcı olarak kodu daha okunabilir hale getirir ve karmaşık mantığı gizler. Ancak verilerinin ve fonksiyonlarının sabit referanslara sahip olup olmadığını da bizden saklıyorlar. Örneğimde, gönderme işlevinin uygulanması özel kanca useForm'unda gizlidir ve Parent bileşeninin her oluşturulmasında kancalar yeniden yürütülecektir.


 const Parent = () => { const { submit } = useForm() return <ComponentMemo onChange={submit} /> };


Koddan, gönderim yöntemini, notlandırılmış ComponentMemo bileşenine destek olarak aktarmanın güvenli olup olmadığını anlayabilir miyiz? Tabii ki değil. Ve en kötü durumda, özel kancanın uygulanması şu şekilde görünebilir:


 const Parent = () => { const { submit } = useForm() return <ComponentMemo onChange={submit} /> }; const useForm = () => { const submit = () => {} return { submit } }


Gönderme yöntemini notlandırılmış bileşene geçirerek, gönderme yöntemine yapılan referans, Ana bileşenin her oluşturulmasında yeni olacağından notlandırmayı bozacağız. Bu sorunu çözmek için useCallback kancasını kullanabilirsiniz. Ancak vurgulamak istediğim asıl nokta, uyguladığınız notlandırmayı bozmak istemiyorsanız, özel kancalardan gelen verileri notlandırılmış bileşenlere aktarmak için körü körüne kullanmamanız gerektiğidir.


 const Parent = () => { const { submit } = useForm() return <ComponentMemo onChange={submit} /> }; const useForm = () => { const submit = useCallback(() => {}, []) return { submit } }


Her şeyi ezberleseniz bile, ezberlemek ne zaman aşırı olur?

Her yaklaşımda olduğu gibi, tam not alma da dikkatli bir şekilde kullanılmalı ve bariz bir şekilde aşırı not almaktan kaçınmak için çaba gösterilmelidir. Aşağıdaki örneği ele alalım:


 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} />))


Bu örnekte, HandleChange yöntemini useCallback'e sarmak cazip gelebilir çünkü HandleChange'in geçerli biçiminde geçirilmesi, HandleChange referansı her zaman yeni olacağından, Giriş bileşeninin hafızaya alınmasını bozacaktır. Ancak durum değiştiğinde, input bileşeni yine de yeniden oluşturulacaktır çünkü ona value prop'ta yeni bir değer iletilecektir. Dolayısıyla, HandleChange'i useCallback'e sarmamış olmamız, Giriş bileşeninin sürekli olarak yeniden oluşturulmasını engellemez. Bu durumda useCallback'in kullanılması aşırı olacaktır. Daha sonra kod incelemeleri sırasında görülen gerçek kodlardan birkaç örnek vermek istiyorum.


 const activeQuestionNumber = useMemo(() => { return activeQuestionIndex + 1 }, [activeQuestionIndex]) const userAnswerImage = useMemo(() => { return `/i/call/quiz/${quizQuestionAnswer.userAnswer}.png` }, [quizQuestionAnswer.userAnswer])


İki sayı ekleme veya dizeleri birleştirme işleminin ne kadar basit olduğu ve bu örneklerde çıktı olarak ilkel değerler aldığımız göz önüne alındığında, burada useMemo kullanmanın hiçbir anlam ifade etmediği açıktır. Benzer örnekler burada.


 const cta = useMemo(() => { return activeOverlayName === 'photos' ? 'gallery' : 'profile' }, [activeOverlayName]) const attendeeId = useMemo(() => { return userId === senderId ? recipientId : senderId }, [userId, recipientId, senderId])


UseMemo'daki bağımlılıklara bağlı olarak farklı sonuçlar elde edilebilir, ancak yine de bunlar ilkeldir ve içinde karmaşık hesaplamalar yoktur. Bu koşullardan herhangi birinin bileşenin her oluşturulmasında yürütülmesi, useMemo'yu kullanmaktan daha ucuzdur.


Sonuçlar

  1. Optimizasyon – her zaman faydalı değildir. *Performans optimizasyonları ücretsiz değildir ve bu optimizasyonların maliyeti, elde edeceğiniz faydalarla her zaman orantılı olmayabilir.
    *
  2. Optimizasyonların sonuçlarını ölçün. *Ölçüm yapmazsanız optimizasyonlarınızın herhangi bir gelişme sağlayıp sağlamadığını bilemezsiniz. Ve en önemlisi, ölçüm yapmadan işleri daha da kötüleştirip kötüleştirmediklerini bilemezsiniz.
    *
  3. Ezberlemenin etkinliğini ölçün. *Tam notlandırmanın kullanılıp kullanılmayacağı ancak sizin özel durumunuzda nasıl performans gösterdiği ölçülerek anlaşılabilir. Hesaplamaları önbelleğe alırken veya not alırken not alma ücretsiz değildir ve bu, uygulamanızın ilk kez ne kadar hızlı başlatılacağını ve kullanıcıların onu ne kadar hızlı kullanmaya başlayabileceğini etkileyebilir. Örneğin, bir düğmeye basıldığında sonucunun sunucuya gönderilmesi gereken bazı karmaşık hesaplamaları ezberlemek istiyorsanız, uygulamanız başlatıldığında bunu ezberlemeniz mi gerekiyor? Belki de hayır, çünkü kullanıcının o düğmeye asla basmama ihtimali vardır ve bu karmaşık hesaplamayı yürütmek tamamen gereksiz olabilir.
    *
  4. Önce düşünün, sonra ezberleyin. *Bir bileşene aktarılan donanımların not edilmesi, yalnızca 'not' içine sarılmışsa veya alınan donanımlar kancaların bağımlılıklarında kullanılıyorsa ve ayrıca bu donanımlar diğer notlandırılmış bileşenlere aktarılıyorsa anlamlıdır.
    *
  5. JavaScript'in temel ilkelerini hatırlayın. React veya başka bir kütüphane ve çerçeve ile çalışırken tüm bunların JavaScript'te uygulandığını ve bu dilin kurallarına göre çalıştığını unutmamak önemlidir.


Ölçüm için hangi araçlar kullanılabilir?

Uygulama kodunuzun performansını ölçmek için bu araçlardan en az 4 tanesini önerebiliriz.


React.Profiler

İle React.Profiler , ilk ve sonraki işlemelerin zamanı hakkında bilgi edinmek için ihtiyacınız olan belirli bileşeni veya uygulamanın tamamını sarabilirsiniz. Ayrıca metriğin tam olarak hangi aşamada alındığını da anlayabilirsiniz.


React Geliştirici Araçları

React Geliştirici Araçları bileşen hiyerarşisini incelemenize, durum ve desteklerdeki değişiklikleri izlemenize ve uygulamanın performansını analiz etmenize olanak tanıyan bir tarayıcı uzantısıdır.


React Render Takipçisi

Bir başka ilginç araç ise React Render Takipçisi , notlandırılmamış bileşenlerdeki donanımlar değişmediğinde veya benzerleriyle değiştirildiğinde potansiyel olarak gereksiz yeniden oluşturma işlemlerinin tespit edilmesine yardımcı olur.


Hikaye kitabı eklentisi performans eklentisine sahip hikaye kitabı.

Ayrıca Storybook'ta, adında ilginç bir eklenti yükleyebilirsiniz. hikaye kitabı-eklenti-performansı Atlassian'dan. Bu eklentiyle, ilk oluşturma, yeniden oluşturma ve sunucu tarafı oluşturma hızı hakkında bilgi edinmek için testler çalıştırabilirsiniz. Bu testler, birden fazla kopya için ve aynı anda birden fazla çalıştırma için çalıştırılabilir, böylece test hatalıkları en aza indirilir.



** Social Discovery Group Kıdemli Yazılım Mühendisi Sergey Levkovich tarafından yazılmıştır.