Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Стратегії завантаження ресурсів - preload, prefetch, modulepreload». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Preload, prefetch і modulepreload** — це `<link rel>` підказки, що керують пріоритетом і часом завантаження ресурсів. Preload запускається з високим пріоритетом одразу при виявленні (LCP-зображення, шрифти, критичний CSS). Prefetch ставить ресурси в чергу з найнижчим пріоритетом під час простою (навігація на наступну сторінку). Modulepreload завантажує ES-модулі та весь граф імпортів заздалегідь, без виконання коду. ```html <link rel="preload" href="hero.jpg" as="image" fetchpriority="high"> <link rel="prefetch" href="/next-page.js"> <link rel="modulepreload" href="/app.mjs"> ``` **Головне:** завжди додавай `as` до preload і `crossorigin` до preload шрифтів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Стратегії завантаження ресурсів** — це `<link rel>` підказки (resource hints), які повідомляють браузеру, коли і як завантажувати ресурси до того, як вони реально знадобляться сторінці. Preload забирає критичні ресурси з високим пріоритетом у момент виявлення підказки. Prefetch ставить ресурси майбутньої сторінки в чергу з найнижчим пріоритетом. Modulepreload завчасно завантажує граф ES-модулів без виконання жодного коду. ## Теорія ### TL;DR - Аналогія: preload = шеф-кухар бере інгредієнти, потрібні прямо зараз (високий пріоритет). prefetch = поповнення комори для завтрашнього рецепта (фон, тільки під час простою). modulepreload = підготовка модульних інгредієнтів без початку готування. - Різниця в пріоритеті: preload запускається з тим самим пріоритетом, що й CSS, вставлений парсером. prefetch отримує найнижчий слот і виконується лише коли черга вільна. modulepreload має високий пріоритет, але тільки для ES-модулів. - Правило вибору: зображення LCP або критичний шрифт → preload. Ресурс наступної сторінки → prefetch. Точка входу ES-модуля та весь граф імпортів → modulepreload. - Важливо: `preload` без атрибута `as` Chrome ігнорує з повідомленням "Did not assign resource type". Preload шрифту без `crossorigin` дає промах кешу при кожному завантаженні. ### Швидкий приклад ```html <head> <!-- Високий пріоритет, спрацьовує при виявленні, як і CSS --> <link rel="preload" href="/hero.avif" as="image" fetchpriority="high"> <!-- Шрифт потребує crossorigin, інакше браузер завантажить його двічі --> <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> <!-- Найнижчий пріоритет, виконується у фоні під час простою --> <link rel="prefetch" href="/checkout.js"> <!-- Завантажує app.mjs та всі його імпорти паралельно, без виконання --> <link rel="modulepreload" href="/app.mjs"> </head> <body> <img src="/hero.avif" alt="Hero"> <!-- Вже в кеші, без мерехтіння --> <script type="module" src="/app.mjs"></script> <!-- Залежності готові --> </body> ``` Зображення-герой відображається менш ніж за 100ms. Шрифт завантажується без спалаху невидимого тексту (FOIT). Імпорти модулів беруться з кешу замість послідовного ланцюжка мережевих запитів. ### Ключова різниця Preload — це високопріоритетний запит, який спрацьовує при парсингу `<head>`, ще до того, як браузер знайшов би ресурс у звичайному потоці. Prefetch — спекуляція під час простою: браузер завантажує ресурс лише тоді, коли немає нічого важливішого, і зберігає його для майбутньої навігації. Modulepreload орієнтований саме на ES-модулі. Він завантажує вхідний модуль, рекурсивно знаходить граф `import`-залежностей і завантажує все паралельно, але V8 не парсить і не виконує жодного з модулів на цьому етапі. Ти отримуєш повну мережеву перевагу без навантаження на основний потік до моменту, коли реально виконається тег `<script type="module">`. ### Коли використовувати - Зображення LCP або відео вище лінії згину → preload з `fetchpriority="high"` - Веб-шрифт, що використовується в критичному CSS → preload з `as="font"` і `crossorigin` - JavaScript, який блокує перший рендер → preload з `as="script"` - CSS вище лінії згину → preload з `as="style"` - Ресурс на сторінці, куди користувач, найімовірніше, перейде → prefetch - Точки входу ES-модулів та весь граф імпортів → modulepreload - Сторонні скрипти, потрібні, але не термінові → prefetch - Не використовуй більше 4-5 preload на сторінці: вони конкурують між собою за пропускну здатність. ### Таблиця порівняння | | preload | prefetch | modulepreload | |---|---|---|---| | **Пріоритет** | Високий (як CSS) | Найнижчий (тільки простій) | Високий (тільки модулі) | | **Коли спрацьовує** | Одразу при виявленні | Коли браузер вільний | До тега `<script>` | | **Блокує рендер** | Ні | Ні | Ні | | **Що завантажує** | Будь-який тип (потрібен `as`) | Будь-який тип | JS-модулі + граф імпортів | | **Виконує код** | Ні | Ні | Ні | | **Основний кейс** | Зображення LCP, шрифт | Навігація на наступну сторінку | SPA та ES-модульні застосунки | | **Потрібен `crossorigin`** | Для шрифтів — так | Ні | Залежить від CORS-політики | | **Коли використовувати** | Критичний шлях зараз | Спекулятивне майбутнє | Сучасні JS-застосунки | ### Як браузер обробляє це зсередини Коли браузер натрапляє на `<link rel="preload">` під час парсингу `<head>`, Chromium ставить запит у чергу `MEDIUM` або `HIGH` у `LoadingScheduler::Priority` — той самий рівень, що й таблиці стилів, вставлені парсером. Prefetch отримує рівень `LOWEST` і виконується тільки після того, як вся робота з вищим пріоритетом завершена. Modulepreload (Chrome 66+, всі основні браузери з 2023 року) використовує внутрішній ModuleGraph браузера. Він запускає `fetch()` для вхідного модуля, парсить `import`-вирази, знаходить залежності та завантажує все паралельно. V8 пропускає етапи парсингу й виконання. Це усуває класичний каскад модулів: без modulepreload граф з 3 рівнями глибини потребує 3 послідовних мережевих round trip-и до початку виконання коду. З ним все завантажується в один паралельний пакет. Safari 16.4 і раніше підтримував лише частину специфікації без рекурсивного розв'язання залежностей. Повна підтримка з'явилася в Safari 17.0. ### preload на практиці Атрибут `as` обов'язковий. Без нього Chrome ігнорує підказку та виводить попередження в консоль. Браузеру потрібен `as`, щоб визначити правильні заголовки запиту та зв'язати попередньо завантажений ресурс з тим, хто його пізніше запросить. ```html <!-- Правильно: браузер знає тип, ключ кешу збігається --> <link rel="preload" href="/api-data.json" as="fetch" crossorigin> <!-- Браузер ігнорує. При пізнішому запиті кеш не спрацює. --> <link rel="preload" href="/api-data.json"> ``` Для шрифтів `crossorigin` — не побажання, а вимога. Шрифти завантажуються через CORS. Preload без `crossorigin` і фактичний запит шрифту використовують різні ключі кешу. Шрифт завантажується двічі, що зводить нанівець усю ідею. ### prefetch для навігації Prefetch добре працює, коли намір користувача передбачуваний. На сторінці товару — prefetch скрипту кошика. На сторінці входу — prefetch бандла дашборду. Браузер завантажує у фоні і ніколи не конкурує з поточною сторінкою. ```html <!-- Сторінка товару: користувач, швидше за все, піде в кошик --> <link rel="prefetch" href="/cart.js"> <link rel="prefetch" href="/cart.css"> <!-- Список статей: користувач клацне по статті --> <link rel="prefetch" href="/article-renderer.js"> ``` Варто пам'ятати: service worker перехоплює prefetch-запити так само, як і будь-який інший fetch. Якщо у твого SW є cache-first стратегія для цього URL, ресурс може прийти з кешу SW, а не з мережі. ### modulepreload для ES-модульних застосунків Без modulepreload завантаження ES-модульного застосунку відбувається послідовно: браузер завантажує `app.mjs`, парсить, знаходить `import { utils } from './utils.mjs'`, завантажує `utils.mjs`, парсить, знаходить наступний імпорт і так далі. Граф з 3 рівнями — це 3 послідовних round trip до запуску жодного рядка коду. ```html <!-- Без modulepreload: послідовні запити, ~600ms на 3G --> <script type="module" src="/app.mjs"></script> <!-- З modulepreload: все завантажується паралельно при виявленні --> <link rel="modulepreload" href="/app.mjs"> <link rel="modulepreload" href="/utils.mjs"> <link rel="modulepreload" href="/api.mjs"> <script type="module" src="/app.mjs"></script> ``` Vite робить це автоматично. У продакшн-збірці Vite генерує `<link rel="modulepreload">` для кожного чанка в графі модулів, перетворюючи каскад на один паралельний пакет запитів. ### preconnect і dns-prefetch Ці дві підказки працюють на рівні з'єднання, а не ресурсу. Корисні для сторонніх origin-ів, до яких ти точно звернешся. `preconnect` робить ранє рукостискання: DNS-запит, TCP-з'єднання і TLS-переговори. Це економить 100-500ms на першому запиті до того origin-у. `dns-prefetch` вирішує тільки DNS. Дешевше, але й виграш менший (20-120ms). Використовуй для доменів, до яких можеш звернутися, але не гарантовано. ```html <!-- Fonts: ти точно звернешся сюди --> <link rel="preconnect" href="https://fonts.googleapis.com"> <!-- Аналітика: скоріш за все, але некритично --> <link rel="dns-prefetch" href="https://analytics.google.com"> ``` Обмеж preconnect двома-трьома origin-ами. Кожен тримає відкрите TCP/TLS з'єднання, що коштує пам'яті. Більше трьох — і ти витрачаєш ресурси на з'єднання, які можуть закінчитися таймаутом до першого запиту. ### Атрибут fetchpriority Це окремий важіль від типу підказки (Chrome 101+). Він дозволяє підняти або опустити ресурс всередині його стандартного пріоритетного кошика. ```html <!-- Підняти LCP-зображення вище інших високопріоритетних ресурсів --> <img src="/hero.jpg" fetchpriority="high" loading="eager"> <!-- Опустити піксель відстеження поза критичним шляхом --> <img src="/tracking-pixel.jpg" fetchpriority="low"> <!-- Комбінація, яку використовує Next.js для Image з пропом priority --> <link rel="preload" href="/hero.jpg" as="image" fetchpriority="high"> ``` Дані HTTP Archive показують покращення LCP на 30-50% при поєднанні `preload` і `fetchpriority="high"` для зображень вище лінії згину. Next.js застосовує цей шаблон автоматично для зображень з пропом `priority`. ### Типові помилки **Відсутній `as` у preload** ```html <!-- Chrome виводить "Did not assign resource type". Запит ігнорується. --> <link rel="preload" href="script.js"> <!-- Правильно --> <link rel="preload" href="script.js" as="script"> ``` **prefetch для критичного CSS поточної сторінки** ```html <!-- Найнижчий пріоритет. CSS може прийти після рендеру. FOUC у продакшні. --> <link rel="prefetch" href="critical.css" as="style"> <!-- Правильно: якщо потрібно зараз, використовуй preload --> <link rel="preload" href="critical.css" as="style"> ``` **modulepreload для класичного скрипту** ```html <!-- Нічого не робить для не-модульних скриптів. Залежності не вирішуються. --> <link rel="modulepreload" href="bundle.js"> <!-- Працює тільки разом з type="module" --> <link rel="modulepreload" href="app.mjs"> <script type="module" src="app.mjs"></script> ``` **Preload шрифту без crossorigin** ```html <!-- Промах кешу: preload і фактичний запит шрифту мають різні ключі. --> <link rel="preload" href="font.woff2" as="font"> <!-- Правильно --> <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin> ``` **Забагато preload одночасно** Преленти 8-10 ресурсів потрапляють в одну високопріоритетну чергу і конкурують між собою та з CSS, що блокує рендер. Тримай кількість preload в межах 4-5 на сторінку. ### Де зустрічається - Next.js: автоматично додає `<link rel="preload">` для зображень з пропом `priority` та для `_next/static/css` чанків, з `fetchpriority="high"` для LCP-зображень. - Vite: у продакшн-збірці генерує `<link rel="modulepreload">` для кожного чанка в графі модулів. - React 18 streaming SSR: вставляє modulepreload-підказки у HTML-потік для залежностей гідратації. - Lighthouse: аудит "Preload key requests" позначає LCP-ресурси без preload. - Chrome DevTools, вкладка Network: колонка Priority показує High для цілей preload і Low для кандидатів на prefetch. ### Питання на співбесіді **Q:** Яка різниця між пріоритетами preload і prefetch у планувальнику Chromium? **A:** Preload потрапляє на рівень MEDIUM/HIGH у `LoadingScheduler::Priority`, той самий, що й CSS від парсера. Prefetch — на рівень LOWEST і виконується лише коли черга вільна. **Q:** Чи блокує preload парсинг HTML? **A:** Ні. Запит виконується паралельно з парсингом. Якщо ти навмисно затримуєш рендер через обробник `onload` на елементі preload — це твій свідомий вибір, а не автоматична блокування. **Q:** Яка різниця між modulepreload і dns-prefetch? **A:** modulepreload завантажує повний вміст модулів і вирішує весь граф імпортів, наповнюючи кеш модулів реальним вмістом. dns-prefetch тільки перетворює ім'я хоста на IP-адресу. Жодного контенту не завантажується. **Q:** Якщо preloaded ресурс повертає 404, чи отруює це кеш? **A:** Ні. Кожен наступний запит іде в мережу заново. Можна додати обробник події `error` до елемента preload-посилання, щоб відловити збій і підставити запасне джерело — корисно в SPA-роутерах з динамічними шляхами до ресурсів. **Q:** Чи взаємодіє preload з service worker? **A:** Так, service worker перехоплює preload-запити, як і будь-який fetch. Якщо у SW є cache-first стратегія для того URL, ресурс прийде з кешу SW, а не з мережі. **Q:** Ти в продакшні, потрібен максимальний LCP. Який повний шаблон? **A:** Поєднуй preload + `fetchpriority="high"` + `loading="eager"` на елементі LCP-зображення. Потім вимірюй через `PerformanceObserver` з типом `largest-contentful-paint`. На повільних з'єднаннях пропуск цієї комбінації додає ~1 секунду до TTI для сторінок із зображеннями. HTTP Archive показує покращення LCP на 30-50% для зображень вище лінії згину. ## Приклади ### Базовий: preload, prefetch і modulepreload на одній сторінці ```html <!DOCTYPE html> <html> <head> <!-- Preload: високий пріоритет, спрацьовує при парсингу head --> <link rel="preload" href="/hero.avif" as="image" fetchpriority="high"> <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> <!-- Prefetch: фоновий запит під час простою --> <link rel="prefetch" href="/checkout.js"> <!-- Modulepreload: app.mjs та його імпорти завантажуються паралельно --> <link rel="modulepreload" href="/app.mjs"> <link rel="modulepreload" href="/utils.mjs"> </head> <body> <!-- hero.avif вже в кеші, відображається одразу --> <img src="/hero.avif" alt="Hero image" loading="eager"> <!-- Залежності модуля закешовані, без каскаду --> <script type="module" src="/app.mjs"></script> <!-- При кліці checkout.js вже в кеші --> <a href="/checkout">Оформити замовлення</a> </body> </html> ``` Зображення-герой знаходиться в кеші до того, як браузер дійде до тега `<img>`. Шрифт завантажується без спалаху невидимого тексту. Граф модулів вирішується з кешу приблизно за 10ms замість 3 послідовних round trip-ів. ### Середній рівень: продакшн-налаштування Next.js (оптимізація LCP) Це шаблон, який Next.js генерує для сторінки з hero-зображенням і власним шрифтом. ```html <head> <!-- LCP-зображення: preload + fetchpriority — шаблон Next.js Image з priority --> <link rel="preload" href="/hero.avif" as="image" fetchpriority="high"> <!-- Шрифт: crossorigin обов'язковий, type прискорює визначення MIME --> <link rel="preload" href="/_next/static/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> <!-- Критичний CSS-чанк зі збірки Next.js --> <link rel="preload" href="/_next/static/css/app.css" as="style"> <!-- Prefetch ймовірного наступного маршруту --> <link rel="prefetch" href="/_next/static/chunks/dashboard.js"> </head> ``` Без комбінації preload + fetchpriority LCP-зображення конкурує з іншими ресурсами. З нею Lighthouse показує LCP 0.8s проти 2.5s без. CLS залишається на рівні 0, бо шрифт готовий до першого рендеру тексту. ### Просунутий: modulepreload з динамічними імпортами (усунення каскаду) ```html <head> <!-- Декларуємо всі модулі графа на етапі парсингу head --> <link rel="modulepreload" href="/app.mjs"> <link rel="modulepreload" href="/utils.mjs"> <link rel="modulepreload" href="/api.mjs"> </head> <script type="module"> // Без modulepreload: // Round 1 - завантажити app.mjs, знайти utils.mjs + api.mjs // Round 2 - завантажити utils.mjs + api.mjs // Round 3 - виконати: ~600ms на 3G // З modulepreload вище: // Всі три завантажені паралельно при парсингу head // Динамічний імпорт вирішується з кешу за ~10ms import('./app.mjs').then(app => app.init()); </script> ``` Timeline у Chrome DevTools показує, що всі три модулі завантажуються паралельно з моменту парсингу `<head>`: 200ms загалом. Без modulepreload вони завантажуються послідовно після парсингу `app.mjs`: 600ms. Ці 400ms різниці відчутні як регресія TTI на повільних з'єднаннях і одразу видно в Lighthouse.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.