یک ترفند ساده برای بهینه‌سازی re-render های react

ترجمه و تالیف : مهدی عقیقی
تاریخ انتشار : 02 اسفند 98
خواندن در 2 دقیقه
دسته بندی ها : react

من درحال کار کردن روی یک مقاله ساده مربوط به re-render های react بودم، که ناگهان به این الماس گران‌بها از react رسیدم، که فکر می‌کنم خیلی ازش خوش‌تان بیاید.

اگر شما همان المنتی که در render قبلی به react داده بودید را دوباره به react بدهید، دیگر زحمت re-render کردن آن‌را نمی‌کشید.

  • Kent C.Dodds

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

کمی پس از بهینه‌سازی بدون استفاده از React.memo من از رندر شدن در ۱۳.۴ میلی‌ثانیه به ۳.۶ میلی‌ثانیه رسیدم!

  • Brooks Lybrand

بیاید آن‌را با یک مثال ساده و متناقض تجربه کنیم و سپس در مورد استفاده‌ی کاربردی از آن صحبت کنیم.

// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-g9mt5

import React from 'react'
import ReactDOM from 'react-dom'

function Logger(props) {
  console.log(`${props.label} rendered`)
  return null // what is returned here is irrelevant...
}

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <button onClick={increment}>The count is {count}</button>
      <Logger label="counter" />
    </div>
  )
}

ReactDOM.render(<Counter />, document.getElementById('root'))

وقتی کد بالا run می‌شود، “counter renderd” در کنسول نوشته می‌شود. و هر بار هم که عدد اضافه شود، دوباره “counter rendered” در کنسول نوشته می‌شود. این اتفاق برای این می‌افتد که وقتی روی دکمه کلیک می‌شود، state تغییر می‌کند و با تغییر state ،react سعی می‌کند که المنت‌های جدید را بگیرد. و پس از گرفتن المنت‌های جدید آن‌ها را render کند و به DOM تحویل دهد.

این جا قسمتی است که جالب می‌شود؛ در نظر بگیرید که <Logger label="counter" /> هرگز در بین این re-render شدن‌ها تغییر نکند و ثابت باشد، بنا‌بر‌این می‌توانیم جدایش کنیم.

بیایید این کار را به یک روش امتحان کنیم. (من هرگز این روش رو پیشنهاد نمی‌کنم، کمی بایستید؛ جلوتر می‌توانید کاربردی‌ترین راه را پیدا کنید.)

// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-o9e9f

import React from 'react'
import ReactDOM from 'react-dom'

function Logger(props) {
  console.log(`${props.label} rendered`)
  return null // what is returned here is irrelevant...
}

function Counter(props) {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <button onClick={increment}>The count is {count}</button>
      {props.logger}
    </div>
  )
}

ReactDOM.render(
  <Counter logger={<Logger label="counter" />} />,
  document.getElementById('root'),
)

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

چه اتفاقی در حال رخ دادن است؟

چه چیزی باعث این اختلاف می‌شود؟ خب این مربوط به React می‌شود. چرا یک استراحت کوتاه نمی‌کنید و مقاله‌ی “JSX” چیست را نمی‌خوانید؟

وقتی React تابع counter را فراخوانی می‌کند، چیزی شبیه به این دریافت می‌کند.

// some things removed for clarity
const counterElement = {
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment, // this is the click handler function
          children: 'The count is 0',
        },
      },
      {
        type: Logger, // this is our logger component function
        props: {
          label: 'counter',
        },
      },
    ],
  },
}

این‌ها آبجکت‌های “UI Descriptor” می‌باشند و UI که React باید render کند را توضیح می‌دهند. وقتی روی دکمه کلیک می‌کنیم تغییرات زیر رخ می‌دهد.

const counterElement = {
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment,
          children: 'The count is 1',
        },
      },
      {
        type: Logger,
        props: {
          label: 'counter',
        },
      },
    ],
  },
}

چیزی که می‌توانیم بگوییم این است که، تنها چیز‌هایی که تغییر کرده‌اند onClick و children از آبجکت props در button هستند. در حالی‌که، تمام آبجکت بالا جدید می‌باشد! از زمان طلوع React شما با هر render، چیزی شبیه به این می‌ساختید. (خوشبختانه، حتی مرورگر‌های موبایل هم در انجام این کار سریع هستند و این قضیه هیچ وقت یک مشکل بزرگ نبوده است.)

شاید آسان‌تر باشد اگر دنبال بخش‌هایی از آبجکت پایین که تغییری نکرده است بگردیم. می‌توانید چیز‌هایی که تغییر نکرده را ببینید:

const counterElement = {
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment,
          children: 'The count is 1',
        },
      },
      {
        type: Logger,
        props: {
          label: 'counter',
        },
      },
    ],
  },
}

تمامی Typeها و پراپرتی label بدون تغییر هستند. در حالی که خود آبجکت props هر دفعه بعد از render تغییر می‌کند. حتی اگر پراپرتی‌های آن بدون تغییر باقی بمانند.

مشکل دقیقا همین‌جاست. چون آبجکت props کامپوننت Logger تغییر کرده است، React مجبور است تا دوباره تابع Logger را run کند تا مطمئن شود که JSX جدیدی نسبت به تغییرات props دریافت نمی‌کند.حالا؛ این در کنار این است که کارهای دیگر را باید دقیقا بعد از تغییر props انجام دهید. اما اگر می‌توانستیم کاری کنیم که جلوی تغییر props بعد از هر render را بگیریم، چه می‌شد؟

اگر props تغییر نکند، React خواهد فهمید که نیاز نیست تابع ما re-run شود و نیازی به تغییر JSX نیست.

این دقیقا همان کاری است که React برای انجام آن در این موقعیت، برنامه‌نویسی شده است و از همان اول طلوع React هم به همین منوال بوده است.

اما مشکل این است که React هر دفعه که ما یک المنت درست می‌کنیم یک props جدید می‌سازد، پس چگونه اطمینان حاصل کنیم که آبجکت props بعد از هر render تغییر نمی‌کند؟ خوشبختانه شما الان فهمیدید و می‌دانید که چرا در مثال دوم Logger؛rerender نمی‌شد. اگر المنت JSX را یک بار بسازیم و چند بار از آن استفاده کنیم، هر دفعه فقط از یک المنت استفاده می‌شود.

مثال دوم را می‌توانید در پایین ببینید : ( تا دوباره به بالا اسکرول نکنید )

// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-o9e9f

import React from 'react'
import ReactDOM from 'react-dom'

function Logger(props) {
  console.log(`${props.label} rendered`)
  return null // what is returned here is irrelevant...
}

function Counter(props) {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <button onClick={increment}>The count is {count}</button>
      {props.logger}
    </div>
  )
}

ReactDOM.render(
  <Counter logger={<Logger label="counter" />} />,
  document.getElementById('root'),
)

حالا بیایید چیز‌هایی که بین renderها تغییر نمی‌کند را پیدا کنیم:

const counterElement = {
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment,
          children: 'The count is 1',
        },
      },
      {
        type: Logger,
        props: {
          label: 'counter',
        },
      },
    ],
  },
}

چون المنت Logger کاملا بدون تغییر باقی می‌ماند (و همین‌طور props هم کاملا بدون تغییر است)، React به صورت اتوماتیک این بهینه‌سازی را انجام می‌دهد و هر دفعه Logger را rerender نمی‌کند چون نیازی به rerender شدن ندارد.

این کار دقیقا کاری است که React.memo انجام می‌دهد، اما به جای چک کردن هر پراپرتی به طور جداگانه، React آبجکت props را به طور جامع چک می‌کند.

خب این چه معنی می‌دهد؟

به طور خلاصه؛ اگر اشکالات پرفورمنس در برنامه‌ی خودتان دارید، راه زیر را تست کنید.

  1. کامپوننت سنگینی که احتمال rerender شدن آن کم است را از درون والدش بردارید.
  2. آن کامپوننت را به عنوان پراپرتی به والد خودش بدهید.

شما هم‌اکنون راهی پیدا کردید که بدون استفاده از React.memo یک سری از مشکلات پرفورمنسی خود را حل کنید.

دمو

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

سندباکس

منبع

گردآوری و تالیف مهدی عقیقی
آفلاین
user-avatar

برنامه‌نویس وب، عاشق جاوااسکریپت و ریکت و لاراول :)

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید