Webrium
Подписаться

Размер бандла больше не главная метрика

Почему война с общим весом сборки не ускоряет приложение и при каких условиях размер бандла всё-таки важен

Сетка ячеек с заполненной частью изображает вес сборки, а рядом стоит отдельный счётчик другой величины — путь до первого действия: одно число больше не определпуть до действия

Короткого ответа нет

Вопрос «важен ли размер бандла» сформулирован как запрос на «да» или «нет», но честного короткого ответа у него давно нет. Не потому, что мы уходим от ответственности, а потому что доставка кода в браузер перестала быть одной операцией.

Когда-то модель была честной и простой: страница грузила один скрипт, тот блокировал рендер, целиком парсился и целиком исполнялся до того, как человек что-нибудь увидит. В этой модели байты почти линейно превращались в ожидание: больше кода — дольше ехать по сети, дольше парсить, дольше исполнять. Вес был жёстко привязан к тому, что чувствует человек, и мог с чистой совестью называться главной метрикой.

Потом модель разобрали. Код режется на куски и отдаётся по требованию, запросы едут параллельно, сжатие на лету меняет и вес, и его смысл, кэш делает один и тот же файл бесплатным для вернувшегося и полновесным для новичка, разметка отдаётся потоком, а часть работы вообще уехала на сервер. «Размер бандла» из одного числа превратился в форму кривой: что приезжает, когда приезжает и что из этого стоит между человеком и первым действием.

Обычно за абстрактным вопросом стоит бытовой спор в ревью: тянуть ли зависимость, спорить ли из-за лишнего веса или махнуть рукой. Ответ «зависит» в такой ситуации бесполезен — решение нужно сегодня.

Наш короткий ответ: важен — но как ограничение, а не как цель. Дальше — тупик, в который почти каждый успевает упереться до этого ответа, и условие, при котором мы неправы.

Тупик: война с общим весом

Первое, что пробуют, когда приложение ругают за скорость, — уменьшить общий вес сборки. Рефлекс понятен: тормозит — значит, кода много — значит, код надо ужать. К тому же вес — единственное число про производительность, которое видно без приборов. Дальше сценарий развивается примерно одинаково.

Сначала гигиена: минификация пожёстче, выкидывание «тяжёлой» зависимости, добивание tree-shaking. Цифра в отчёте сборки подрастает вниз. Следом порядок: бюджет в CI — сборка выше порога, билд красный, регрессии заперты. Дальше визуализатор сборки: открываем, находим самый большой прямоугольник, выпиливаем его. Цифра падает уже заметно.

Ожидание простое: цифра вниз — жалобы вниз.

Стена выглядит так: цифра вниз, а жалобы на месте. Визуально — прогресс, в отчёте — прогресс, по ощущениям людей — ничего. И самое неприятное — непонятно почему: каждый шаг ведь «правильный», из учебника.

Разберём по механизмам, потому что дело не в плохом исполнении, а в самой постановке.

Ожидание пользователя — не число, а цепочка: соединение, первый байт, разметка, критические куски, парс, гидрация, первый осмысленный отклик. Общий вес — один член этой суммы. Полировать его имеет смысл, только если именно он доминирует, а общее число принципиально не сообщает, какой член доминирует у вас. Если узкое место не в нём, вы прилежно шлифуете не ту деталь — с чувством прогресса и без результата.

Визуализатор усугубляет: он сортирует куски по весу, а не по посещаемости. Самый большой прямоугольник может жить на роуте, куда почти никто не заходит, а тонкая полоска на входном экране держит всех. Это две разные сортировки, и второй в визуализаторе нет.

Бюджет на общий вес ломается с обеих сторон. Он молчит, когда тяжелеет входной роут: сумма не сдвинулась, а путь, которым ходят все, стал длиннее. И он же орёт, когда вы добавили предзагрузку или перенесли что-то в кэшируемый слой: сумма выросла, ожидание упало — гейт наказывает за улучшение. Метрика, которая наказывает за улучшение и пропускает регрессию, оторвана от цели, что бы она ни измеряла.

Отдельно про замену библиотеки самописным куском покороче: это обмен измеренных байтов на неизмеренные баги. Если байты не были тем, что чувствовал человек, приложения вы не ускорили — вы взяли на себя сопровождение чужой работы.

Справедливости ради: гигиена из первого шага — дело правильное, минифицировать и выкидывать мёртвый код надо всегда. Просто гигиена — не лечение. Тупик не в том, что делали плохо, а в том, что мерили не то.

Почему байт не равен байту

У байта кода по меньшей мере три цены. Передача — сеть. Парс и компиляция — процессор; эта цена растёт с числом байтов. Исполнение — растёт не с байтами, а с работой, и происходит там, где живёт отзывчивость: на главном потоке. Два куска кода одного веса могут стоить по-разному: один — это только доставка и парс, другой — доставка, парс и исполнение между человеком и первым откликом. Складывать их в одно число — всё равно что складывать счёт за электричество со штрафом за просрочку: сумма что-то сообщает, но не то, что вам нужно.

Сжатие и кэш усложняют картину. То, что измерено в сборке, и то, что реально едет по проводам, — разные величины: сжатие съедает часть веса, и сколько именно — зависит от характера кода. Дальше кэш: для вернувшегося пользователя передача почти ничего не стоит, но парс и исполнение он оплачивает целиком, при каждом визите. Метрика веса не знает, кто перед ней — новичок или постоянный посетитель, а от этой пропорции зависит, какой рычаг вообще имеет смысл двигать.

Есть и скрытые платежи, которых в весе не видно. Код, который ни разу не исполнился, всё равно проходит парс и компиляцию: «догрузили лениво, но распарсили сразу» — не бесплатно. Гидрация заставляет платить за одно дерево дважды: сначала нарисовать, потом оживить, и вторая оплата идёт главным потоком — ровно в момент, когда человек уже тянется что-то нажать.

Порядок и момент оплаты в итоге важнее суммы. Предзагрузка, инлайны, ленивые куски — каждое такое решение меняет не вес, а расписание платежей: кто, что и когда оплачивает. Общая цифра не может арбитрировать эти сделки: она видит сумму, человек живёт в моменте оплаты.

Потоковый рендер сдвинул финиш ещё дальше: человек начинает читать раньше, чем приехал весь код, и порог проходит не «скачано», а «оживлено и отвечает». После этого бой за вес файла — бой за этап, который человек мог вообще не заметить.

Это тот же спор, что и вокруг серверных компонентов, о котором мы писали отдельно: перенос работы с клиента на сервер меняет распределение стоимости, но не отменяет её. Там, где рендер уехал на сервер, вопрос «что и когда исполняется на клиенте» никуда не девается.

Что замерять вместо общего числа

Метод, в который вы подставляете свои числа.

Возьмите роут, который пользователи открывают первым, — или тот, что приносит деньги; обычно это одно и то же. Откройте его на устройстве и в сети, похожих на целевые: не на рабочем ноутбуке, а на том, на чём сидит аудитория. Не знаете, на чём сидит аудитория, — берите худшее из разумных предположений: оптимистичный замер обманывает первым делом того, кто его сделал.

Прогоните дважды: с холодным кэшем и с тёплым. Разложите время от первого байта до первого осмысленного взаимодействия на две части — ожидание сети и работу главного потока: парс, компиляция, гидрация, обработчики.

Дальше арифметика:

  • доминирует сеть при холодном кэше — байты на критическом пути и правда важны: режьте их и выстраивайте порядок загрузки;
  • доминирует главный поток — вес сборки почти ни при чём: рычаг — делать меньше работы при старте, разносить исполнение во времени, уводить его с главного потока;
  • тёплый прогон почти не отличается от холодного — сеть и не была вашей проблемой, и вся борьба за байты шла не за то.

Тёплый прогон вообще недооценён: он показывает, сколько стоимости вы заставляете повторно оплачивать тех, кто уже всё скачал. Если разница между визитами мала, вопрос не в байтах, а в том, что вы делаете на главном потоке при каждом заходе.

В CI имеет смысл гейтить два числа на каждый входной роут: холодный путь до первого взаимодействия и работу главного потока до первого отклика. Общий вес оставьте как санитарную норму: он ловит грубые ошибки вроде случайно втащенной зависимости, но не должен быть местом, где заканчивается разговор о производительности.

Позиция редакции

Наша позиция: размер бандла — санитарная норма, а не цель. Держите его как бюджет, чтобы регрессия в байтах на критическом пути не проезжала молча: рост веса почти всегда тянет за собой рост передачи и парса, и гейт здесь отрабатывает честно. Но главной метрикой мы считаем время до первого осмысленного взаимодействия на целевом устройстве, по конкретным роутам. Число, которое показывают на ревью и празднуют в отчётах, должно быть тем, что чувствует человек, — иначе вы оптимизируете слайд, а не продукт.

Оговорка для тех, кому пока нечем мерить: если доступ к целевым устройствам и телеметрии есть не везде, дешёвый прокси — общий вес с бюджетом — лучше, чем ничего. Но это костыль, и держать его стоит ровно до появления настоящих замеров.

Условие, при котором мы неправы: у вас преимущественно холодный трафик — первые визиты, кэш почти не срабатывает, сеть слабая. Тогда передача байтов по критическому пути становится доминирующим членом суммы, и общий вес снова оказывается хорошим приближением главной метрики. В этом мире спорить о каждом байте — не педантизм, а прямая работа над скоростью. Проверка — замер из предыдущего раздела: если сеть съедает ожидание, вы наш контрпример, и правы вы, а не мы.

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Поле разделено швом «use client»: элементы-пропсы пересекают границу между сервером и клиентом и становятся сетевым контрактомсерверклиент
Спор

Серверные компоненты: год в продакшене

Бандл уже не главное? Серверные компоненты всё равно пересылают дерево по проводам, а проп через границу становится частью сетевого контракта — и поломки модели предсказуемы заранее.

Состояние бывает четырёх видов, и библиотека нужна только одному из нихвиды состояниянужна библиотека
Предыстория

Состояние в React: когда нужна библиотека, а когда нет

База под остальное: четыре вида состояния, из которых библиотеке достаётся только один, и признаки, по которым их различают.

На шкале времени жизни экрана величина — момент, когда становятся известны входные данные, — пробивает пороговую линию между сервером и клиентом: если данные изсервер/клиентвремя жизни экрана
Практика

Где проходит граница клиент — сервер

Раз метрика бандла отошла на второй план, границу клиент — сервер стоит проводить не по типам компонентов, а по моменту, когда становятся известны входные данные. Один экран разобран по шагам.

Схема совпадения заголовков CORS: разрешённый и заблокированный запросЗапрос разрешёнЗапрос заблокирован
Углубление

CORS: почему браузер блокирует запрос и что менять на сервере

Почему браузер блокирует ваш запрос, как это связано с размером бандла и что исправить на сервере, чтобы всё заработало.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть