Обнаружение ресурсов, а не их передача: предзагрузка приложений React через HTTP/3
Узнайте, почему HTTP/3 превращает позднее обнаружение ресурсов в настоящий узкий место в приложениях React, и как функции preloadModule, preinit, Early Hints и chunking решают эту проблему.
Обновление сервера до 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: что, вероятно, понадобится при следующем переходе, но с более низким приоритетом?Только последний пункт касается транспортировки данных. Все остальные связаны с поиском ресурсов, определением времени или расписания их загрузки, причем именно в этих аспектах обычно заключается основная польза от использования новых технологий.
Где проявились ограничения мультиплексирования в HTTP/2
В рамках протокола 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 (HOL 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, а затем отдельных переговоров по протоколу 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-связи.
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">
Строго говоря, генерируемый Vite HTML содержит ссылки modulepreload для входного чанка и его статических импортов. Для динамически импортируемых чанков вспомогательный инструмент Vite во время выполнения операции import() вставляет ссылки preload для их зависимостей, благодаря чему чанк и его импорты загружаются параллельно, а не по порядку. Для получения точной информации о поведении обратитесь к документации по сборке вашей версии Vite.
По сравнению с обычным атрибутом rel="preload", атрибут modulepreload позволяет браузеру парсировать и компилировать модуль сразу по его прибытии, вместо ожидания момента выполнения. При использовании протокола HTTP/3 несколько таких указаний передаются по независимым потокам QUIC, поэтому потеря пакета в чанке с контентом поставщика не замедляет загрузку чанка с дашбордом.
От серверного пуша к 103 Early Hints
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 с низким приоритетом.Рассматривайте каждый пункт как рекомендацию, а не как строгое правило. Универсального списка ресурсов для предзагрузки не существует — есть только те ресурсы, которые одновременно важны и обнаруживаются поздно в вашем конкретном приложении.
То же ограничение действует и для fetchpriority. Браузеры уже устанавливают приоритет ресурсов с помощью сложных гибридных алгоритмов, поэтому указание значения high для всех ресурсов эквивалентно его отсутствию. Изменяйте стандартные настройки только тогда, когда измерения показывают, что браузер допускает ошибки.
Пять вопросов для анализа стратегии загрузки
При проверке того, как приложение React загружает свои ресурсы, последовательно рассмотрите следующие вопросы:
- В какой момент обнаруживается этот ресурс? Если честный ответ — «после выполнения JavaScript» или «после отрисовки React», скорее всего, есть возможность показать его раньше.
- В какой момент пользователю он понадобится? Что-то может быть важным, но не требоваться сразу. Именно это различие определяет выбор между
preload(сейчас) иprefetch(в свободное время).
preconnect, затем preload или preloadModule, затем preinit, затем 103 ранних сигнала, и наконец потоковый SSR с сигналами, которые сервер генерирует на основе того, что он отрисовывает.Как соединяются эти слои
Если рассматривать процесс загрузки современного приложения на 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
Старый подход предусматривал передачу ресурсов на клиент как можно активнее: использование серверной доставки, предзагрузку всего необходимого и объединение пакетов для сокращения количества запросов. Современный подход заключается в предоставлении браузеру более полной информации на каждом уровне, позволяя ему самостоятельно определять порядок действий. HTTP/3 обеспечивает транспорт, способный эффективно реагировать на такие решения. Ранние указания передают информацию от сервера в браузер ещё до того, как существует HTML. API для управления ресурсами в React позволяют приложению указать свои намерения в момент готовности компонента.
Ни один слой не может заменить другой. HTTP/3 не может загрузить то, что ещё не было обнаружено. Предварительные указания бесполезны, когда сервер не знает, какие маршруты используются для отображения контента. Кроме того, preloadModule не может спасти монолитный пакет размером 2 МБ, компиляция которого на смартфоне среднего класса занимает 800 мс.
Основные выводы
Самое значимое влияние HTTP/3 на предзагрузку заключается не в ускорении передачи данных, а в том, что более быстрая передача делает позднее обнаружение информации относительно более дорогостоящим процессом. Когда время передачи сокращается с 400 мс до 80 мс, задержка обнаружения, ранее скрытая внутри этого времени, становится основной стоимостью, и стратегия, адаптированная под прежний узкий место, теперь неэффективна.
- Загружайте ресурс заранее, потому что в противном случае браузер обнаружит его слишком поздно, а не только потому, что это важно. Важные ресурсы, обнаруженные вовремя, не нуждаются в подсказках, а незначительные ресурсы, обнаруженные поздно, не заслуживают их.
Suspenseулучшает то, что видит пользователь во время ожидания; он не заставляет блоки данных загружаться быстрее.- Выбирайте наименее эффективную подсказку, которая сработает:
preconnectдля источников,preloadдля файлов,preloadModuleдля модулей,preinit, когда необходимо что-то применить или запустить. - Инициируйте предзагрузку блоков по маршрутам на основе сигналов намерения, таких как наведение курсора и фокусировка, и позвольте функциям Early Hints и потоковому SSR показать то, что сервер уже знает.
- Делайте разделение по маршрутам и крупным библиотекам, помните о возможности перехода на HTTP/2, и измеряйте эффективность перед тем, как уходить в более детальную настройку.
HTTP/3 не сделал предзагрузку устаревшей. Он лишь прояснил, для чего она нужна.
Связанные статьи
- Основы SSR в React 19.2: Activity, cacheSignal и PPR — подробное объяснение — Узнайте, как новый компонент Activity, функция cacheSignal и технология частичной предвыполнительной обработки в React 19.2 дают разработчикам прямой контроль над производительностью серверной выгрузки контента.
- Управление потоками обработки данных Docling через HTTP: от настройки проекта до индексированных блоков — Пошаговое руководство по REST API потоков Docling: запуск сервера, поиск операторов, проверка и выполнение DAG для обработки данных, а также просмотр метрик его работы.