Головна / Статті / Виявлення ресурсів, а не їх передача: попереднє завантаження додатків React через HTTP/3

Виявлення ресурсів, а не їх передача: попереднє завантаження додатків React через HTTP/3

Дізнайтеся, чому HTTP/3 робить запізнє виявлення ресурсів справжньою перешкодою у додатках React, та як preloadModule, preinit, Early Hints та чанкування це вирішують.

4462 слів

Оновлення сервера до HTTP/3 прискорює передачу даних, проте багато додатків на React майже не відчувають прискорення після цього. Зазвичай причиною є те, що повільною була не сама передача даних, а момент, коли браузер узагалі дізнався про існування ресурсу. У цьому посібнику ці дві проблеми розглядаються окремо, показано, де QUIC справді допомагає, а також описано інструменти, які дозволяють раніше здійснювати пошук ресурсів: API ресурсів React 19, попереднє завантаження модулів на основі намірів, повідомлення 103 Early Hints, стрімінг SSR та стратегія часткового завантаження, придатна для мультиплексованої передачі даних.

„Водоспад“, який виглядає нормально, але все одно здається повільним

Уявіть собі команду, яка аналізує панель керування даними через з’єднання 4G. Кожен важливий фрагмент вже завантажений, структура мережевих операцій виглядає організованою, проте час до початку взаємодії все одно значно перевищує цільове значення. Кожен фрагмент швидко завантажується як тільки починається процес, тож HTTP/3 безумовно виконує свою роль.

Якщо подивитися уважніше, схема змінюється. Завантаження відбувається швидко, але запити починаються пізно. React мусить завантажити код, запустити його та відобразити інтерфейс, поки не досягне межі „ледачого“ завантаження, і лише тоді браузер дізнається про необхідність файлу Dashboard.js. Минули сотні мілісекунд, перш ніж було зроблено запит на перший байт цього фрагмента. Затримка виникає ще на етапі попереднього пошуку ресурсів, на кроці, який більшість списків перевірки продуктивності ніколи не розглядає окремо: пошук ресурсів на відміну від їх доставки.

Доставка та пошук — це дві різні проблеми

Допомагає розділити поняття „завантаження ресурсу“ на два запитання:

  • Швидкість передачі: як тільки надійшло запит, наскільки швидко байти подорожують від сервера до браузера?
  • Час виявлення: у який момент браузер узагалі розуміє, що йому потрібні ці байти?

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

HTML → CSS → @font-face rule → font request

Яким би швидким не було з’єднання, запит на шрифт не може бути надісланий до того, як не надійде HTML, не буде завантажено та проаналізовано CSS, і не буде знайдено правило @font-face та протиставлено його відображеному тексту. Це є затримкою у виявленні. Тег <link rel="preload"> не прискорює завантаження шрифту навіть на один мілісекунду; він просто дозволяє браузеру надіслати запит раніше. Майже все в решті цього посібника є варіацією на цю тему.

Який інструмент відповідає на яке запитання

Кожна з технологій, про які йдеться нижче, спрямована на різну стадію процесу завантаження:

  • preconnect: з яких джерел браузер повинен почати встановлювати з’єднання ще до того, як буде надісланий будь-який запит?
  • preload: який саме файл браузер знайде занадто пізно самостійно?
  • preloadModule та modulepreload: які частини ES-модулів слід завантажити та скомпілювати заздалегідь перед виконанням?
  • preinit та preinitModule: які ресурси потрібно не лише завантажити, а й застосувати чи виконати на ранній стадії?
  • prefetch: що, ймовірно, знадобиться під час наступного переходу, але з нижчим пріоритетом?
  • 103 Early Hints: що вже знає сервер до того, як HTML буде готовий?
  • Streaming SSR: як сервер може поступово розкривати контент та ресурси, які за ним стоять?
  • HTTP/3 та QUIC: як ефективно передаються байти запиту після його створення?
  • Лише останній пункт стосується транспортування даних. Усе інше пов’язано з виявленням ресурсів, таймінгом чи плануванням, і ця частка дає чітке уявлення про те, де зазвичай досягаються решта переваг.

    Де HTTP/2 multiplexing виявився недостатнім

    За протоколом HTTP/1.1 браузери досягали паралелізму шляхом відкриття кількох TCP-з’єднань до кожного хоста, зазвичай не більше шести, причому кожне з’єднання передавало один ресурс за раз. HTTP/2 замінив це єдиним з’єднанням, яке об’єднує багато потоків:

    HTTP/1.1                     HTTP/2
    ────────────────             ────────────────────
    TCP conn 1 → JS              One connection
    TCP conn 2 → CSS               ├── Stream A: JS
    TCP conn 3 → Font              ├── Stream B: CSS
    TCP conn 4 → Image             ├── Stream C: Font
                                   └── Stream D: Image
    

    Це був справжній крок вперед, але HTTP/2 все ще базувався на TCP, який гарантує суворо впорядковану передачу одного потоку байтів. Він не розуміє, що деякі байти належать до потоку JavaScript, а інші — до потоку шрифтів. Якщо зникне один пакет, TCP затримує всю передачу після нього до моменту повторної передачі, навіть якщо втрачений пакет належав до потоку C, а інші три потоки не мали з ним нічого спільного.

    Це є блокуванням через проблеми в передачі даних у TCP (TCP head-of-line blocking). У стабільних мережах це рідко помітно; у нестабільних мобільних з’єднаннях це є основною причиною того, чому мультиплексування в HTTP/2 так і не досягло повністю своїх обіцянок.

    Які зміни в QUIC під HTTP/3

    HTTP/3 замінює TCP на QUIC, який працює через UDP та самостійно займається шифруванням:

    HTTP/2          HTTP/3
    ────────        ────────
    HTTP/2          HTTP/3
      ↓               ↓
     TCP            QUIC
      ↓               ↓
     TLS            UDP
      ↓               ↓
     IP             IP
    

    Незалежні потоки усувають блокування на рівні транспорту

    Оскільки QUIC керує потоками всередині протоколу транспорту, кожен потік відновлюється незалежно. Втрачена пакетна інформація затримує лише той потік, до якого вона належала:

    Stream A ──────────────────── ✓
    Stream B ──────────────────── ✓
    Stream C ──────── X ─ retry
    Stream D ──────────────────── ✓
    

    Стріми A, B та D продовжують передаватися, поки C чекає на свою повторну передачу. Найбільший ефект спостерігається саме там, де TCP зазнав найбільших проблем. Дослідження Catchpoint, опубліковане у липні 2025 року та проведене у шести країнах, показало, що на лініях із сильними втратами час до отримання першого байту зменшився на 41,8%. Внутрішні тести в Wix показали, що налаштування з’єднання відбувається на 33% швидше, а показник p75 LCP покращився на 20%. На стабільній широкосмуговій лінії перевага над HTTP/2 зменшується до приблизно 5%. Ця асиметрія сама по собі є інформативною: покращення спостерігаються саме там, де раніше виникали проблеми через блокування HOL. Розглядайте ці цифри як результати досліджень, а не гарантії для вашого трафіку, і вимірюйте показники для своїх користувачів.

    Комплексніші операції перед отриманням першого байту

    За допомогою HTTP/2 через TCP нове з’єднання вимагає виконання процедури TCP handshake та окремих переговорів TLS, що означає два раунд-тріпи перед тим, як почнеться передача даних додатку. QUIC об’єднує процеси обміну даними TLS 1.3 у процедуру налаштування з’єднання та завершує їх обидві протягом одного раунд-тріпа. Для повертаючихся відвідувачів функція 0-RTT дозволяє зашифровані дані запиту передаватися разом із початковим пакетом. На міжконтинентальному каналі затримкою 150 мс це дозволяє заощадити від 150 до 300 мс при кожному новому з’єднанні. Слід пам’ятати, що дані 0-RTT можуть бути переграні, тому сервери зазвичай приймають їх лише для ідемпотентних запитів, таких як отримання статичних ресурсів.

    З’єднання, які виживають після зміни мережевого комутатора

    TCP ідентифікує з’єднання за допомогою поєднання IP-адрес та портів вихідного та кінцевого пристроїв. Коли телефон переходить від мережі Wi-Fi до мобільної, його адреса змінюється, і TCP-з’єднання розривається. Натомість QUIC використовує непрозорий ідентифікатор з’єднання, тож сеанс може мігрувати на новий шлях. Частину маршруту, яку попередньо завантажують, не потрібно починати знову через те, що потяг від’їхав зі станції.

    Чи робить HTTP/3 попереднє завантаження зайвим?

    Ні, і розуміння причин цього є основою всієї теми. HTTP/3 оптимізує спосіб передачі ресурсів; попереднє завантаження оптимізує спосіб їх запиту якомога раніше. Ці два підходи діють у різних етапах процесу:

    Browser
      │
      │  ← "I don't know I need this yet"
      ↓
    Resource discovery    ← preload operates here
      │
      ↓
    Request
      │
      ↓
    QUIC transport        ← HTTP/3 operates here
      │
      ↓
    Server
    

    Транспортний протокол не може отримати щось, про що ще ніхто не просив. Насправді швидший транспорт робить запізнє виявлення проблем ще більш помітним. Припустимо, час передачі даних зменшується з 300 мс до 80 мс. Затримка виявлення у 400 мс, яка раніше частково приховувалася серед загального часу, тепер становить більшу його частину. Вузьке місце просто перемістилося, а не зникло.

    Чому React приховує залежності від браузера

    Звичайні ресурси HTML, такі як джерела зображень <img> та таблиці стилів <link>, знаходяться рано, оскільки парсер браузера (та його сканер спекулятивного попереднього завантаження) бачить їх під час читання документа. React, який обробляється на клієнті, створює набагато довшу ланцюгову залежність, перш ніж деякі з них стануть видимими:

    HTML → main.js → React executes → render → lazy() → discover Dashboard.js → download → render
    

    За допомогою React.lazy() імпорт відбувається після виконання JavaScript. Браузер не може дізнатися про існування Dashboard.js, поки основний пакет не буде завантажений, розібраний, скомпільований та запущений, а React не обробить достатню кількість контенту, щоб дістатися до „лежачого“ компонента. При першому відвідуванні з повільного телефону це може означати кілька секунд очікування перед тим, як почнеться запит на цей частинний пакет.

    const Dashboard = lazy(() => import("./Dashboard"));
    // The browser has no idea Dashboard.js exists
    // until this renders. And it only renders after
    // React has fully bootstrapped.
    

    Suspense покращує час очікування, а не процес виявлення

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

    <Suspense fallback={<Loading />}>
      <Dashboard />
    </Suspense>
    

    Те, що забезпечує Suspense, — це координація: поки лендж-компонент ще не оброблений, React відображає альтернативний варіант замість того, щоб заблокувати всю структуру. Це корисно для покращення сприйманої якості, але це не механізм для прогнозування ресурсів. Запит все одно починається у той самий пізній момент. HTTP/3 дозволить ефективно передавати дані після цього, проте він не впливає на те, скільки часу знадобилося для їх отримання.

    API ресурсів у React 19 та їхнє фактичне призначення

    У React 19 у бібліотеці react-dom надано сукупність функцій, які дозволяють компонентам надсилати інформацію про ресурси до планувальника браузера саме у той момент під час відображення, коли з’являється потреба в них. Це більше, ніж просто обгортки навколо HTML-тегів: React усуває дублікати цих функцій, а під час серверного відображення може вставляти їх у заголовок документа, щоб браузер побачив їх раніше.

    preconnect: підготовка до з’єднання з іншим доменом

    Використовуйте preconnect, коли запит до іншого домену обов’язково буде надісланий незабаром. Цей метод заздалегідь починає процес розрішення DNS, встановлення з’єднання та проведення процедури TLS handshake.

    import { preconnect } from "react-dom";
    // Call this when you know a cross-origin
    // request is coming - not just "might be coming."
    preconnect("https://cdn.example.com");
    

    Використовуйте його лише для тих доменів, з якими ви обов’язково будете взаємодіяти. Кожне попередньо підготовлене з’єднання вимагає ресурсів як на клієнті, так і на сервері, а невикористане з’єднання просто викидається.

    preload: завантаження певного файлу заздалегідь

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

    import { preload } from "react-dom";
    // Font hidden behind CSS - the browser won't
    // find this until it processes @font-face.
    // Preload surfaces it earlier.
    preload("/fonts/inter.woff2", {
      as: "font",
      crossOrigin: "anonymous",
    });
    

    Зверніть увагу на опцію crossOrigin: „anonymous“. Шрифти завжди запитуються у режимі CORS, тому завантаження шрифта без цієї опції призводить до запиту, який не відповідає реальному, і браузер завантажує файл двічі.

    preloadModule: отримання та компіляція ES-модуля

    preloadModule виконує ту саму функцію для ES-модулів, але йде ще далі: модуль завантажується, аналізується та компілюється, після чого зберігається у карті модулів, щоб його можна було використати моментально, як тільки import() звернеться до нього.

    import { preloadModule } from "react-dom";
    // Use this for lazy route chunks you know
    // are likely to be needed soon.
    preloadModule("/assets/Dashboard-abc123.js");
    

    Це ідеальне рішення для „лінивих“ частин маршрутів, які, ймовірно, знадобляться незабаром.

    preinit та preinitModule: отримання та використання

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

    import { preinit } from "react-dom";
    // You don't just want this downloaded -
    // you want it applied before render.
    preinit("/styles/app.css", { as: "style" });
    

    Для CSS найважливішою є різниця між двома фазами обробки. Попередньо завантажений стиль не застосовується. Якщо цей стиль потрібен ще до першого малювання елемента, ви можете завантажити його раніше, але відображення все одно буде чекати, поки щось насправді його не вставить. Функція preinit охоплює обидві ці фази. Щодо скриптів, то діє протилежна порада: використовувати лише той код preinit, який можна безпечно запустити негайно.

    Запуск preloadModule за бажанням користувача

    Виклик preloadModule для кожного маршруту під час запуску марнує пропускну здатність мережі. Найкращий момент — це тоді, коли намір користувача стає очевидним, що зазвичай означає переміщення курсору на посилання або фокус клавіатури на нього безпосередньо перед кліком.

    Наведений нижче компонент з’єднує все це між собою. Він відображає звичайний тег anchor, тож посилання продовжує працювати без JavaScript; викликає функцію preloadModule як при onMouseEnter, так і при onFocus, щоб користувачі клавіатури також могли скористатися функціоналом, а саму навігацію передає функції navigate з React Router:

    import { preloadModule } from "react-dom";
    import { useNavigate } from "react-router-dom";
    
    function NavLink({ to, chunkPath, children }) {
      const navigate = useNavigate();
      return (
        <a
          href={to}
          onMouseEnter={() => preloadModule(chunkPath)}
          onFocus={() => preloadModule(chunkPath)}
          onClick={(e) => {
            e.preventDefault();
            navigate(to);
          }}
        >
          {children}
        </a>
      );
    }
    
    // Usage
    <NavLink to="/dashboard" chunkPath="/assets/Dashboard-abc123.js">
      Dashboard
    </NavLink>
    

    Інтервал між наведенням курсору та кліком зазвичай становить від 100 до 400 мс. Через протокол HTTP/3 середнього розміру дані часто можуть бути оброблені протягом цього часу: при швидкості 10 Мбіт/с 150 КБ потребують приблизно 120 мс. До моменту кліку модуль вже буде скомпільований у карті модулів, проблема з відкладеною загрузкою миттєво вирішиться, і замінник Suspense так і не з’явиться.

    Те, що досягає обробник onMouseEnter, — це те, чого не може зробити жоден протокол передачі даних: він перетворює сигнал наміру на інформацію про ресурс ще до подання запиту на навігацію. HTTP/3 потім ефективно керує передачею даних. Кожен рівень виконує свою функцію.

    Два практичні зауваження. По-перше, chunkPath має бути ім’ям файлу у форматі хешу, яке насправді створив ваш інструмент для об’єднання файлів, тож у реальному проекті воно має походити з маніфесту будови проекту, а не вводитися вручну. По-друге, пристрої на дотик не мають функції наведення курсору, тому якщо для вас важлива мобільна навігація, розгляньте використання onTouchStart або тригерів, заснованих на вікні перегляду.

    Попереднє завантаження на рівні інструменту для об’єднання файлів

    Для масового, низькопріоритетного попереднього завантаження під час вільного часу webpack підтримує спеціальний коментар всередині динамічного імпорту:

    const Dashboard = lazy(
      () => import(/* webpackPrefetch: true */ "./Dashboard")
    );
    

    Зверніть увагу, що webpackPrefetch створює тег <link rel="prefetch">, який браузер розглядає як завдання низької пріоритетності, що виконується у вільний час для можливої майбутньої навігації. Це відрізняється від тегів preload чи modulepreload високої пріоритетності, які використовуються для ресурсів, необхідних прямо зараз.

    Vite використовує більш автоматичний підхід та генерує за вас теги modulepreload. З webpack потрібен спеціальний коментар чи плагін. Нативна HTML-форма цього сигналу виглядає так:

    <!-- Vite generates these for lazy chunks automatically -->
    <link rel="modulepreload" href="/assets/Dashboard-abc123.js">
    <link rel="modulepreload" href="/assets/vendor-react-def456.js">
    

    Якщо говорити точніше, HTML, створений Vite, містить посилання modulepreload для основного блоку та його статичних імпортів. Для динамічно імпортованих блоків вбудований інструмент Vite додає посилання на попереднє завантаження їхніх залежностей у момент виконання import(), тож блок та його імпорти завантажуються паралельно, а не по черзі. Для отримання точної інформації про таку поведінку перегляньте документацію щодо збірки вашої версії Vite.

    Порівняно зі звичайним rel="preload", modulepreload дозволяє браузеру аналізувати та компілювати модуль відразу після його отримання, замість того щоб чекати моменту виконання. Через HTTP/3 кілька таких підказок передаються по окремих потоках QUIC, тому втрата пакета у блоку постачальника не заважає завантаженню блоку панелі керування.

    HTTP/2 намагався вирішити проблему виявлення ресурсів з боку сервера за допомогою технології Server Push: сервер надсилав ресурси, яких браузер ще не запитував. Мета передачі необхідної інформації раніше була правильною, але її реалізація зазнала невдачі. У сервера не було надійного способу дізнатися, чи вже є у браузері кешована копія ресурсу, тому він часто надсилав дублікати, споживав більше пропускної здатності та конкурував за ресурси з тими, які сам браузер вважав більш терміновими. Зрештою Chrome припинив підтримку Server Push.

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

    103 Early Hints дозволяє впровадити це на практиці. Поки сервер ще готує основну відповідь, він надсилає тимчасовий статус 103 із заголовками Link. Браузер може негайно почати завантажувати ці ресурси, і до моменту отримання остаточної відповіді 200 OK з HTML деякі з них вже можуть бути завершені.

    Browser                    Server
      │                          │
      │──── GET / ─────────────→ │
      │                          │ (generating HTML...)
      │ ←─── 103 Early Hints ─── │
      │   Link: </assets/main.js>; rel=modulepreload
      │   Link: </assets/vendor.js>; rel=modulepreload
      │                          │
      │ (fetching chunks now...) │ (still generating...)
      │                          │
      │ ←─── 200 OK + HTML ───── │
      │   (chunks already downloading or done)
    

    На момент написання цього тексту NGINX вже має вбудовану підтримку Early Hints з версії 1.29.0 у червні 2025 року, а Cloudflare надає її у вигляді перемикача у своєму панелі керування. У Node.js об’єкт відповіді має метод writeEarlyHints(), який можна викликати у власному сервері чи проміжковому програмному забезпеченні перед надсиланням справжньої відповіді:

    // In a custom server or middleware
    res.writeEarlyHints({
      link: [
        "</assets/main.js>; rel=modulepreload; as=script",
        "</assets/vendor.js>; rel=modulepreload; as=script",
        "</assets/Dashboard.js>; rel=modulepreload; as=script",
      ],
    });
    
    // Then proceed with normal response
    res.status(200).send(html);
    

    У цьому фрагменті для остаточної відповіді використовується синтаксис у стилі Express res.status().send(). У звичайному сервері Node.js http слід було б використовувати res.writeHead() та res.end(). Функція Early Hints є корисною лише тоді, коли сервер справді витрачає час на обробку, наприклад, на виконання запитів до бази даних чи генерацію вмісту, поки браузер інакше залишався б без діяльності.

    Дослідження, опубліковане на corewebvitals.io з використанням даних про час виконання з Chrome DevTools, показало, що розміщення критичного CSS-файлу через Early Hints призводить до того, що елемент LCP з’являється приблизно на 35% швидше, ніж у разі звичайного попереднього завантаження всередині HTML. Для React-додатку це означає, що основний фрагмент коду завантажується під час роботи сервера, а не після того, як HTML вже надійшов.

    Поєднання стрімингового SSR, Early Hints та HTTP/3

    Функція потокового серверного рендерингу в React додає ще один інструмент. Замість того, щоб затримувати відповідь до повної готовності сторінки, сервер надсилає HTML поетапно:

    HTML shell → Suspense fallback → more HTML → resolved content → hydration
    

    Під час рендерингу сервер знає, які межі Suspense будуть оброблені та від яких частин вони залежать. Цю інформацію можна передати браузеру через Early Hints ще до початку потоку HTML. Спрощена часова шкала:

    0ms  ── Browser sends request
         ── Server starts rendering
    1ms  ── Server knows Dashboard boundary will render
         ── Server sends 103 Early Hints: Dashboard.js
         ── Browser starts fetching Dashboard.js
    50ms ── Server streams HTML shell
         ── Browser starts parsing
    120ms── Server streams Dashboard content
         ── Dashboard.js already downloaded
         ── Hydration starts immediately
    

    Порівняйте ту саму програму без Early Hints, де процес виявлення інформації відбувається у очікуванні від клієнта:

    0ms  ── Browser sends request
    50ms ── Server streams HTML shell
         ── Browser starts parsing
         ── Browser discovers <script> tags
         ── main.js starts downloading
    180ms── React executes
         ── Hits Dashboard lazy boundary
         ── Dashboard.js request starts (now)
    300ms── Dashboard.js downloads
         ── Hydration starts
    

    HTTP/3 скорочує кожен сегмент у обох таймлайнах. Те, що змінюють Early Hints, — це момент початку запиту до панелі керування, що призводить до іншої, часто більшої економії ресурсів. Якщо процес відображення у вашій фреймворку вже обробляється іншими примітивами, стаття про примітиви SSR React 19.2, такі як Activity та часткове попереднє відображення детальніше розглядає, як створюються межі стрімінгу.

    Переосмислення гранулярності чанків для мультиплексованої передачі

    Давня рекомендація щодо мінімізації кількості HTTP-запитів походила від обмеження у шість з’єднань у HTTP/1.1 та від того, що мультиплексування у HTTP/2 було лише частково паралельним через один TCP-стрім. Завдяки незалежним QUIC-стрімам кількість запитів більше не є провідним фактором, як раніше, тож можна більш активно ділити контент, не платячи таких самих штрафів за кожен запит.

    Розумний стандартний спосіб розділення виглядає так:

    • React та ReactDOM: окремий, стабільний чанк від постачальника. Він рідко змінюється, тому може мати довгий термін зберігання в кеші.
    • Бібліотека Router: власний чанк з тієї ж причини стабільності.
    • Важкі бібліотеки сторонніх розробників, такі як інструменти для створення графіків чи редактори: по одному чанку на кожну бібліотеку, щоб оновлення однієї не усувало ефект від інших.
  • Компоненти маршруту: по одному фрагменту на маршрут через React.lazy(), тож навігація завантажує лише те, що їй потрібно.
  • Спільні утиліти: єдиний спільний фрагмент, створений бандлером, який завантажується один раз та використовується у всіх маршрутах.
  • Адміністративні або рідко використовувані функції: окремі „лежачі“ фрагменти, які користувачі, що ніколи не відкривають ці екрани, ніколи не завантажують.
  • Перевагою є точність кешування. Редагування Dashboard.tsx має очищувати кеш лише для цього фрагмента маршруту, тоді як бандл постачальника залишається у кеші, і це можливо лише завдяки дробленню на окремі частини. Через HTTP/3 отримані 8–15 фрагментів завантажуються через окремі потоки без затримок між ними.

    Vite обробляє більшість цих ситуацій без налаштувань. У webpack параметр splitChunks.cacheGroups відображає ту саму політику. Наведене нижче налаштування створює частину коду для React, частину для маршрутизації та частину лише з асинхронним кодом для графіків; значення priority визначають, яка група буде використана, коли модуль відповідає кільком критеріям, а параметр chunks: "async" не дозволяє коду для графіків завантажуватися під час початкового завантаження:

    // webpack.config.js
    module.exports = {
      optimization: {
        splitChunks: {
          cacheGroups: {
            reactVendor: {
              test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
              name: "vendor-react",
              chunks: "all",
              priority: 40,
            },
            routerVendor: {
              test: /[\\/]node_modules[\\/](react-router|react-router-dom)[\\/]/,
              name: "vendor-router",
              chunks: "all",
              priority: 30,
            },
            chartsVendor: {
              test: /[\\/]node_modules[\\/](recharts|d3)[\\/]/,
              name: "vendor-charts",
              chunks: "async",
              priority: 20,
            },
          },
        },
      },
    };
    

    Є один недолік. Дуже маленькі частини коду збільшують навантаження на обробку та компіляцію кожного файлу в браузері. Крім того, не кожен відвідувач має підтримку HTTP/3: поширені корпоративні фаєрволи, які блокують UDP на порту 443, тож такі користувачі змушені використовувати HTTP/2, де частина витрат, пов’язаних із кількістю запитів, зникає. Перед тим, як дробити код на ще менші частини, необхідно провести вимірювання. Зазвичай доцільно робити це за рівнем маршруту чи окремих великих бібліотек; за рівнем окремих компонентів це зазвичай не ефективно. Якщо ви використовуєте Next.js, стаття „Контроль частинок у Turbopack“ описує відповідні налаштування для цього фреймворку.

    Чому попереднє завантаження всього все одно має негативні наслідки

    Мультиплексування дозволяє кільком потокам користуватися одним з’єднанням, але воно не надає кожному з них необмеженої пропускної здатності та не робить їх однаково важливими. Двадцять підказок modulepreload все одно використовують один канал; конкуренція просто розподіляється між більшою кількістю потоків.

    Тож корисне запитання полягає не в тому, „що можна завантажити заздалегідь?“, а в тому, „який важливий ресурс браузер помітить занадто пізно?“. Як вихідну точку, розгляньте:

    • Критичний шрифт: preload.
    • Критичний файл стилів: preload, або preinit, якщо його потрібно застосувати до малювання елементів.
    • Критичний модуль JavaScript: preloadModule.
    • Важливий CDN або джерело API з іншого походження: preconnect.
  • Зображення героя, яке відображається під час завантаження: preload з параметром fetchpriority="high".
  • Зображення, розташовані далі у сторінці: без попереднього завантаження.
  • Частини контенту, які завантажуються з запізненням: preloadModule, активоване за бажанням користувача.
  • Скрипти аналітики: без попереднього завантаження.
  • Віджет чату: завантажується з запізненням без попереднього завантаження.
  • Код, доступний лише для адміністратора: без попереднього завантаження.
  • Найімовірніша наступна навігація: prefetch з низьким пріоритетом.
  • Критичні ресурси, про які сервер вже знає: 103 Early Hints.
  • Кожен пункт слід розглядати як рекомендацію, а не як обов’язкове правило. Не існує універсального списку ресурсів для попереднього завантаження — є лише ті ресурси, які є важливими та виявляються пізно у вашому конкретному додатку.

    Такі самі обмеження стосуються також fetchpriority. Браузери вже пріоритезують ресурси за допомогою складних евристик, тож позначення всього як high еквівалентне відсутності будь-яких позначок. Змінюйте значення за замовчуванням лише тоді, коли показники свідчать про помилку браузера.

    П’ять запитань для перегляду стратегії завантаження

    Під час аудиту того, як React-додаток завантажує свої ресурси, послідовно розгляньте наступні запитання:

    1. У який момент виявляється цей ресурс? Якщо чесна відповідь — „після виконання JavaScript“ або „після відображення React“, ймовірно, є можливість виявити його раніше.
    2. У який момент користувач його потребує? Щось може бути важливим, але не потрібним негайно. Саме це розрізнення визначає використання preload (зараз) та prefetch (у вільний час).
  • Чи можна передати ці знання раніше? Приблизно у порядку зростання зусиль: preconnect, потім preload або preloadModule, потім preinit, потім 103 Early Hints, а далі — стрімінг SSR із підказками, які сервер отримує на основі того, що він відображає.
  • З чим це конкурує? Кожна підказка має вартість у вигляді інших можливостей. Попереднє завантаження частини Dashboard означає, що інші елементи отримують менше уваги, тож потрібно знати, що саме це.
  • Чи є обмеженням мережа чи процесор? Попереднє завантаження пакета розміром 2 МБ призводить до його раннього завантаження, але не впливає на час парсингу, компіляції та виконання. Якщо обмеженням є основний потік, раннє виявлення змінює лише те, де чекає користувач, а не протягом якого часу.
  • Як взаємодіють ці шари

    Якщо розглядати процес завантаження сучасного додатку на React в цілому, він включає шість рівнів, кожен з яких має власний діапазон функціональності:

    Application intent (React knows which routes and components are needed)
           ↓
    Resource APIs (preconnect / preload / preloadModule / preinit)
           ↓
    Server-side surfacing (103 Early Hints / streaming SSR)
           ↓
    Browser resource scheduler (priority, cache, bandwidth estimation)
           ↓
    HTTP/3 / QUIC transport (independent streams, 0-RTT, connection migration)
           ↓
    Network
    

    Старіший підхід полягав у максимальному надсиланні ресурсів на клієнт: Server Push, попереднє завантаження всього та об’єднання пакетів для зменшення кількості запитів. Сучасний підхід передбачає надання браузеру більш детальної інформації на кожному рівні та дозвіл йому самостійно здійснювати оптимізацію. HTTP/3 забезпечує транспортну систему, яка може ефективно виконувати ці рішення. Функція Early Hints передає інформацію від сервера до браузера ще до створення HTML-коду. API ресурсів React дозволяють додатку вказати свої наміри у момент, коли компонент готовий до використання.

    Жоден шар не може замінити інший. HTTP/3 не може отримати те, що ще не було виявлено. Попередні підказки марні, коли сервер не знає, які маршрути використовуються. А preloadModule не може врятувати монолітичний пакет розміром 2 МБ, який потребує 800 мс для компіляції на телефоні середнього класу.

    Ключові висновки

    Найбільший вплив HTTP/3 на попереднє завантаження полягає не у прискоренні передачі даних, а у тому, що швидша передача робить пізнє виявлення даних відносно більш дорогим. Коли час передачі з 400 мс скорочується до 80 мс, затримка виявлення, яка раніше була прихована всередині неї, стає основною витратою, і стратегія, налаштована під старий бутлек, тепер не ефективна.

    • Завантажуйте ресурс заздалегідь, тому що інакше браузер знайде його занадто пізно, а не лише тому, що це важливо. Важливі ресурси, які знаходяться вчасно, не потребують підказок, а неважливі, які знаходяться пізно, не заслуговують на них.
    • Suspense покращує те, що бачить користувач під час очікування; він не прискорює завантаження частин контенту.
    • Виберіть найслабшу ефективну підказку: preconnect для початкових адрес, preload для файлів, preloadModule для модулів, preinit, коли щось потрібно застосувати або запустити.
    • Запускайте попереднє завантаження частин контенту за маршрутом на основі сигналів наміру, таких як наведення курсора та фокусування, і дозвольте Early Hints та стріминговому SSR показати те, що сервер вже знає.
    • Розділяйте завантаження за маршрутами та великими бібліотеками, пам’ятайте про альтернативу HTTP/2 та вимірюйте ефективність перед тим, як переходити до більш детального підходу.

    HTTP/3 не зробив попереднє завантаження застарілим. Він зробив більш зрозумілим, для чого воно потрібне.

    Пов’язана література