Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Продуктивність і SSR»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

Обидві директиви дозволяють пропустити роботу з оновлення частини шаблону.

v-once - відрендерити елемент чи компонент один раз і більше ніколи не оновлювати:

<footer v-once>
  <p>© {{ year }} {{ companyName }}</p>
  <LegalLinks />
</footer>

При наступних рендерах Vue пропускає всю цю гілку разом з нащадками. Підходить для статичного за змістом вмісту, що будується з даних лише раз.

v-memo - оновлювати лише тоді, коли змінилося одне зі значень у масиві залежностей:

<div v-for="item in list" :key="item.id" v-memo="[item.id === selectedId]">
  <p>ID: {{ item.id }} - вибрано: {{ item.id === selectedId }}</p>
  <ExpensiveRow :item="item" />
</div>

Коли змінюється selectedId, перерендерюються лише два рядки - той, що втратив вибір, і той, що його отримав. Решта тисячі рядків пропускається.

Коли це має сенс:

  • v-memo - великі списки (сотні й тисячі елементів), де зміна стосується небагатьох рядків, і вимірювання показали, що рендер списку гальмує;
  • v-once - великі статичні блоки, побудовані з даних, які не змінюються протягом життя компонента.

Пастки:

  • забута залежність: якщо в шаблоні під v-memo використано значення, якого немає в масиві, зміни цього значення не з'являться на екрані. Найчастіша помилка - баг виглядає як «реактивність зламалася»;
  • v-memo="[]" працює як v-once;
  • v-memo всередині v-for не працює на дочірніх елементах - тільки на тому ж елементі, що й v-for;
  • передчасна оптимізація: у більшості компонентів оновлення й так дешеві. Ці директиви - інструмент для виміряних проблем, а не для кожного шаблону;
  • v-once на компоненті з даними, які все ж змінюються (наприклад, переклад після зміни мови), - застарілий інтерфейс.

Докладніше в документації: Вбудовані директиви: v-memo

Звичайний імпорт компонента потрапляє в головний бандл: навіть якщо важкий редактор чи графік відкриють раз на тиждень, його код завантажується з кожною сторінкою.

defineAsyncComponent завантажує компонент при першому рендері:

import { defineAsyncComponent } from 'vue'

const ChartPanel = defineAsyncComponent(() => import('./ChartPanel.vue'))
<button @click="showChart = true">Показати графік</button>
<ChartPanel v-if="showChart" :data="sales" />

Динамічний import() змушує збирач винести компонент і його залежності (бібліотеку графіків) в окремий файл. Він завантажиться, лише коли showChart стане true.

Стани завантаження й помилки:

const ChartPanel = defineAsyncComponent({
  loader: () => import('./ChartPanel.vue'),
  loadingComponent: Spinner,
  delay: 200,           // показати спінер лише якщо завантаження довше 200 мс
  errorComponent: LoadError,
  timeout: 10000,       // вважати помилкою після 10 с
})

delay за замовчуванням 200 мс - щоб на швидкому з'єднанні спінер не «блимав».

Де застосовувати:

  • модальні вікна, редактори тексту, карти, графіки, PDF-переглядачі;
  • вкладки, які відкривають рідко;
  • компоненти для певних ролей (адмінські панелі всередині звичайної сторінки).

Сторінки маршрутизатора розділяють так само, але простіше - Vue Router приймає функцію з import() прямо в маршруті.

Що варто знати:

  • асинхронний компонент приймає ті самі props, слоти й події, що й звичайний - для батька різниці немає;
  • не дробити надто дрібно: десятки маленьких частин - десятки запитів. Виносять важке й рідко потрібне;
  • помилки після деплою: у давно відкритій вкладці стара частина може зникнути з сервера - errorComponent з пропозицією оновити сторінку краще за порожнє місце;
  • у SSR асинхронні компоненти можна ще й гідрувати ліниво (Vue 3.5+).

Докладніше в документації: Асинхронні компоненти

Коли компонент зникає з екрана (перемкнули вкладку, v-if став false, змінився маршрут), Vue знищує його екземпляр: стан, введені дані, позиція прокрутки губляться. При поверненні компонент створюється заново.

<KeepAlive> кешує екземпляри компонентів замість знищення:

<KeepAlive :include="['OrdersTab', 'ReportsTab']" :max="5">
  <component :is="currentTab" />
</KeepAlive>

Перемкнулися з вкладки «Замовлення» на «Звіти» й назад - фільтри, введений пошук і завантажені дані на місці.

Параметри:

  • include / exclude - які компоненти кешувати (за назвою компонента: у <script setup> - назва файлу чи defineOptions({ name }));
  • max - скільки екземплярів тримати; при переповненні витісняється найдавніше використаний.

З маршрутизатором:

<RouterView v-slot="{ Component }">
  <KeepAlive>
    <component :is="Component" />
  </KeepAlive>
</RouterView>

Хуки кешованого компонента: onActivated і onDeactivated - замість onMounted/onUnmounted, які при показі з кешу не викликаються.

Підводні камені:

  • застарілі дані: компонент з кешу показує дані з моменту, коли його сховали. Оновлювати їх треба в onActivated, інакше користувач бачить замовлення годинної давнини;
  • пам'ять: кожен кешований екземпляр тримає свій стан, DOM і дані. Без max і include кеш росте з кожною новою сторінкою - особливо з маршрутами виду /orders/:id, де кожен id - окремий запис кешу;
  • побічні ефекти продовжують працювати: таймери, підписки на WebSocket, обробники на window у прихованому компоненті живуть далі. Їх треба призупиняти в onDeactivated;
  • ключі маршрутів: кешування сторінки з параметром може показати дані попереднього id, якщо компонент перевикористовується - потрібен :key чи реакція на зміну параметра.

Коли не потрібен: якщо стан простіше зберегти в Pinia чи URL (фільтри, номер сторінки) - це надійніше й не тримає цілий компонент у пам'яті.

Докладніше в документації: KeepAlive

Без розділення весь застосунок - усі сторінки, адмінка, рідко потрібні розділи - потрапляє в один JavaScript-файл. Користувач, що відкрив головну, завантажує і код сторінки налаштувань, і графіки звітів.

Ліниві маршрути - компонент сторінки задається функцією з динамічним import():

const routes = [
  { path: '/', component: () => import('./pages/Home.vue') },
  { path: '/orders', component: () => import('./pages/Orders.vue') },
  { path: '/reports', component: () => import('./pages/Reports.vue') },
]

Збирач (Vite) виносить кожну сторінку разом з її унікальними залежностями в окремий файл. Він завантажується при першому переході на маршрут.

Не використовувати defineAsyncComponent для маршрутів - Vue Router сам уміє працювати з функцією, що повертає Promise; обгортка лише заважає.

Групування в одну частину - кілька пов'язаних сторінок разом (наприклад, розділ адмінки), щоб не робити три запити поспіль. У Vite це налаштовується через опції збирання (manualChunks / групування частин).

Що варто знати:

  • затримка першого переходу: при кліку на маршрут код ще треба завантажити. Допомагає попереднє завантаження (<link rel="modulepreload">, завантаження при наведенні на посилання) і показ індикатора завантаження;
  • помилки завантаження після деплою: якщо користувач тримав вкладку відкритою, а файли старої збірки видалили, перехід падає. router.onError - місце, щоб обробити це (наприклад, перезавантажити сторінку на новий URL);
  • головна сторінка - не робіть лінивою найчастішу точку входу, якщо це не дає реального виграшу: це лише додатковий запит;
  • спільні залежності (Vue, Pinia, бібліотека компонентів) збирач виносить в окремий спільний файл автоматично.

В Inertia-застосунках (Laravel + Vue) роль маршрутів виконують сторінки Inertia, і розділення налаштовується в resolve через import.meta.glob('./Pages/**/*.vue') - без { eager: true } кожна сторінка стає окремою частиною.

Як перевірити результат: rollup-plugin-visualizer показує, що потрапило в кожну частину і скільки важить.

Докладніше в документації: Vue Router: ліниве завантаження маршрутів