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.
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 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) }, [])
React geliştirme ekiplerinde yaygın bir uygulama kapsamlı notlandırmadır. Bu yaklaşım genellikle şunları içerir:
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.
Bu gibi durumlarda bileşen yalnızca özellik değerleri gerçekten değiştiyse yeniden oluşturulur. Sahne dekorlarındaki ilkeller iyidir.
İ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
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} /> ) }
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} /> ) )
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> ) }
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 |
---|---|
| |
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> ) }
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 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.
Uygulama kodunuzun performansını ölçmek için bu araçlardan en az 4 tanesini önerebiliriz.
React.Profiler
İle
React Geliştirici Araçları
React Render Takipçisi
Bir başka ilginç araç ise
Hikaye kitabı eklentisi performans eklentisine sahip hikaye kitabı.
Ayrıca Storybook'ta, adında ilginç bir eklenti yükleyebilirsiniz.
** Social Discovery Group Kıdemli Yazılım Mühendisi Sergey Levkovich tarafından yazılmıştır.