Anophel-آنوفل چه زمانی نباید از هوک useMemo استفاده کرد

چه زمانی نباید از هوک useMemo استفاده کرد

انتشار:
2
0

علیرغم سودمندی هوک useMemo در بهینه سازی عملکرد React، من دیدم که برخی از توسعه دهندگان تمایل دارند از useMemo Hook بیش از حد استفاده کنند. برای کاوش عمیق تر در این موضوع، تصمیم گرفتم برخی از کاوش های کد را انجام دهم تا سناریوهایی را که استفاده از useMemo ممکن است مفید نباشد، تعیین کنم.

در این مقاله، سناریوهایی را بررسی خواهیم کرد که در آنها ممکن است بیش از حد از useMemo استفاده کنید.

Memo Hook چه کاربردی دارد؟

UseMemo Hook در React یک ابزار بهینه‌سازی عملکرد است که به شما امکان می‌دهد محاسبات زمان بر و پر هزینه را به خاطر بسپارید و از رندرهای غیرضروری اجتناب کنید. هنگامی که از useMemo استفاده می کنید، می توانید مقدار یک متغیر یا تابع را یک بار محاسبه کنید و از آن در چندین رندر استفاده مجدد کنید، نه اینکه هر بار که کامپوننت شما دوباره ارائه می شود، دوباره آن را محاسبه کنید.

این می تواند به طور قابل توجهی عملکرد برنامه شما را بهبود بخشد، به خصوص اگر محاسبات پیچیده یا زمان بری دارید که باید در کامپوننت های خود انجام دهید.

توجه به این نکته مهم است که فقط زمانی باید از useMemo استفاده کنید که محاسبات گران قیمتی دارید که باید به حافظه سپرده شوند. استفاده از آن برای هر مقدار در کامپوننت شما در واقع می تواند به عملکرد آسیب برساند، زیرا useMemo خود سربار کمی دارد.

بیایید به چند مثال و سناریو نگاه کنیم که در آنها باید در استفاده از useMemo Hook در یک برنامه React تجدید نظر کنید.

چه زمانی از useMemo استفاده نکنید

اگر عملیات شما ارزان است از useMemo استفاده نکنید.


کامپوننت مثال زیر را در نظر بگیرید:

/** 
  @param {number} page 
  @param {string} type 
**/
const myComponent({page, type}) { 
  const resolvedValue = useMemo(() => {
     return getResolvedValue(page, type)
  }, [page, type])

  return <ExpensiveComponent resolvedValue={resolvedValue}/> 
}

در این مثال، توجیه استفاده برنامه نویس از useMemo توسط خود آن آسان است. چیزی که در ذهن آنها می گذرد این است که آنها نمی خواهند وقتی مرجع ResoledValue تغییر می کند ExpensiveComponent دوباره رندر شود.

در حالی که این یک نگرانی معتبر است، دو سوال برای توجیه استفاده از useMemo در هر زمان وجود دارد.

اول، آیا تابعی که به useMemo منتقل می شود، گران است؟ در این مورد، آیا محاسبه getResolvedValue گران است؟

اکثر روش‌ها در انواع داده‌های جاوا اسکریپت بهینه شده‌اند، به عنوان مثال. Array.map()، Object.getOwnPropertyNames، و غیره. اگر عملیاتی را انجام می‌دهید که گران نیست (نشان‌گذاری Big O را در نظر بگیرید)، پس نیازی به حفظ مقدار بازگشتی ندارید. هزینه استفاده از useMemo ممکن است بیشتر از هزینه ارزیابی مجدد عملکرد باشد.

دوم، با توجه به مقادیر ورودی یکسان، آیا ارجاع به مقدار ذخیره شده تغییر می کند؟ به عنوان مثال، در بلوک کد بالا، با توجه به page بعنوان 2 و تایپ کردن "GET"، آیا ارجاع به solvedValue تغییر می کند؟

پاسخ ساده این است که نوع داده متغیر solvedValue را در نظر بگیرید. اگر solvedValue ابتدایی باشد (به عنوان مثال رشته، عدد، بولی، null، undefined یا symbol)، آنگاه مرجع هرگز تغییر نمی کند. به طور ضمنی، ExpensiveComponent دوباره ارائه نخواهد شد.

کد اصلاح شده زیر را در نظر بگیرید:

/** 
  @param {number} page 
  @param {string} type 
**/
const MyComponent({page, type}) {
  const resolvedValue = getResolvedValue(page, type)
  return <ExpensiveComponent resolvedValue={resolvedValue}/> 
}

طبق توضیحات بالا، اگر solvedValue یک رشته یا مقدار اولیه دیگر را برمی گرداند، و getResolvedValue یک عملیات گران قیمت نیست، این کد کاملاً صحیح و کارآمد است.

تا زمانی که صفحه و نوع یکسان هستند، یعنی هیچ تغییری در پروپوزال وجود ندارد، solvedValue همان مرجع را نگه می‌دارد به جز اینکه مقدار برگشتی یک چیز اولیه نباشد (مثلاً یک شی یا آرایه).

این دو سوال را به خاطر بسپارید: آیا تابعی که در حافظه ذخیره می شود، یک تابع سنگین و زمان بر است و آیا مقدار بازگشتی یک مقدار اولیه است؟ با این سوالات، همیشه می توانید استفاده خود از useMemo را ارزیابی کنید.

اگر در حال به خاطر سپردن یک شیء defaultState هستید، از useMemo استفاده نکنید

بلوک کد زیر را در نظر بگیرید:

/** 
  @param {number} page 
  @param {string} type 
**/
const myComponent({page, type}) { 
  const defaultState = useMemo(() => ({
    fetched: someOperationValue(),
    type: type
  }), [type])

  const [state, setState] = useState(defaultState);
  return <ExpensiveComponent /> 
}

کد بالا برای برخی بی ضرر به نظر می رسد، اما صدا زدن useMemo در آنجا غیر ضروری است.

اول از همه، هدف در اینجا این است که وقتی نوع prop تغییر می کند، یک شی defaultState جدید داشته باشیم، و هیچ ارجاعی به آبجکت defaultState در هر رندر مجدد باطل نشود.

در حالی که اینها نگرانی‌های مناسبی هستند، رویکرد اشتباه است و یک اصل اساسی را نقض می‌کند: useState در هر رندر مجدد، فقط زمانی که کامپوننت دوباره نصب شود، دوباره راه‌اندازی نمی‌شود.

آرگومان ارسال شده به useState بهتر است INITIAL_STATE نامیده شود. هنگامی که کامپوننت در ابتدا نصب شده باشد، فقط یک بار محاسبه می شود (یا فعال می شود):

useState(INITIAL_STATE)

اگرچه در بلوک کد بالا، زمانی که وابستگی آرایه نوع برای useMemo تغییر می کند، ما علاقه مند به دریافت یک مقدار defaultState جدید هستیم، اما این قضاوت اشتباهی است زیرا useState شی defaultState تازه محاسبه شده را نادیده می گیرد.

این برای مقداردهی اولیه useState به شکل زیر است:

/**
   @param {number} page 
   @param {string} type 
**/
const myComponent({page, type}) {
  // default state initializer 
  const defaultState = () => {
    console.log("default state computed")
    return {
       fetched: someOperationValue(),
       type: type
    }
  }

  const [state, setState] = useState(defaultState);
  return <ExpensiveComponent /> 
}

در مثال بالا، تابع init defaultState فقط یک بار فراخوانی می شود، در حالت mount. این تابع در هر رندر مجدد فراخوانی نمی شود. در نتیجه، گزارش «محاسبه حالت پیش‌فرض» فقط یک بار دیده می‌شود، مگر زمانی که کامپوننت دوباره نصب شود.

در اینجا کد قبلی بازنویسی شده است:

/**
   @param {number} page 
   @param {string} type 
**/
const myComponent({page, type}) {
  const defaultState = () => ({
     fetched: someOperationValue(),
     type,
   })

  const [state, setState] = useState(defaultState);

  // if you really need to update state based on prop change, 
  // do so here
  // pseudo code - if(previousProp !== prop){setState(newStateValue)}

  return <ExpensiveComponent /> 
}

ما اکنون سناریوهای ظریف تری را در نظر خواهیم گرفت که در آنها باید از useMemo اجتناب کنید.

استفاده از useMemo به عنوان راه فرار برای هشدارهای ESLint Hook

در حالی که نمی‌توانم خودم را به خواندن تمام نظرات افرادی که به دنبال راه‌هایی برای سرکوب هشدارها از پلاگین رسمی ESLint برای Hooks هستند، بخوانم، اما مشکل آنها را درک می‌کنم.

من با دن آبراموف در این مورد موافقم. سرکوب هشدارهای ESLint از افزونه احتمالاً روزی در آینده شما را گریبان گیر خواهد گرفت.

به طور کلی، من فکر می کنم که سرکوب این هشدارها در برنامه های تولیدی ایده بدی است زیرا احتمال معرفی باگ های ظریف در آینده نزدیک را افزایش می دهید.

با این اوصاف، هنوز موارد معتبری برای سرکوب این هشدارها وجود دارد. در زیر نمونه ای است که خودم با آن برخورد کرده ام. کد برای درک آسان تر ساده شده است:

function Example ({ impressionTracker, propA, propB, propC }) {
  useEffect(() => {
    // Track initial impression
    impressionTracker(propA, propB, propC)
  }, [])

  return <BeautifulComponent propA={propA} propB={propB} propC={propC} />                 
}

این یک مشکل است.

در این مورد خاص، برای شما مهم نیست که آیا پایه ها تغییر می کنند یا نه. شما فقط علاقه مند به فراخوانی تابع track با هر چیز اولیه هستید. ردیابی برداشت اینگونه است. فقط زمانی که کامپوننت نصب می شود، تابع track نمایش را فراخوانی می کنید. تفاوت در اینجا این است که شما باید تابع را با چند پایه اولیه فراخوانی کنید.

در حالی که ممکن است فکر کنید که صرفاً تغییر نام props به چیزی مانند initialProps مشکل را حل می کند، این کار نمی کند. این به این دلیل است که BeautifulComponent به دریافت مقادیر prop به روز شده نیز متکی است.

در این مثال، پیام هشدار lint را دریافت خواهید کرد: ReactuseEffect  Hook وابستگی‌های گمشده دارد: 'impressionTracker'، 'propA'، 'propB' و 'propC'. یا آنها را درج کنید یا آرایه وابستگی را حذف کنید.

این یک پیام نسبتاً عجولانه است، اما لینتر به سادگی کار خود را انجام می دهد. راه حل آسان استفاده از eslint-disable است، اما این همیشه بهترین راه حل نیست، زیرا می توانید در آینده باگ هایی را در همان صدا زدن useEffect معرفی کنید:

useEffect(() => {
  impressionTracker(propA, propB, propC)
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

راه حل پیشنهادی من این است که از useRef Hook برای به روز نگه داشتن ارجاع به مقادیر اولیه که نیازی به آنها ندارید استفاده کنید:

function Example({impressionTracker, propA, propB, propC}) {
  // keep reference to the initial values         
  const initialTrackingValues = useRef({
      tracker: impressionTracker, 
      params: {
        propA, 
        propB, 
        propC, 
    }
  })

  // track impression 
  useEffect(() => {
    const { tracker, params } = initialTrackingValues.current;
    tracker(params)
  }, []) // you get NO eslint warnings for tracker or params

  return <BeautifulComponent propA={propA} propB={propB} propC={propC} />   
}

در تمام تست ها، لینتر فقط برای چنین مواردی به useRef احترام می گذارد و هشدار آن غیرفعال می شود. با useRef، لینتر متوجه می شود که مقادیر ارجاع شده تغییر نمی کنند و بنابراین هیچ هشداری دریافت نمی کنید! حتی useMemo از این هشدارها جلوگیری نمی کند.

مثلا:

function Example({impressionTracker, propA, propB, propC}) {

  // useMemo to memoize the value i.e so it doesn't change
  const initialTrackingValues = useMemo({
    tracker: impressionTracker, 
    params: {
       propA, 
       propB, 
       propC, 
    }
  }, []) // 👈 you get a lint warning here

  // track impression 
  useEffect(() => {
    const { tracker, params} = initialTrackingValues
    tracker(params)
  }, [tracker, params]) // 👈 you must put these dependencies here

  return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}

در راه حل معیوب بالا، حتی اگر من مقادیر اولیه را با به خاطر سپردن مقادیر اولیه با useMemo دنبال می کنم، لینتر همچنان هشدار می دهد. در فراخوانی useEffect، ردیاب مقادیر حفظ شده و پارامترها همچنان باید به عنوان وابستگی آرایه وارد شوند.

من دیده ام که مردم از Memo به این شکل استفاده می کنند. این کد ضعیف است و باید از آن اجتناب کرد. همانطور که در راه حل اولیه نشان داده شده است از useRef Hook استفاده کنید.

در پایان، در اکثر موارد قانونی که من واقعاً می‌خواهم هشدارهای lint را خاموش کنم، userRef را متحد کاملی یافتم.

useMemo در مقابل userRef

اکثر مردم می گویند از useMemo برای محاسبات گران قیمت و برای حفظ برابری های ارجاعی استفاده کنید. با اولی موافقم ولی با دومی مخالفم. از useMemo Hook فقط برای برابری های ارجاعی استفاده نکنید. تنها یک دلیل برای انجام این کار وجود دارد، که بعداً در مورد آن صحبت خواهم کرد.

چرا استفاده از useMemo صرفاً برای برابری های ارجاعی چیز بدی است؟ آیا این چیزی نیست که دیگران موعظه می کنند؟

مثال ساختگی زیر را در نظر بگیرید:

function Bla() {
  const baz = useMemo(() => [1, 2, 3], [])
  return <Foo baz={baz} />
}

در کامپوننت Bla، مقدار baz به خاطر گران بودن ارزیابی آرایه [1،2،3] نیست، بلکه به این دلیل که ارجاع به متغیر baz در هر رندر مجدد تغییر می‌کند، به خاطر سپرده می‌شود.

در حالی که به نظر نمی رسد این مشکلی باشد، من معتقد نیستم useMemo هوک مناسبی برای استفاده در اینجا باشد.

اول، به وابستگی آرایه نگاه کنید:

useMemo(() => [1, 2, 3], [])

در اینجا یک آرایه خالی به useMemo Hook ارسال می شود. به طور ضمنی، مقدار [1،2،3] فقط یک بار محاسبه می شود، زمانی که کامپوننت سوار می شود.

بنابراین، ما دو چیز را می دانیم: مقدار ذخیره شده یک محاسبه گران قیمت نیست، و پس از نصب مجدد محاسبه نمی شود.

اگر در چنین موقعیتی قرار گرفتید، از شما می خواهم که در مورد استفاده از useMemo Hook تجدید نظر کنید. شما در حال به خاطر سپردن مقداری هستید که محاسبه گران قیمتی نیست و در هیچ نقطه ای از زمان دوباره محاسبه نمی شود. هیچ راهی وجود ندارد که این با تعریف اصطلاح "حافظه نویسی" مطابقت داشته باشد.

این یک استفاده وحشتناک از useMemo Hook است. از نظر معنایی اشتباه است و مسلماً از نظر تخصیص و عملکرد حافظه برای شما هزینه بیشتری دارد.

خب چکاری باید انجام بدی؟

اول، نویسنده کد دقیقاً چه کاری را در اینجا انجام می دهد؟ آنها سعی نمی کنند یک مقدار را به خاطر بسپارند. بلکه می‌خواهند مرجع یک مقدار را در رندرهای مجدد یکسان نگه دارند.

در این موارد از useRef Hook استفاده کنید.

به عنوان مثال، اگر استفاده از ویژگی فعلی را دوست ندارید، به سادگی تغییر شکل داده و مانند شکل زیر نام آن را تغییر دهید:

function Bla() {
  const { current: baz } = useRef([1, 2, 3])
  return <Foo baz={baz} />
}

مشکل حل شد.

در واقع، می‌توانید از useRef برای حفظ ارجاع به ارزیابی عملکرد گران‌قیمت استفاده کنید، تا زمانی که نیازی به محاسبه مجدد تابع در تغییر props نباشد.

useRef Hook مناسب برای چنین سناریوهایی است، نه useMemo Hook.

توانایی استفاده از useRef Hook برای تقلید از متغیرهای نمونه یکی از کم استفاده ترین قدرت های فوق العاده است که Hook ها از ما استفاده می کنند. userRef Hook می تواند بیشتر از حفظ ارجاعات به گره های DOM انجام دهد.

لطفاً به یاد داشته باشید، شرط اینجا این است که شما یک مقدار را فقط به این دلیل حفظ می کنید که باید یک مرجع ثابت به آن داشته باشید. اگر نیاز دارید که مقدار را بر اساس یک پایه یا مقدار در حال تغییر دوباره محاسبه کنید، لطفاً از useMemo Hook استفاده کنید. در برخی موارد، همچنان می‌توانید از useRef استفاده کنید، اما useMemo با توجه به لیست وابستگی آرایه‌ها بیشتر راحت است.

تفاوت بین هوک useMemo و useCallback چیست؟

نتیجه

در این مقاله، می‌بینیم که اگرچه useMemo Hook برای بهینه‌سازی عملکرد در برنامه‌های React مفید است، اما در واقع سناریوهایی وجود دارد که در آنها به هوک ها نیازی نیست. 

برخی از این سناریوها عبارتند از:

1.وقتی محاسبات گران نیست. اگر انجام یک محاسبات نسبتاً ارزان باشد، ممکن است استفاده از useMemo برای به خاطر سپردن آن ارزش نداشته باشد. به عنوان یک قاعده کلی، اگر یک محاسبه کمتر از چند میلی ثانیه طول بکشد، احتمالاً ارزش حفظ کردن را ندارد.
2.زمانی که محاسبات به قطعاتی بستگی دارد که مرتباً تغییر می کنند. اگر یک محاسبات به ابزارهایی بستگی دارد که مرتباً تغییر می کنند، ممکن است ارزش حفظ کردن را نداشته باشد
3.هنگامی که مقدار ذخیره شده اغلب استفاده نمی شود. اگر یک مقدار ذخیره شده فقط در یک یا دو مکان در کامپوننت شما استفاده می شود، ممکن است برای به خاطر سپردن آن ارزش هزینه سربار استفاده از useMemo را نداشته باشد. در این سناریو، ممکن است به جای حفظ یک نسخه حفظ شده از آن، صرفاً محاسبه مجدد مقدار در صورت نیاز کارآمدتر باشد.


به طور کلی، استفاده عاقلانه از useMemo و تنها زمانی مهم است که به احتمال زیاد یک مزیت عملکرد قابل اندازه گیری ارائه دهد. اگر مطمئن نیستید که از useMemo استفاده کنید یا خیر، ایده خوبی است که قبل از تصمیم گیری، برنامه خود را نمایه کنید و تأثیر عملکرد بهینه سازی های مختلف را اندازه گیری کنید.

#هوک_ها#react#useRef#useMemo
نظرات ارزشمند شما :
Loading...