Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Асинхронні компоненти та затримка в Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Асинхронні компоненти** у Vue використовують `defineAsyncComponent`, щоб розбивати JS-бандл і завантажувати код компонента лише за потреби. `<Suspense>` огортає такі компоненти і показує слот `#fallback` поки їхні проміси не виконаються. ```vue <Suspense> <AsyncChart /> <!-- завантажується за потреби --> <template #fallback>Завантаження...</template> </Suspense> ``` **Ключове:** асинхронні компоненти зменшують бандл; Suspense керує UI під час очікування.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Асинхронні компоненти та Suspense** вирішують різні задачі, але добре працюють разом. `defineAsyncComponent` розбиває JS-бандл і завантажує код компонента лише тоді, коли він потрібен. `<Suspense>` керує тим, що бачить користувач під час цього завантаження. ## Теорія ### TL;DR - Асинхронні компоненти використовують динамічний `import()` для створення окремих JS-чанків, зменшуючи початковий бандл на 50-90% у великих застосунках - `<Suspense>` відстежує проміси від асинхронних компонентів та `await` у `<script setup>`, показує `#fallback` поки всі не виконаються - Головна різниця: async = розбивка коду (мережа); Suspense = координація завантаження (UI) - Використовуй async для компонентів понад 10KB, яких немає на першому екрані; додавай Suspense якщо очікування перевищує 500ms - Suspense чекає на *найповільніший* проміс. Часткового відображення всередині одного boundary немає ### Швидкий приклад ```vue <script setup> import { defineAsyncComponent } from 'vue' // Vite/webpack виносить Chart.vue в окремий JS-чанк const AsyncChart = defineAsyncComponent(() => import('./Chart.vue')) </script> <template> <Suspense> <template #default> <AsyncChart /> <!-- чанк завантажується при монтуванні --> </template> <template #fallback> <div>Завантаження графіка...</div> <!-- рендериться одразу --> </template> </Suspense> </template> ``` "Завантаження графіка..." з'являється відразу. Chart.vue завантажується у фоні та підміняється після готовності. ### Ключова різниця `defineAsyncComponent` про мережу. Він каже Vite або webpack покласти компонент в окремий JS-файл і завантажити його через динамічний `import()` при першому рендері. Suspense нічого не знає про розбивку коду. Він сканує дочірнє дерево в пошуку незавершених промісів, від асинхронних компонентів або від `await` у `<script setup>`, і тримає слот `#fallback` поки всі не виконаються. Один зменшує бандл. Другий не дає екрану залишатись пустим. ### defineAsyncComponent з параметрами Проста форма огортає один `import()`. Форма з параметрами дає контроль над станами завантаження та помилок: ```vue <script setup> import { defineAsyncComponent } from 'vue' const AsyncUsersTable = defineAsyncComponent({ loader: () => import('./UsersTable.vue'), loadingComponent: LoadingSpinner, // fallback для конкретного компонента errorComponent: ErrorDisplay, // показується якщо чанк не завантажився delay: 300, // показувати loadingComponent тільки після 300мс timeout: 8000 // показати errorComponent якщо не завершено за 8 секунд }) </script> ``` Параметр `delay` вирішує реальну UX-проблему. На швидкому з'єднанні чанк завантажується менш ніж за 300мс, тому користувачі спінер взагалі не бачать. Він з'являється тільки на повільних мережах. Без `delay` спінер блимає при кожному завантаженні сторінки, навіть у тих хто сидить на гарному інтернеті. Більшість команд дізнаються про це вже в продакшені. ### Як Suspense відстежує асинхронні компоненти Коли Suspense монтується, Vue реєструє незавершені проміси від кожного асинхронного нащадка: компонентів через `defineAsyncComponent` і будь-яких `await` у `<script setup>`. Слот fallback рендериться одразу. Як тільки всі зареєстровані проміси виконуються, Vue підміняє DOM актуальним контентом. Якщо компонент використовує `await` в setup без батьківського Suspense, Vue виводить попередження в режимі розробки. Це не просто шум. Компонент зависає в невирішеному стані і нічого не показує. ```vue <!-- UserProfile.vue: асинхронний через await в setup --> <script setup> const response = await fetch('/api/user/1') const user = await response.json() </script> <template> <div>{{ user.name }}</div> </template> ``` ```vue <!-- Parent.vue: Suspense перехоплює проміси від UserProfile --> <template> <Suspense> <UserProfile /> <template #fallback> <p>Завантаження користувача...</p> </template> </Suspense> </template> ``` ### Suspense та Vue Router Ліниве завантаження маршрутів це найпоширеніший продакшн-патерн. Кожен маршрут завантажується як окремий чанк, а Suspense не дає екрану залишатись пустим під час навігації: ```vue <template> <RouterView v-slot="{ Component }"> <Suspense> <template #default> <component :is="Component" /> </template> <template #fallback> <PageLoading /> </template> </Suspense> </RouterView> </template> ``` ### Обробка помилок Suspense сам по собі помилки не перехоплює. Два варіанти: `onErrorCaptured` в батьківському компоненті або `errorComponent` в `defineAsyncComponent`. ```vue <script setup> import { onErrorCaptured, ref } from 'vue' const error = ref(null) onErrorCaptured((err) => { error.value = err return false // зупинити поширення помилки далі }) </script> <template> <div v-if="error">{{ error.message }}</div> <Suspense v-else> <AsyncComponent /> <template #fallback><Loading /></template> </Suspense> </template> ``` Завжди визначай `errorComponent` в `defineAsyncComponent`. Деплойменти, що змінюють хеші в іменах чанків, призводять до 404 для користувачів зі старим закешованим HTML. Без стану помилки вони бачать порожню сторінку без жодного пояснення. ### Типові помилки **`v-if` напряму на `<Suspense>`** призводить до демонтування і перезапуску всіх відстежуваних промісів при кожній зміні умови: ```vue <!-- Неправильно: Suspense демонтується при false, перезапускає проміси при true --> <Suspense v-if="isReady"> <AsyncComponent /> </Suspense> <!-- Правильно: v-if ставимо на внутрішній контент --> <Suspense> <AsyncComponent v-if="isReady" /> <template #fallback><Loading /></template> </Suspense> ``` **Відсутність `timeout` на повільних мережах.** За замовчуванням таймауту немає взагалі. На 3G користувач може чекати нескінченно. Встанови `timeout` в `defineAsyncComponent`. **Огортання синхронних компонентів в Suspense.** Suspense завжди рендерить `#fallback` першим, навіть для миттєвих нащадків. Якщо в дереві немає асинхронного, ти додаєш накладні витрати без причини. **Відсутній `errorComponent`.** Невдале завантаження чанку дає тиху порожню сторінку. Завжди передбачай стан помилки з можливістю повторної спроби. **Каскадні вкладені Suspense без чіткого плану.** Внутрішні boundary вирішуються незалежно від зовнішніх. Нагромади забагато і користувач бачить послідовність станів завантаження, яка виглядає як баг. Один Suspense на маршрут, як правило, правильний рівень. ### Де використовується - Nuxt 3: `<NuxtPage>` автоматично огортає асинхронні лейаути в Suspense для island architecture - Vue Router застосунки: компоненти маршрутів як `() => import('./View.vue')` з патерном `RouterView` зі слотом - Великі адмінки: важкі таблиці з `delay: 300`, щоб уникнути блимання спінера на швидких з'єднаннях - Quasar Framework: `QPage` інтегрує Suspense для асинхронного контенту панелей ### Питання на співбесіді **Q:** Як Suspense обробляє кілька асинхронних нащадків? **A:** Він збирає всі незавершені проміси в чергу і тримає fallback поки кожен не виконається. Найшвидший компонент чекає на найповільніший. Часткового відображення всередині одного boundary немає. **Q:** Яка різниця між `loadingComponent` в `defineAsyncComponent` і слотом `#fallback` в Suspense? **A:** `loadingComponent` діє на рівні конкретного компонента і враховує `delay` та `timeout`. Слот `#fallback` покриває весь boundary і не має параметрів таймінгу. Використовуй `loadingComponent` коли кожен компонент потребує власної поведінки завантаження; `#fallback` коли потрібен один стан завантаження для групи. **Q:** Що станеться якщо асинхронний компонент кинув помилку після того як Suspense вже розрішився? **A:** Помилка спливає до найближчого `onErrorCaptured` або до `errorComponent` в `defineAsyncComponent`. Suspense не повертається в режим fallback після розрішення. **Q:** Як асинхронні компоненти працюють з SSR? **A:** Динамічні імпорти пропускаються на сервері. Компонент рендериться на клієнті, а Suspense показує fallback під час гідратації. **Q:** У Vue 3.4+: що відбувається коли async setup охоплює teleport boundary? **A:** Проміс setup підвішує teleport-джерело. Boundary Suspense має огортати компонент-відправник, а не teleport-ціль. Огортання цілі приховує підвішене дерево вузлів від boundary, і fallback ніколи не показується. ## Приклади ### Базовий: лінива модалка за дією користувача ```vue <script setup> import { defineAsyncComponent, ref } from 'vue' // Чанк HeavyModal завантажується тільки коли користувач відкриває модалку const AsyncModal = defineAsyncComponent({ loader: () => import('./HeavyModal.vue'), errorComponent: ErrorDisplay, timeout: 5000 }) const showModal = ref(false) </script> <template> <button @click="showModal = true">Відкрити налаштування</button> <Suspense> <template #default> <AsyncModal v-if="showModal" @close="showModal = false" /> </template> <template #fallback> <div v-if="showModal">Завантаження налаштувань...</div> </template> </Suspense> </template> ``` Чанк модалки не потрапляє в початковий бандл. Перший клік запускає завантаження. Suspense показує повідомлення під час отримання чанку, потім рендерить модалку. Ні порожнього екрану, ні зайвого початкового навантаження. ### Середній: панель дашборда з async-отриманням даних ```vue <!-- UserDashboard.vue: асинхронний через await в setup --> <script setup> const res = await fetch('/api/dashboard') const stats = await res.json() </script> <template> <div class="dashboard"> <h2>Замовлення: {{ stats.orders }}</h2> <p>Виручка: {{ stats.revenue }}</p> </div> </template> ``` ```vue <!-- App.vue: Suspense перехоплює async setup від UserDashboard --> <script setup> import { onErrorCaptured, ref } from 'vue' const error = ref(null) onErrorCaptured((err) => { error.value = err; return false }) </script> <template> <div v-if="error">Помилка завантаження: {{ error.message }}</div> <Suspense v-else> <template #default> <UserDashboard /> </template> <template #fallback> <DashboardSkeleton /> </template> </Suspense> </template> ``` `<DashboardSkeleton />` показується поки API-запит виконується. Якщо запит впав, `onErrorCaptured` перехоплює і показує повідомлення про помилку. Користувач завжди бачить зворотній зв'язок. ### Просунутий: кілька асинхронних нащадків паралельно ```vue <script setup> import { defineAsyncComponent } from 'vue' // Три окремих чанки, всі завантажуються паралельно const AsyncChart = defineAsyncComponent(() => import('./RevenueChart.vue')) const AsyncTable = defineAsyncComponent(() => import('./OrdersTable.vue')) const AsyncMap = defineAsyncComponent(() => import('./DeliveryMap.vue')) </script> <template> <Suspense> <template #default> <!-- Всі три паралельно; fallback тримається до завершення найповільнішого --> <AsyncChart /> <AsyncTable /> <AsyncMap /> </template> <template #fallback> <div>Завантаження панелей дашборда...</div> </template> </Suspense> </template> ``` Всі три чанки отримуються паралельно. Єдиний Suspense boundary чекає на всі три перед підміною fallback. Якщо потрібно щоб кожна панель з'являлась одразу по готовності, огорни кожну в окремий `<Suspense>`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.