همانند هر ابزار یا روش برنامه نویسی دیگری، caching نقش مهمی در بهینه‌سازی برنامه‌های React ایفا می‌کند. مفهوم caching در React معمولاً با اصطلاح memoization خوانده می‌شود. این مفهوم برای بهبود عملکرد با کاهش تعداد دفعاتی که یک کامپوننت به دلیل تغییرات state یا prop رندر می‌شود، مورد استفاده قرار می‌گیرد.

React دو API برای caching ارائه می‌دهد: useMemo و useCallback .useCallback یک هوک است که یک تابع را به خاطر می‌سپارد، در حالی که useMemo هوکی است که یک مقدار را به خاطر می‌سپارد. این دو هوک اغلب همراه با Context API برای بهبود بیشتر کارایی استفاده می‌شوند.

در این مقاله قصد داریم تا موضوعات زیر را باهم بررسی کنیم:

داشتن آشنایی با مفاهیم مربوط به React و کامپوننت‌های stateful می‌تواند به درک بهتر مقاله کمک کند.

رفتار پیش‌فرض caching در React

به‌طور پیش‌فرض، React از تکنیکی به نام مقایسه سطحی(shallow comparison) برای تعیین اینکه آیا یک کامپوننت باید دوباره رندر شود یا خیر استفاده می‌کند. این موضوع به این معنی است که اگر props یا state یک کامپوننت تغییر نکرده باشد، React فرض می‌کند که خروجی کامپوننت نیز تغییر نکرده است و آن را دوباره رندر نمی‌کند.

با این که این رفتار caching پیش‌فرض به خودی خود بسیار مؤثر است، اما همیشه برای بهینه‌سازی کامپوننت‌های پیچیده‌ای که به مدیریت state پیشرفته نیاز دارند، کافی نیست.

بنابراین React به منظور دستیابی به کنترل بیشتر بر رفتار caching و رندر کامپوننت‌ها، هوک‌های useMemo و useCallback را ارائه می‌کند.

Caching در React با استفاده از هوک useMemo

استفاده از هوک useMemo زمانی مفید است که ما برای بازیابی یک مقدار نیاز به انجام یک محاسبات پرهزینه داریم و می‌خواهیم اطمینان حاصل کنیم که این محاسبات فقط در مواقع ضروری انجام می‌شود. با به خاطر سپردن مقدار با استفاده از هوک useMemo، می‌توانیم اطمینان حاصل کنیم که مقدار فقط زمانی که dependencyهای آن تغییر کند محاسبه می‌شود.

در کامپوننت React، ممکن است چندین ویژگی داشته باشیم که state ما را تشکیل می‌دهند. اگر stateای تغییر کند که ربطی به محاسبات پرهزینه ما ندارد، در این شرایط چرا باید دوباره آن را محاسبه کنیم؟

در ادامه یک بلاک کد داریم که پیاده‌سازی اولیه هوک useMemo را نشان می‌دهد:

react
import React, { useState, useMemo } from 'react';
function Example() {
const [txt, setTxt] = useState(“Some text”);
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const sum = useMemo(() => {
console.log('Computing sum...');
return a + b;
}, [a, b]);
return (
<div>
<p>Text: {txt}</p>
<p>a: {a}</p>
<p>b: {b}</p>
<p>sum: {sum}</p>
<button onClick={() => setTxt(“New Text!”)}>Set Text</button>
<button onClick={() => setA(a + 1)}>Increment a</button>
<button onClick={() => setB(b + 1)}>Increment b</button>
</div>
);
}

فرض کنید در کامپوننت مثال بالا، تابع sum()یک محاسبه پرهزینه را انجام می‌دهد. اگر state مربوط به txtبه‌روزرسانی شود، React کامپوننت ما را دوباره رندر می‌کند، اما چون مقدار بازگشتی sumرا به خاطر می‌سپاریم، این تابع در این زمان دوباره اجرا نمی‌شود.

تنها زمانی که تابع sum() اجرا می‌شود این است که state مربوط به aیا bتغییر کرده باشد. این موضوع یک پیشرفت عالی نسبت به رفتار پیش‌فرض است، چرا که این متد را در هر رندر مجدد اجرا می‌کند.

Caching در React با استفاده از هوک useCallback

استفاده از هوک useCallback زمانی مفید است که ما نیاز داریم یک تابع را به عنوان یک prop به یک کامپوننت child منتقل کنیم و می‌خواهیم اطمینان حاصل کنیم که reference تابع بی‌دلیل تغییر نمی‌کند. با به خاطر سپردن تابع با استفاده از هوک useCallback، می‌توانیم اطمینان حاصل کنیم که reference تابع تا زمانی که dependencyهای آن تغییر نکند، ثابت می‌ماند.

اکنون می‌خواهیم بدون عمیق‌تر شدن در مفهوم reference توابع جاوااسکریپت بررسی کنیم که چگونه می‌توانند بر روی رندر برنامه React ما تأثیر بگذارند.

همانطور که قبلاً اشاره کردیم این موضوع به این دلیل است که React یک مقایسه سطحی از مقادیر prop انجام می‌دهد تا مشخص کند آیا یک کامپوننت باید دوباره رندر شود یا خیر. بنابراین یک reference تابع جدید همیشه مقداری متفاوت از قبلی در نظر گرفته می‌شود.

به عبارت دیگر، عمل ساده تعریف مجدد یک تابع (حتی همان تابع موجود)، باعث تغییر reference شده و باعث می شود که کامپوننت childای که تابع را به عنوان یک prop دریافت می‌کند، دوباره رندر غیرضروری انجام دهد.

در ادامه یک بلاک کد داریم که پیاده‌سازی اولیه هوک useCallback را نشان می‌دهد:

react
import React, { useState, useCallback } from 'react';
function ChildComponent({ onClick }) {
console.log('ChildComponent is rendered');
return (
<button onClick={onClick}>Click me</button>
);
}
function Example() {
const [count, setCount] = useState(0);
const [txt, setTxt] = useState(“Some text…”);
const incrementCount = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, [setCount]);
return (
<div>
<p>Text: {txt}</p>
<p>Count: {count}</p>
<button onClick={setTxt}>Set Text</button>
<button onClick={setCount}>Increment</button>
<ChildComponent onClick={incrementCount} />
</div>
);
}

همانطور که در مثال بالا می‌بینید، ما متد incrementCountرا به جای متد setCountبه کامپوننت child می‌دهیم. این کار به این دلیل است که incrementCountبه حافظه سپرده می‌شود و وقتی متد setTxtرا اجرا می‌کنیم، باعث می‌شود که کامپوننت child به شکل غیرضروری دوباره رندر نشود.

تنها راهی که باعث می‌شود تا کامپوننت child ما در این مثال مجدداً رندر شود این است که متد setCountاجرا شود. زیرا ما آن را به عنوان یک پارامتر dependency به هوک useCallback ارسال می‌کنیم تا در حافظه سپرده شود.

جمع‌بندی

Caching یک تکنیک مهم برای بهینه‌سازی برنامه‌های React است. این تکنیک با کاهش رندرهای غیر ضروری، می‌تواند به بهبود عملکرد و کارایی برنامه ما کمک کند.

React با استفاده از یک DOM مجازی برای مقایسه تغییرات state و props، یک رفتار caching پیش‌فرض ارائه می‌کند، و فقط به‌روزرسانی کامپوننت‌ها پس از مقایسه سطحی تغییرات را منعکس می‌کند. این یک تکنیک بهینه‌سازی عالی است که در بسیاری از موارد کافی می‌باشد، اما گاهی اوقات کنترل دقیق‌تری مورد نظر است. هوک‌های useMemo و useCallback برای دستیابی به این کنترل دقیق ایجاد شده‌اند.

هوک useMemo برای به خاطر سپردن نتایج فراخوانی تابع استفاده می‌شود و زمانی مفید است که محاسبه تابع پر هزینه باشد و نتیجه نیز اغلب تغییر نمی‌کند.

هوک useCallback برای به خاطر سپردن reference واقعی یک تابع به جای مقدار بازگشتی استفاده می‌شود و زمانی مورد استفاده قرار می‌گیرد که تابع به‌عنوان prop به کامپوننت child ارسال می‌شود که ممکن است باعث رندرهای غیرضروری شود.