Skip to content

کارایی | Performance

بررسی اجمالی

Vue به گونه‌ای طراحی شده است که برای اکثر موارد استفاده معمول بدون نیاز به بهینه‌سازی‌های دستی عملکرد خوبی داشته باشد. با این حال، همیشه چالش‌هایی وجود دارد که برای آن‌ها نیاز به بهینه‌سازی‌های اضافی است. در این بخش، در مورد نکاتی که باید در زمینه performance در یک برنامه Vue به آن توجه شود، بحث خواهیم کرد.

ابتدا، دو جنبه اصلی performance وب را مرور می‌کنیم:

  • performance لود (load) صفحه: سرعت نمایش محتوای برنامه و تعامل‌پذیر شدن آن در بازدید اولیه. این معمولاً با معیارهای حیاتی وب مانند Largest Contentful Paint (LCP) و First Input Delay (FID) سنجیده می‌شود.

  • performance به‌روزرسانی: سرعت به‌روزرسانی برنامه در پاسخ به ورودی کاربر. به عنوان مثال، سرعت به‌روزرسانی یک لیست وقتی کاربر چیزی در یک جعبه جستجو تایپ می‌کند، یا سرعت تعویض صفحه وقتی کاربر یک لینک پیمایش در یک برنامه تک‌صفحه‌ای (SPA) کلیک می‌کند.

در حالی که ایده‌آل است که performance هر دوی این‌ها به حداکثر برسد، معماری‌های مختلف فرانت‌اند تا حدی تأثیر می‌گذارند که چقدر دستیابی به عملکرد مطلوب در این جنبه‌ها آسان است. علاوه بر این، نوع برنامه‌ای که در حال ساخت آن هستید به شدت تأثیر می‌گذارد که چه چیزی را در زمینه performance باید اولویت‌بندی کنید. بنابراین، اولین گام برای تضمین عملکرد بهینه انتخاب معماری مناسب برای نوع برنامه‌ای است که در حال ساخت آن هستید:

  • به Ways of Using Vue مراجعه کنید تا ببینید چگونه می‌توانید از Vue به روش‌های مختلف استفاده کنید.

  • Jason Miller انواع برنامه‌های وب و پیاده‌سازی / تحویل ایده‌آل آن‌ها را در Application Holotypes مورد بحث قرار می‌دهد.

گزینه‌های پروفایلینگ | Profiling Options

برای بهبود عملکرد، ابتدا باید بدانیم چگونه آن را اندازه بگیریم. تعداد زیادی ابزار عالی وجود دارد که می‌توانند در این زمینه کمک کنند:

برای پروفایلینگ عملکرد بارگذاری (load) در production:

برای پروفایلینگ عملکرد حین توسعه محلی (local development):

بهینه‌سازی‌ لود صفحه

بسیاری از جنبه‌های مستقل از فریم‌ورک برای بهینه‌سازی عملکرد بارگذاری صفحه وجود دارد - این راهنمای web.dev را برای جمع‌بندی جامع مرور کنید. در این‌جا، بیشتر روی تکنیک‌هایی که مختص Vue هستند تمرکز خواهیم کرد.

انتخاب معماری درست

اگر برایی کیس استفاده شما عملکرد بارگذاری صفحه مهم است، از ارسال آن به عنوان یک SPA خالص سمت کلاینت اجتناب کنید. شما می‌خواهید سرور شما مستقیماً HTML حاوی محتوای مورد نیاز کاربران را ارسال کند. رندرینگ سمت کلاینت از time-to-content کند رنج می‌برد. این می‌تواند با Server-Side Rendering (SSR) یا Static Site Generation (SSG) بهبود یابد. راهنمای SSR را برای انجام SSR با Vue مطالعه کنید. اگر برنامه شما نیازهای تعاملی غنی ندارد، می‌توانید از یک سرور بک‌اند سنتی نیز برای رندر HTML و بهبود آن با Vue در سمت کلاینت استفاده کنید.

اگر برنامه اصلی شما باید یک SPA باشد، اما صفحات بازاریابی (لندینگ، درباره، وبلاگ) دارد، آن‌ها را به طور جداگانه منتشر کنید! ایده‌آل‌تر است که صفحات بازاریابی شما به عنوان HTML ایستا با حداقل JS، با استفاده از SSG منتشر شوند.

Bundle Size و Tree-shaking

یکی از مؤثرترین راه‌ها برای بهبود عملکرد بارگذاری صفحه، ارسال بسته‌های JavaScript کوچک‌تر است. چند راه برای کاهش اندازه بسته هنگام استفاده از Vue وجود دارد:

  • اگر امکان دارد از یک build step استفاده کنید.

    • بسیاری از APIهای Vue بصورت "tree-shakable" هستند اگر توسط یک ابزار build مدرن بسته‌بندی شوند. به عنوان مثال، اگر از کامپوننت درونی <Transition> استفاده نکنید، در باندل نهایی که برای production ساخته می‌شود، گنجانده نخواهد شد. Tree-shaking همچنین می‌تواند سایر ماژول‌های استفاده نشده در کد منبع شما را حذف کند.

    • هنگام استفاده از یک build step، تمپلیت‌ها از پیش کامپایل می‌شوند، بنابراین نیازی به ارسال کامپایلر Vue به مرورگر نیست. این باعث صرفه جویی 14kb از حجم کد جاوااسکریپت می‌شود و از هزینه کامپایل در زمان اجرا جلوگیری می‌کند.

  • هنگام معرفی وابستگی‌های جدید (dependencies)، از نظر اندازه محتاط باشید! در برنامه‌های واقعی، باندل‌های پرحجم معمولاً نتیجه معرفی وابستگی‌های سنگین بدون توجه به آن است.

    • اگر برنامه دارای مرحله build است، وابستگی‌هایی را ترجیح دهید که فرمت‌های ماژول ESM ارائه می‌دهند و دوستدار tree-shaking هستند. به عنوان مثال، lodash-es را به lodash ترجیح دهید.

    • اندازه وابستگی و ارزش عملکردی که ارائه می‌دهد را بررسی کنید. توجه داشته باشید اگر وابستگی دوستدار tree-shaking باشد، افزایش اندازه واقعی بستگی به APIهایی دارد که واقعاً از آن وارد می‌کنید. ابزارهایی مانند bundlejs.com می‌توانند برای بررسی‌های سریع مورد استفاده قرار گیرند، اما اندازه‌گیری با تنظیمات build واقعی همیشه دقیق‌تر خواهد بود.

  • اگر عمدتاً از Vue برای پیشرفت تدریجی استفاده می‌کنید (مترجم: پیشرفت تدریجی (Progressive Enhancement) یک رویکرد در توسعه وب است که برای ایجاد تجربه کاربری بهتر و قابل دسترس تر در وبسایت‌ها و وب اپلیکیشن‌ها استفاده می‌شود.) و ترجیح می‌دهید از یک مرحله ساخت اجتناب کنید، استفاده از petite-vue (فقط 6kb) را در نظر بگیرید.

تقسیم کد | Code Splitting

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

بسته‌بندها مانند Rollup (که Vite بر اساس آن است) یا webpack می‌توانند با تشخیص سینتکس ایمپورت داینامیک ESM، به طور خودکار قطعات را ایجاد کنند:

js
// و وابستگی‌های آن به یک قطعه جداگانه تقسیم می‌شود lazy.js
// صدا زده شود بارگذاری می‌شود `loadLazy()` و فقط هنگامی که
function loadLazy() {
  return import('./lazy.js')
}

بارگذاری تنبلانه (lazy loading) بهتر است برای ویژگی‌هایی استفاده شود که بلافاصله پس از بارگذاری صفحه اولیه مورد نیاز نیستند. در برنامه‌های Vue، این می‌تواند در ترکیب با ویژگی Async Component برای ایجاد قطعات تقسیم شده برای درخت کامپوننت‌ها استفاده شود:

js
import { defineAsyncComponent } from 'vue'

// و وابستگی‌های آن ایجاد می‌شود Foo.vue یک قطعه جداگانه برای
// در صفحه رندر می‌شود async component فقط زمانی که
// درخواست می‌شود
const Foo = defineAsyncComponent(() => import('./Foo.vue'))

برای برنامه‌هایی که از Vue Router استفاده می‌کنند، بارگذاری تنبلانه برای کامپوننت‌های route به شدت توصیه می‌شود. Vue Router از پشتیبانی صریح برای بارگذاری تنبلانه، مجزا از defineAsyncComponent، برخوردار است. برای جزئیات بیشتر به Lazy Loading Routes مراجعه کنید.

بهینه‌سازی‌های به‌روزرسانی

Props Stability

در Vue، یک کامپوننت فرزند فقط زمانی به‌روزرسانی می‌شود که حداقل یکی از prop‌های دریافتی آن تغییر کرده باشد. مثال زیر را در نظر بگیرید:

template
<ListItem
  v-for="item in list"
  :id="item.id"
  :active-id="activeId" />

داخل کامپوننت <ListItem>، از prop‌های id و activeId خود برای تعیین اینکه آیا آیتم فعال فعلی است یا خیر استفاده می‌کند. در حالی که این کار می‌کند، مشکل این است که هر بار که activeId تغییر می‌کند، همه <ListItem>‌ها در لیست باید به‌روزرسانی شوند!

ایده‌آل این است که فقط آیتم‌هایی که وضعیت active آن‌ها تغییر کرده به‌روزرسانی شوند. می‌توانیم این کار را با انتقال محاسبه وضعیت active به والد و ساختن <ListItem> که به طور مستقیم یک active prop دریافت کند، انجام دهیم:

template
<ListItem
  v-for="item in list"
  :id="item.id"
  :active="item.id === activeId" />

حالا، برای اکثر کامپوننت‌ها، prop active هنگام تغییر activeId ثابت می‌ماند، بنابراین دیگر نیازی به به‌روزرسانی ندارند. به طور کلی، ایده این است که prop‌های ارسالی به کامپوننت‌های فرزند را تا حد امکان ثابت نگه داریم.

v-once

v-once یک دایرکتیو ساخته شده است که می‌تواند برای رندر محتوایی که به داده‌های رانتایم وابسته است اما هرگز نیاز به به‌روزرسانی ندارد، استفاده شود. کل زیردرختی که از آن استفاده می‌شود برای همه به‌روزرسانی‌های آینده رد خواهد شد. برای جزئیات بیشتر به مرجع API آن مراجعه کنید.

v-memo

v-memo یک دایرکتیو ساخته شده است که می‌تواند برای رد شرطی به‌روزرسانی زیردرخت‌های بزرگ یا لیست‌های v-for استفاده شود. برای جزئیات بیشتر به مرجع API آن مراجعه کنید.

Computed Stability

از نسخه 3.4 به بعد، یک Computed تنها زمانی اِفِکت خود را فراخوانی می‌کند که مقدار محاسبه‌شده آن نسبت به قبل تغییر کرده باشد. به عنوان مثال، isEven زیر تنها در صورتی افکت را فراخوانی می‌کند که مقدار برگردانده‌شده از true به false تغییر کند یا برعکس:

js
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)

watchEffect(() => console.log(isEven.value)) // true

// است true همچنان computed منجر به لاگ‌های جدید نمی‌شود زیرا مقدار
count.value = 2
count.value = 4

این از فراخوانی‌های غیرضروری کاسته می‌کند، اما متأسفانه اگر computed در هر محاسبه آبجکت جدیدی ایجاد کند کار نمی‌کند:

js
const computedObj = computed(() => {
  return {
    isEven: count.value % 2 === 0
  }
})

چون در هر بار یک آبجکت جدید ایجاد می‌شود، مقدار جدید از نظر فنی همیشه متفاوت از مقدار قدیمی است. حتی اگر خاصیت isEven یکسان بماند، مگر اینکه Vue مقایسه‌ای عمیق بین مقدار قدیم و جدید انجام دهد. چنین مقایسه‌ای ممکن است پرهزینه باشد و احتمالا ارزش آن را نداشته باشد.

به جای آن، می‌توانیم این را با مقایسه‌ی دستی مقدار جدید و قدیم و برگرداندن شرطی مقدار قدیمی اگر می‌دانیم هیچ تغییری نکرده است، بهینه کنیم:

js
const computedObj = computed((oldValue) => {
  const newValue = {
    isEven: count.value % 2 === 0
  }
  if (oldValue && oldValue.isEven === newValue.isEven) {
    return oldValue
  }
  return newValue
})

در Playground امتحان کنید

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

بهینه‌سازی‌های عمومی

نکات زیر هر دو عملکرد بارگذاری صفحه و به‌روزرسانی را تحت تأثیر قرار می‌دهند.

لیست‌های بزرگ را مجازی کنید

یکی از شایع‌ترین مشکلات عملکردی در همه برنامه‌های فرانت‌اند رندر لیست‌های بزرگ است. بدون توجه به عملکرد یک فریم‌ورک، رندر کردن یک لیست با هزاران آیتم کند خواهد بود به دلیل تعداد بالای نودهای DOM که مرورگر باید مدیریت کند.

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

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

کاهش هزینه‌ی بیش از حد واکنش‌پذیری برای ساختارهای بزرگِ غیرقابل تغییر

سیستم واکنش‌پذیری Vue به طور پیش‌فرض عمیق است. در حالی که این امر مدیریت وضعیت را قابل درک می‌کند، هنگامی که اندازه داده‌ها بزرگ است، مقداری هزینه اضافی ایجاد می‌کند، زیرا هر دسترسی به پراپرتی منجر به گیر افتادن در proxy می‌شود که ردیابی وابستگی را انجام می‌دهد. این معمولاً زمانی مشهود می‌شود که با آرایه‌های بزرگی از اشیاء عمیقاً تودرتو سر و کار داریم، جایی که یک رندر به دسترسی به 100،000+ پراپرتی نیاز دارد، بنابراین فقط باید موارد استفاده بسیار خاص را تحت تأثیر قرار دهد.

Vue یک راه فرار برای خارج شدن از واکنش‌پذیری عمیق با استفاده از shallowRef()‎ و shallowReactive()‎ ارائه می‌دهد. APIهای سطحی حالتی ایجاد می‌کنند که فقط در سطح ریشه واکنش‌پذیر است و اشیاء تودرتو رادست نخورده در معرض دید قرار می دهد. این دسترسی به پراپرتی‌های تودرتو را سریع نگه می‌دارد، با این معامله که اکنون باید همه اشیاء تودرتو را غیرقابل تغییر در نظر بگیریم و به‌روزرسانی‌ها فقط می‌توانند با جایگزین کردن state ریشه فراخوانی شوند:

js
const shallowArray = shallowRef([
  /* لیست بزرگی از اشیای عمیق */
])

// این به‌روزرسانی‌ها را فراخوانی نمی‌کند
shallowArray.value.push(newObject)
// این فراخوانی می‌کند
shallowArray.value = [...shallowArray.value, newObject]

// این به‌روزرسانی‌ها را فراخوانی نمی‌کند
shallowArray.value[0].foo = 1
// این فراخوانی می‌کند
shallowArray.value = [
  {
    ...shallowArray.value[0],
    foo: 1
  },
  ...shallowArray.value.slice(1)
]

از انتزاع‌های غیرضروری کامپوننت خودداری کنید

گاهی اوقات ممکن است کامپوننت‌های بدون رندر یا کامپوننت‌های مرتبه بالاتر (یعنی کامپوننت‌هایی که کامپوننت‌های دیگر را با prop‌های اضافی رندر می‌کنند) برای انتزاع بهتر یا سازماندهی بهتر کد ایجاد کنیم. در حالی که این کار اشکالی ندارد، توجه داشته باشید که نمونه‌های کامپوننت بسیار گران‌تر از نودهای DOM ساده هستند و ایجاد تعداد زیادی از آن‌ها به دلیل الگوهای انتزاعی هزینه‌ی عملکردی در بر خواهد داشت.

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

کارایی | Performance has loaded