محاسبات سنگین سبک‌تر شده: یادداشت‌برداری واکنش


برای توسعه دهندگان بسیار مهم است که برنامه هایی بسازند که عملکرد خوبی داشته باشند. یک ثانیه تاخیر در زمان بارگذاری می تواند منجر به کاهش 26٪ در نرخ تبدیل شود. پژوهش توسط Akamai پیدا کرده است. React memoization کلید تجربه مشتری سریعتر است – با هزینه جزئی استفاده از حافظه بیشتر.

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

توسعه‌دهندگان React می‌توانند بسته به اینکه کدام بخش از برنامه‌های خود را می‌خواهند بهینه کنند، سه نوع قلاب حافظه‌سازی را روی کد خود اعمال کنند. بیایید حافظه‌گذاری، این نوع قلاب‌های React و زمان استفاده از آنها را بررسی کنیم.

یادداشت در React: نگاهی گسترده تر

یادداشت کردن یک تکنیک بهینه سازی قدیمی است که اغلب در سطح عملکرد در نرم افزار و سطح دستورالعمل در سخت افزار با آن مواجه می شود. در حالی که فراخوانی‌های عملکرد تکراری از حافظه‌سازی سود می‌برند، این ویژگی محدودیت‌های خود را دارد و نباید بیش از حد مورد استفاده قرار گیرد زیرا از حافظه برای ذخیره همه نتایج خود استفاده می‌کند. به این ترتیب، استفاده از یادداشت بر روی یک تابع ارزان که بارها با آرگومان های مختلف خوانده می شود، معکوس است. حافظه‌سازی در توابع با محاسبات گران قیمت به بهترین شکل استفاده می‌شود. همچنین، با توجه به ماهیت حافظه، ما فقط می توانیم آن را برای توابع خالص اعمال کنیم. توابع خالص کاملاً قطعی هستند و عوارض جانبی ندارند.

یک الگوریتم عمومی برای یادداشت

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

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

مجموعه پارامتر منحصر به فرد هر تابع a را تعریف می کند key در حافظه پنهان ما ما تابع را محاسبه کرده و نتیجه را ذخیره می کنیم (value) با آن key. وقتی یک تابع دارای چند پارامتر ورودی باشد، key با الحاق آرگومان های آن با یک خط تیره در میان ایجاد می شود. این روش ذخیره سازی ساده است و امکان ارجاع سریع به مقادیر ذخیره شده ما را فراهم می کند.

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

// Function memoize takes a single argument, func, a function we need to memoize.
// Our result is a memoized version of the same function.
function memoize(func) {

  // Initialize and empty cache object to hold future values
  const cache = {};

  // Return a function that allows any number of arguments
  return function (...args) {

    // Create a key by joining all the arguments
    const key = args.join(‘-’);

    // Check if cache exists for the key
    if (!cache[key]) {

      // Calculate the value by calling the expensive function if the key didn’t exist
      cache[key] = func.apply(this, args);
    }

    // Return the cached result
    return cache[key];
  };
}

// An example of how to use this memoize function:
const add = (a, b) => a + b;
const power = (a, b) => Math.pow(a, b); 
let memoizedAdd = memoize(add);
let memoizedPower = memoize(power);
memoizedAdd(a,b);
memoizedPower(a,b);

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

توابع ذخیره سازی در React

برنامه های React معمولا دارای یک رابط کاربری بسیار پاسخگو با رندر سریع هستند. با این حال، توسعه‌دهندگان ممکن است با رشد برنامه‌هایشان با نگرانی‌های مربوط به عملکرد مواجه شوند. همانطور که در مورد حافظه عمومی توابع، ممکن است از حافظه‌سازی در React برای بازپردازی سریع کامپوننت‌ها استفاده کنیم. سه توابع و قلاب ذخیره سازی React اصلی وجود دارد: memo، useCallback، و useMemo.

React.memo

وقتی می خواهیم یک جزء خالص را به خاطر بسپاریم، آن جزء را با آن می پیچیم memo. این تابع کامپوننت را بر اساس اجزای آن به حافظه می سپارد. یعنی React درخت DOM کامپوننت پیچیده شده را در حافظه ذخیره می کند. React این نتیجه ذخیره شده را به جای رندر کردن مجدد کامپوننت با همان props برمی گرداند.

ما باید به یاد داشته باشیم که مقایسه بین قطعات قبلی و فعلی کم عمق است، همانطور که در اینجا مشهود است واکنش نشان دهیدکد منبع s. اگر وابستگی‌های خارج از این ویژگی‌ها باید در نظر گرفته شوند، این مقایسه سطحی ممکن است به درستی باعث بازیابی نتایج به خاطر سپرده‌شده نشود. بهتر است استفاده شود memo در مواردی که به‌روزرسانی در مؤلفه والد باعث می‌شود مؤلفه‌های فرزند دوباره ارائه شوند.

واکنش نشان دهید memo بهتر است از طریق یک مثال درک شود. فرض کنید می‌خواهیم کاربران را بر اساس نام جستجو کنیم و فرض کنیم a داریم users آرایه ای حاوی 250 عنصر ابتدا باید هر کدام را رندر کنیم User در صفحه برنامه ما و آنها را بر اساس نامشان فیلتر کنید. سپس یک کامپوننت با ورودی متن ایجاد می کنیم تا متن فیلتر را دریافت کند. یک نکته مهم: ما ویژگی فیلتر نام را به طور کامل پیاده سازی نمی کنیم. به جای آن، مزایای حفظ کردن را برجسته می کنیم.

این رابط کاربری ما است (توجه داشته باشید: اطلاعات نام و آدرس استفاده شده در اینجا واقعی نیست):

تصویری از رابط کاربری در حال کار.  از بالا به پایین، یک را نشان می دهد

پیاده سازی ما شامل سه جزء اصلی است:

  • NameInput: تابعی که اطلاعات فیلتر را دریافت می کند
  • User: مؤلفه ای که جزئیات کاربر را ارائه می کند
  • App: جزء اصلی با همه منطق کلی ما

NameInput یک جزء عملکردی است که یک حالت ورودی می گیرد، nameو یک تابع به روز رسانی، handleNameChange. توجه: ما به طور مستقیم یادداشت را به این تابع اضافه نمی کنیم زیرا memo روی قطعات کار می کند. بعداً برای اعمال این روش بر روی یک تابع از یک رویکرد حافظه‌سازی متفاوت استفاده خواهیم کرد.

function NameInput({ name, handleNameChange }) {
  return (
    <input
      type="text"
      value={name}
      onChange={(e) => handleNameChange(e.target.value)}
    />
  );
}

User همچنین یک جزء کاربردی است. در اینجا، نام، آدرس و تصویر کاربر را نمایش می دهیم. همچنین هر بار که React کامپوننت را رندر می‌کند، یک رشته را به کنسول وارد می‌کنیم.

function User({ name, address }) {
  console.log("rendered User component");
  return (
    <div className="user">
      <div className="user-details">
        <h4>{name}</h4>
        <p>{address}</p>
      </div>
      <div>
        <img
          src={`
          alt="profile"
        />
      </div>
    </div>
  );
}
export default User;

برای سادگی، ما داده های کاربر خود را در یک فایل جاوا اسکریپت ذخیره می کنیم. ./data/users.js:

const data = [ 
  { 
    id: "6266930c559077b3c2c0d038", 
    name: "Angie Beard", 
    address: "255 Bridge Street, Buxton, Maryland, 689" 
  },
  // —-- 249 more entries —--
];
export default data;

اکنون حالت های خود را تنظیم می کنیم و این مؤلفه ها را از آن فراخوانی می کنیم App:

import { useState } from "react";
import NameInput from "./components/NameInput";
import User from "./components/User";
import users from "./data/users";
import "./styles.css";

function App() {
  const [name, setName] = useState("");
  const handleNameChange = (name) => setName(name);
  return (
    <div className="App">
      <NameInput name={name} handleNameChange={handleNameChange} />
      {users.map((user) => (
        <User name={user.name} address={user.address} key={user.id} />
      ))}
    </div>
  );
}
export default App;

ما همچنین یک سبک ساده برای برنامه خود اعمال کرده ایم، تعریف شده در styles.css. نمونه برنامه ما، تا این مرحله، زنده است و ممکن است در ما مشاهده شود جعبه شنی.

ما App کامپوننت یک حالت را برای ورودی ما مقداردهی می کند. هنگامی که این حالت به روز می شود، App کامپوننت با مقدار حالت جدید خود rerend می کند و از همه مؤلفه های فرزند می خواهد که دوباره رندر شوند. React، را دوباره رندر می کند NameInput جزء و همه 250 User اجزاء. اگر کنسول را تماشا کنیم، می‌توانیم ۲۵۰ خروجی را برای هر کاراکتر اضافه یا حذف شده از فیلد متنی خود ببینیم. این تعداد زیادی بازپرداخت غیر ضروری است. فیلد ورودی و حالت آن مستقل از User جزء فرزند رندر می شود و نباید این مقدار محاسبات را ایجاد کند.

واکنش نشان دهید memo می تواند از این رندر بیش از حد جلوگیری کند. تنها کاری که باید انجام دهیم این است که آن را وارد کنیم memo تابع و سپس بسته بندی ما User قبل از صادرات با آن جزء کنید User:

import { memo } from “react”;
 
function User({ name, address }) {
  // component logic contained here
}

export default memo(User);

بیایید برنامه خود را دوباره اجرا کنیم و کنسول را تماشا کنیم. تعداد رندرهای روی User مولفه اکنون صفر است هر جزء فقط یک بار رندر می شود. اگر این را روی یک نمودار رسم کنیم، به نظر می رسد:

یک نمودار خطی با تعداد رندرها در محور Y و تعداد اقدامات کاربر در محور X.  یک خط ثابت (بدون یادداشت) به صورت خطی در زاویه 45 درجه رشد می کند و ارتباط مستقیمی را بین کنش ها و رندرها نشان می دهد.  خط نقطه‌دار دیگر (با حافظه‌گذاری) نشان می‌دهد که تعداد رندرها بدون توجه به تعداد اقدامات کاربر ثابت است.
رندر در مقابل اقدامات با و بدون حافظه

علاوه بر این، ما می‌توانیم زمان رندر را بر حسب میلی‌ثانیه برای برنامه‌مان با و بدون استفاده مقایسه کنیم memo.

دو جدول زمانی رندر برای برنامه و رندرهای کودک نشان داده شده است: یکی بدون حافظه و دیگری با.  جدول زمانی بدون یادداشت برچسب گذاری شده است

این زمان‌ها به‌شدت متفاوت است و تنها با افزایش تعداد مؤلفه‌های فرزند، متفاوت می‌شوند.

React.useCallback

همانطور که اشاره کردیم، یادداشت کامپوننت مستلزم آن است که props یکسان بماند. توسعه React معمولاً از مراجع تابع جاوا اسکریپت استفاده می کند. این مراجع می توانند بین رندرهای مؤلفه تغییر کنند. هنگامی که یک تابع به عنوان یک پایه در جزء فرزند ما گنجانده می شود، تغییر مرجع عملکرد ما باعث می شود که حافظه ما شکسته شود. واکنش نشان دهید useCallback هوک تضمین می‌کند که پایه‌های عملکرد ما تغییر نمی‌کنند.

بهتر است از useCallback زمانی که ما نیاز به ارسال یک تابع فراخوانی به یک مؤلفه متوسط ​​تا گران قیمت داریم که می‌خواهیم از بازپرداخت اجتناب کنیم، قلاب کنید.

در ادامه مثال خود، یک تابع اضافه می کنیم تا زمانی که شخصی روی a کلیک کند User جزء فرزند، فیلد فیلتر نام آن مؤلفه را نمایش می دهد. برای رسیدن به این هدف، تابع را ارسال می کنیم handleNameChange به ما User جزء. جزء فرزند این تابع را در پاسخ به یک رویداد کلیک اجرا می کند.

بیایید به روز کنیم App.js با اضافه کردن handleNameChange به عنوان پشتوانه ای برای User جزء:

function App() {
  const [name, setName] = useState("");
  const handleNameChange = (name) => setName(name);

  return (
    <div className="App">
      <NameInput name={name} handleNameChange={handleNameChange} />
      {users.map((user) => (
        <User
          handleNameChange={handleNameChange}
          name={user.name}
          address={user.address}
          key={user.id}
        />
      ))}
    </div>
  );
}

بعد، ما به رویداد کلیک گوش می دهیم و فیلد فیلتر خود را به طور مناسب به روز می کنیم:

import React, { memo } from "react";

function Users({ name, address, handleNameChange }) {
  console.log("rendered `User` component");

  return (
    <div
      className="user"
      onClick={() => {
        handleNameChange(name);
      }}
    >
      {/* Rest of the component logic remains the same */}
    </div>
  );
}

export default memo(Users);

هنگامی که ما این کد را اجرا کنید، متوجه می شویم که حافظه ما دیگر کار نمی کند. هر بار که ورودی تغییر می‌کند، همه مؤلفه‌های فرزند دوباره رندر می‌شوند زیرا handleNameChange مرجع پروپوزال در حال تغییر است. اجازه دهید تابع را از طریق a عبور دهیم useCallback قلاب برای رفع حافظه کودک.

useCallback تابع ما را به عنوان آرگومان اول و لیست وابستگی را به عنوان آرگومان دوم خود می گیرد. این قلاب را نگه می دارد handleNameChange نمونه در حافظه ذخیره می شود و تنها زمانی که وابستگی ها تغییر می کند یک نمونه جدید ایجاد می کند. در مورد ما، ما هیچ وابستگی به عملکرد خود نداریم، و بنابراین مرجع تابع ما هرگز به روز نمی شود:

import { useCallback } from "react";

function App() {
  const handleNameChange = useCallback((name) => setName(name), []);
  // Rest of component logic here
}

اکنون حافظه ما دوباره کار می کند.

React.useMemo

در React، ما همچنین می‌توانیم از حافظه‌گذاری برای مدیریت عملیات و عملیات گران‌قیمت درون یک کامپوننت با استفاده از آن استفاده کنیم useMemo. وقتی این محاسبات را اجرا می کنیم، معمولاً روی مجموعه ای از متغیرها به نام وابستگی انجام می شوند. useMemo دو استدلال می گیرد:

  1. تابعی که یک مقدار را محاسبه و برمی گرداند
  2. آرایه وابستگی مورد نیاز برای محاسبه آن مقدار

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

در مثال ما، بیایید یک محاسبه گران قیمت را روی خود انجام دهیم users آرایه. قبل از نمایش هر یک از کاربران، یک هش روی آدرس هر کاربر محاسبه می‌کنیم:

import { useState, useCallback } from "react";
import NameInput from "./components/NameInput";
import User from "./components/User";
import users from "./data/users";
// We use “crypto-js/sha512” to simulate expensive computation
import sha512 from "crypto-js/sha512";

function App() {
  const [name, setName] = useState("");
  const handleNameChange = useCallback((name) => setName(name), []);

  const newUsers = users.map((user) => ({
    ...user,
    // An expensive computation
    address: sha512(user.address).toString()
  }));

  return (
    <div className="App">
      <NameInput name={name} handleNameChange={handleNameChange} />
      {newUsers.map((user) => (
        <User
          handleNameChange={handleNameChange}
          name={user.name}
          address={user.address}
          key={user.id}
        />
      ))}
    </div>
  );
}

export default App;

محاسبات گران قیمت ما برای newUsers اکنون در هر رندر اتفاق می افتد. هر کاراکتری که وارد فیلد فیلتر می شود باعث می شود که React این مقدار هش را دوباره محاسبه کند. را اضافه می کنیم useMemo قلاب برای دستیابی به حفظ کردن در اطراف این محاسبه.

تنها وابستگی ما به اصل خودمان است users آرایه. در مورد ما، users یک آرایه محلی است، و ما نیازی به ارسال آن نداریم زیرا React می داند که ثابت است:

import { useMemo } from "react";

function App() {
  const newUsers = useMemo(
    () =>
      users.map((user) => ({
        ...user,
        address: sha512(user.address).toString()
      })),
    []
  );
  
  // Rest of the component logic here
}

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


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

  • memo برای به خاطر سپردن یک جزء در حالی که از مقایسه سطحی ویژگی های آن استفاده می کنیم تا بدانیم آیا به رندر نیاز دارد یا خیر.
  • useCallback به ما اجازه می دهد تا یک تابع تماس را به مؤلفه ای که می خواهیم از رندر مجدد اجتناب کنیم، ارسال کنیم.
  • useMemo برای رسیدگی به عملیات گران قیمت در یک تابع و مجموعه ای از وابستگی ها.

آیا باید همه چیز را در React به خاطر بسپاریم؟

حفظ کردن رایگان نیست. هنگامی که حافظه را به یک برنامه اضافه می کنیم، سه هزینه اصلی را متحمل می شویم:

  • استفاده از حافظه افزایش می یابد زیرا React تمام اجزا و مقادیر ذخیره شده را در حافظه ذخیره می کند.
    • اگر چیزهای زیادی را به خاطر بسپاریم، ممکن است برنامه ما برای مدیریت استفاده از حافظه خود دچار مشکل شود.
    • memoسربار حافظه بسیار کم است زیرا React رندرهای قبلی را برای مقایسه با رندرهای بعدی ذخیره می کند. علاوه بر این، این مقایسه ها سطحی و در نتیجه ارزان هستند. برخی از شرکت ها مانند کوین بیس، هر جزء را به خاطر بسپارید زیرا این هزینه حداقل است.
  • وقتی React مقادیر قبلی را با مقادیر فعلی مقایسه می کند، سربار محاسبات افزایش می یابد.
    • این سربار معمولاً کمتر از کل هزینه رندرها یا محاسبات اضافی است. با این حال، اگر مقایسه‌های زیادی برای یک جزء کوچک وجود داشته باشد، ممکن است حافظه‌گذاری بیشتر از صرفه‌جویی هزینه داشته باشد.
  • پیچیدگی کد با دیگ ذخیره سازی اضافی کمی افزایش می یابد، که خوانایی کد را کاهش می دهد.
    • با این حال، بسیاری از توسعه دهندگان تجربه کاربر را در تصمیم گیری بین عملکرد و خوانایی مهم ترین اهمیت می دانند.

Memoization ابزار قدرتمندی است و ما باید این قلاب ها را فقط در مرحله بهینه سازی توسعه برنامه خود اضافه کنیم. حفظ کردن بی رویه یا بیش از حد ممکن است ارزش این هزینه را نداشته باشد. درک کامل حافظه‌گذاری و قلاب‌های React، بهترین عملکرد را برای برنامه وب بعدی شما تضمین می‌کند.


وبلاگ مهندسی Toptal از Tiberiu Lepadatu برای بررسی نمونه کدهای ارائه شده در این مقاله تشکر می کند.

ادامه مطلب در وبلاگ مهندسی تاپتال:



منبع

Matthew Newman

Matthew Newman Matthew has over 15 years of experience in database management and software development, with a strong focus on full-stack web applications. He specializes in Django and Vue.js with expertise deploying to both server and serverless environments on AWS. He also works with relational databases and large datasets
[ Back To Top ]