Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке code splitting?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Code splitting** - це поділ JavaScript-бандла на менші чанки (chunks), які браузер завантажує за потреби, а не всі одразу. Механізм: динамічний `import()` змушує бандлер створити окремий файл, який браузер запрошує тільки коли виконання доходить до цього місця. **Головне:** найпоширеніший підхід - поділ по роутах через `React.lazy`. У Next.js це працює автоматично для кожної сторінки.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Code splitting** - це поділ одного великого JavaScript-бандла на менші чанки, які браузер завантажує лише тоді, коли вони справді потрібні. ## Теорія ### TL;DR - Без code splitting браузер завантажує весь додаток перед тим, як показати будь-що - Динамічний `import()` - сигнал для бандлера: цей модуль іде в окремий файл - Поділ по роутах - найпоширеніша стратегія: один чанк на сторінку або роут - `webpackPrefetch` дозволяє браузеру завантажити чанк у фоні, до того як юзер туди перейде - Надмірний поділ теж шкодить: 50 маленьких запитів можуть бути повільнішими за один середній бандл ### Як браузер отримує код Коли Webpack або Vite збирає додаток, всі статичні `import` потрапляють в один вихідний файл. Щойно зустрічається динамічний `import()` - бандлер виносить той модуль в окремий chunk-файл. Браузер завантажить його тільки тоді, коли виконання дійде до цього місця в коді. ```javascript // Статичний імпорт - потрапляє в основний бандл import { format } from 'date-fns'; // Динамічний імпорт - створює окремий чанк const { Chart } = await import('./Chart.js'); ``` Другий рядок змушує бандлер згенерувати окремий файл, наприклад `chunk-abc123.js`. Браузер завантажить його тільки коли виконання дійде до `await import(...)`. ### Поділ по роутах React Router разом з `React.lazy` - стандартний підхід у React-додатках: ```javascript import { lazy, Suspense } from 'react'; import { Routes, Route } from 'react-router-dom'; const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings')); function App() { return ( <Suspense fallback={<div>Завантаження...</div>}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ); } ``` Кожна сторінка - окремий чанк. Юзер, який відкриває тільки `/dashboard`, ніколи не завантажить бандл сторінки налаштувань. ### Поділ на рівні компонентів Ділити можна не тільки на межах роутів. Важкі компоненти - редактори тексту, мапи, PDF-переглядачі - теж гарні кандидати: ```javascript const RichEditor = lazy(() => import('./RichEditor')); // бібліотека на 200kb function PostForm({ showEditor }) { return ( <div> {showEditor && ( <Suspense fallback={<p>Завантаження редактора...</p>}> <RichEditor /> </Suspense> )} </div> ); } ``` Редактор завантажується тільки коли `showEditor` стає `true`. Якщо більшість юзерів ніколи не натискає «написати пост», вони ніколи не платять за цей код. ### Що відбувається всередині бандлера Webpack знаходить виклики `import()` під час збирання і генерує два артефакти: файл чанку і невеликий runtime-сніпет, який знає як його завантажити. У production-збірці чанки отримують хеш у назві: `312.abc1d2.js`. Назву можна задати через magic comment: ```javascript const Chart = await import( /* webpackChunkName: "analytics-chart" */ './Chart' ); ``` Vite працює схоже, але використовує алгоритм чанкінгу від Rollup. Next.js ділить код автоматично на рівні файлів: кожна сторінка в `app/` або `pages/` стає окремим чанком без жодного налаштування. ### Prefetch і Preload Якщо завантажувати чанк тільки після кліку, юзер чекає на мережевий запит. Webpack підтримує два хінти: - `webpackPrefetch: true` - браузер завантажить чанк у фоні під час простою, до переходу - `webpackPreload: true` - браузер завантажить чанк паралельно з батьківським ```javascript // Завантажується у фоні коли браузер не зайнятий const Analytics = await import( /* webpackPrefetch: true */ './Analytics' ); ``` Prefetch - правильний вибір для більшості переходів між роутами. Preload - для чанків, які точно знадобляться разом з поточною сторінкою. ### Типові помилки **Занадто дрібний поділ.** 50 окремих чанків по 5kb - це 50 HTTP-запитів. Навіть з HTTP/2 multiplexing накладні витрати накопичуються. Практичне правило: все що менше 30-40kb, скоріше за все, не варте окремого чанку. **Suspense без error boundary.** Якщо чанк не завантажився (мережева помилка або деплой під час переходу між сторінками), React кидає помилку. Без error boundary весь додаток падає. ```jsx // Додаток впаде якщо чанк не завантажиться <Suspense fallback={<Loading />}> <LazyRoute /> </Suspense> // Так помилка обробляється нормально <ErrorBoundary fallback={<p>Не вдалося завантажити. Спробуй оновити сторінку.</p>}> <Suspense fallback={<Loading />}> <LazyRoute /> </Suspense> </ErrorBoundary> ``` **Дублювання спільних залежностей.** Якщо `ChartA` і `ChartB` обидва імпортують `lodash`, і обидва розбиті на окремі чанки - `lodash` потрапить у кожен з них. `SplitChunksPlugin` у Webpack вирішує це автоматично в більшості конфігурацій, але варто перевірити через аналіз бандла. **Поділ без аналізу бандла.** Спочатку подивись що в бандлі, потім вирішуй що ділити. `webpack-bundle-analyzer` або `rollup-plugin-visualizer` для Vite покажуть точну картину: де вага, де дублювання. ### Де зустрічається на практиці - **Next.js** - автоматичний поділ на рівні сторінок, нічого налаштовувати не потрібно - **Vite** - автоматичний поділ на динамічних імпортах; `manualChunks` для виділення vendor-коду - **React Router v6** - разом з `React.lazy` для поділу на рівні роутів - **Vue** - `defineAsyncComponent(() => import('./Component.vue'))` працює за тим самим принципом - **Angular** - lazy-loaded модулі через `loadChildren` у роутері ### Питання на співбесіді **Q:** Яка різниця між `React.lazy` і звичайним динамічним `import()`? **A:** `React.lazy` обгортає динамічний `import()` і інтегрується з Suspense - React сам керує станом завантаження. Звичайний `import()` повертає просто Promise, стан завантаження треба обробляти вручну. Ще один нюанс: `React.lazy` працює тільки з default exports. **Q:** Як Next.js обробляє code splitting порівняно зі звичайним React-додатком? **A:** Next.js ділить код автоматично на рівні файлів: кожна сторінка в `app/` або `pages/` - окремий чанк. У звичайному React-додатку треба додавати `React.lazy` вручну. Плюс Next.js рендерить HTML на сервері, тому юзер бачить контент ще до завантаження JS-чанку. **Q:** Що відбувається якщо юзер переходить на іншу сторінку поки чанк ще завантажується? **A:** React скасовує pending-стан попереднього Suspense і починає завантажувати новий чанк. Стара мережева загрузка продовжується у фоні, але її результат ігнорується після завершення. **Q:** Чи можна розділити код по ролях, щоб адміни і звичайні юзери отримували різні чанки? **A:** Так. Якщо юзер не адмін, виклик `import()` для адмін-роутів просто не виконується, і чанк ніколи не запрошується. Це поширений патерн для обмеження доступу до функцій по ролі. **Q:** Яку метрику code splitting покращує найбільше? **A:** Time to Interactive (TTI) і Largest Contentful Paint (LCP), якщо видалений код блокував рендеринг. Зменшення бандла відображається в Lighthouse-аудиті «Remove unused JavaScript». Помітний ефект на першому завантаженні з'являється коли основний бандл зменшується нижче приблизно 150-200kb після gzip. ## Приклади ### Базовий динамічний імпорт ```javascript // Без splitting: heavyLib завантажується при кожному відкритті сторінки // import { processData } from './heavyLib'; // З splitting: heavyLib завантажується тільки коли юзер натискає Експорт async function handleExport() { const { processData } = await import('./heavyLib'); const result = processData(userData); downloadFile(result); } document.getElementById('export-btn') .addEventListener('click', handleExport); ``` Бібліотека `processData` може важити 150kb. Без поділу кожен юзер платить цю ціну при завантаженні сторінки, навіть якщо ніколи не натисне «Експорт». ### Поділ роутів з обробкою помилок ```javascript import { lazy, Suspense } from 'react'; import { Routes, Route } from 'react-router-dom'; import ErrorBoundary from './ErrorBoundary'; const AdminPanel = lazy(() => import('./pages/AdminPanel')); const UserProfile = lazy(() => import('./pages/UserProfile')); function App() { return ( <ErrorBoundary fallback={<p>Сторінку не вдалося завантажити. Оновіть сторінку.</p>}> <Suspense fallback={<div className="spinner" />}> <Routes> <Route path="/admin" element={<AdminPanel />} /> <Route path="/profile" element={<UserProfile />} /> </Routes> </Suspense> </ErrorBoundary> ); } ``` `ErrorBoundary` перехоплює помилки завантаження чанків. Без нього деплой під час переходу юзера між сторінками призведе до краша всього додатку без жодного шляху до відновлення. ### Prefetch при наведенні ```javascript import { lazy } from 'react'; const HeavyDashboard = lazy(() => import('./HeavyDashboard')); function NavLink({ to, children }) { // Запускає завантаження чанку до кліку const prefetch = () => import('./HeavyDashboard'); return ( <a href={to} onMouseEnter={prefetch} // prefetch при наведенні миші onFocus={prefetch} // prefetch при фокусі з клавіатури > {children} </a> ); } ``` До того як юзер підніме палець з кнопки миші, чанк уже, скоріше за все, у кеші. Я бачив як цей підхід зменшував видимий час переходу з 800мс до менш ніж 100мс на повільному з'єднанні.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.