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на компоненті з даними, які все ж змінюються (наприклад, переклад після зміни мови), - застарілий інтерфейс.
Звичайний імпорт компонента потрапляє в головний бандл: навіть якщо важкий редактор чи графік відкриють раз на тиждень, його код завантажується з кожною сторінкою.
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 (фільтри, номер сторінки) - це надійніше й не тримає цілий компонент у пам'яті.
Без розділення весь застосунок - усі сторінки, адмінка, рідко потрібні розділи - потрапляє в один 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: ліниве завантаження маршрутів