Оптимизация React-бандла для приложений, которые росли постепенно
Оптимизация React-бандла не требует переписывать приложение с нуля. Проверьте маршруты, сократите пакеты и задайте правила для изображений, чтобы ускорить загрузку.

Почему время загрузки становится всё хуже
Приложение на React редко начинает тормозить одним большим скачком. Оно замедляется понемногу, почти вежливо. Кто-то добавляет помощник для дат, потом пакет для графиков, потом редактор богатого текста, потом скрипт аналитики. Каждое изменение само по себе кажется мелким, но браузеру всё равно нужно скачать, разобрать и запустить всё это.
Вот почему оптимизация React-бандла часто начинается с простой мысли: рост добавляет вес быстрее, чем команда это замечает. Большинство приложений становятся медленнее не потому, что изменилась основная идея продукта. Они тормозят потому, что полезные мелкие дополнения всё время попадали в кодовую базу.
Вторая проблема в том, что многие приложения отправляют слишком много кода на каждую страницу. Экран входа не нуждается в том же коде, что и админ-панель. Странице с ценами не нужны инструменты построения графиков, которые используются в отчётах. Когда каждый маршрут тащит за собой все функции, пользователи платят цену заранее, даже если они никогда не открывают эти части продукта.
Изображения создают такой же эффект. Они попадают в приложение в обычной работе с контентом, а не только во время редизайна. Коллега загружает большой hero-баннер прямо из Figma. Редактор блога вставляет скриншоты в полном размере. Фотографии продукта выглядят нормально при быстром офисном интернете, поэтому никто не замечает, что мобильные пользователи ждут на несколько секунд дольше.
Именно поэтому время загрузки может ухудшаться, даже когда команда старается быть аккуратной. Никто не принял одного ужасного решения. Приложение просто набрало вес во многих обычных местах.
Хорошая новость в том, что обычно не нужен полный передел, чтобы это исправить. Можно сильно сократить задержку, если отправлять меньше кода на каждый маршрут, убрать пакеты, которые делают одно и то же, и установить понятные правила для изображений. Во многих растущих продуктах этого уже достаточно, чтобы приложение снова ощущалось быстрым.
Где обычно прячется лишний вес
Большинство React-приложений становятся тяжёлыми не из-за одного плохого решения. Они тяжелееют маленькими шагами. Вспомогательная функция попадает в корневой шаблон, библиотека графиков оказывается в общем каркасе, пакет иконок импортируют один раз, а потом он используется везде.
Первое место, которое стоит проверить, — код, который загружается ещё до рендера любого маршрута. Если оболочка приложения тянет меню, аналитику, редакторы, графики или тяжёлую логику состояния, которая нужна только нескольким экранам, за это платит каждый посетитель. Обычно именно с этого и начинается оптимизация React-бандла: вынесите код, завязанный на конкретный маршрут, из глобального слоя и оставьте корневой шаблон максимально простым.
Частая проблема — большой пакет, который нужен только на одном экране. Странице админ-отчёта может требоваться библиотека для графиков, инструмент для PDF и тяжёлые возможности таблиц. Маркетинговым страницам это не нужно. Если такие пакеты лежат в общем коде, они едут с каждой загрузкой страницы.
То же самое происходит с утилитами. Команды часто импортируют всю библиотеку ради одной небольшой задачи — форматирования дат, глубокого копирования или debounce-помощника. Один импорт кажется безобидным. Десять таких импортов быстро дают заметный вес. Наборы иконок тоже легко выходят из-под контроля, когда приложение тянет целый пакет вместо нескольких нужных иконок.
Эти файлы часто незаметно попадают в бандл:
- пакеты для графиков и редакторов, загруженные из общих компонентов
- целые библиотеки утилит вместо небольших прямых импортов
- библиотеки дат с лишними локалями
- большие наборы иконок ради нескольких значков
Изображения заслуживают такого же внимания. Больше всего важны картинки над первым экраном, потому что именно их ждут пользователи. Большой hero-баннер, слишком крупный скриншот продукта или не сжатый ряд аватарок могут замедлить первый показ сильнее, чем кусок JavaScript.
Простое правило помогает: код маршрута должен оставаться при маршруте, а глобальный код должен покрывать только то, что действительно нужно каждой странице. Если экран редкий, тяжёлый или находится за логином, не тащите его в первую загрузку. Одна эта привычка убирает удивительно большой объём лишнего веса.
Сначала измерьте, потом меняйте
Если приложение на React кажется медленным, догадки обычно только тратят время. Оптимизация React-бандла начинается с базовой точки, с которой можно потом сравнивать результаты, иначе любая правка будет казаться случайной.
Запишите текущий размер JavaScript для каждого основного маршрута, а не только для главной страницы. Панель, страница настроек и карточка продукта часто загружают очень разный объём кода. Зафиксируйте и первую загрузку, и дополнительные файлы, которые каждый маршрут подтягивает после перехода.
Затем проверьте несколько страниц так, как их видит большинство людей: на замедленном мобильном соединении и в профиле среднего телефона в инструментах браузера. Страница, которая открывается за 2 секунды на офисном Wi‑Fi, на медленном мобильном интернете может занимать 7 или 8. Отслеживайте две простые цифры: когда страница становится пригодной для работы и сколько данных понадобилось для этого.
Достаточно короткой таблицы:
- Название маршрута
- Объём JS при первой загрузке
- Самое большое изображение на странице
- Время до готовности на замедленном мобильном профиле
- Бюджет для этого маршрута
Потом отсортируйте зависимости по размеру и по реальному использованию. Некоторые пакеты действительно заслуживают своё место. Другие добавляют 80 КБ только для того, чтобы один экран дважды в месяц красиво форматировал даты. Пометьте каждый простым знаком: используется везде, используется на одном маршруте или почти не используется. Потом будет гораздо легче что-то убрать.
То же самое сделайте с изображениями. Составьте список самых тяжёлых файлов, их размеров и мест, где они показываются. Команды часто сжимают JavaScript и забывают, что один слишком большой hero-баннер может свести на нет весь выигрыш.
Установите небольшой лимит для первой загрузки и сделайте его заметным. Например, можно ограничить первый маршрут 200–250 КБ JavaScript и держать изображения над первым экраном ниже определённого размера. Точные цифры зависят от приложения, но правило должно быть настолько понятным, чтобы любой человек, который добавляет пакет или загружает файл, мог проверить его за минуту.
Обычной таблицы вполне достаточно. Здесь не нужны навороченные инструменты. Задача — перестать спорить по ощущениям и начать сравнивать цифры.
Сначала разделяйте маршруты
Для большинства работ по оптимизации React-бандла самый быстрый эффект даёт разделение по маршрутам. Оно сокращает объём JavaScript, который браузер должен скачать до того, как страница станет полезной. И для этого не нужен полный передел.
Начните с маршрутов, которые открывают немногие. Админ-экраны, отчёты, страницы оплаты, журналы аудита и виды с большим количеством графиков часто тянут крупные библиотеки, но в обычной сессии большинство посетителей до них даже не доходит. Если такие маршруты лежат в первом bundle, каждый пользователь платит за код, который ему не нужен.
Простое правило помогает: оставьте быстрым основной путь, а редкий путь отправьте за ленивую загрузку. Во многих приложениях это значит, что сначала вы загружаете основную часть продукта, а такие страницы подгружаете только по запросу:
- админ-панели
- экраны отчётов
- продвинутые графики
- инструменты экспорта
- страницы настроек с множеством вкладок
К общему коду оболочки нужно относиться так же строго. App shell должен содержать только то, что есть почти на каждой странице: навигацию, базовую тему и простое состояние. Команды часто по привычке кладут в верхний уровень библиотеки графиков, редакторы или крупные пакеты-помощники, потому что так удобно. Но это быстро становится дорогим решением.
Логика загрузки данных должна следовать тому же принципу. Когда пользователь открывает маршрут, подгружайте только те данные, которые нужны именно сейчас. Не вытягивайте данные отчёта на дашборде только потому, что страница отчёта находится в одном клике. Код и данные обычно разрастаются вместе, поэтому чинить нужно и то и другое.
При этом пользовательский опыт должен оставаться плавным. Страница с ленивой загрузкой и пустым экраном выглядит сломанной, даже если бандл стал меньше. Оставляйте видимой структуру страницы, показывайте понятное состояние загрузки и делайте ожидание привязанным к конкретной странице. Небольшой скелетон для области графика ощущается намного лучше, чем спиннер на весь экран.
У растущего SaaS-продукта этот паттерн часто виден особенно хорошо. Главная панель остаётся быстрой, а аналитический маршрут загружает пакет графиков только тогда, когда кто-то его открывает. Админ-раздел работает так же. Пользователи, которые туда не заходят, перестают скачивать этот лишний вес, и приложение уже в первый день ощущается легче.
Проверяйте зависимости как список расходов
Каждый пакет добавляет вес, работу по обновлению и риск. В оптимизации React-бандла одни из самых простых побед приходят от кода, который можно убрать, заменить или подгружать позже.
Небольшие пакеты часто накапливаются по одному исправлению за раз. Помощник для дат, пакет для кнопок копирования, библиотека модалок, редактор богатого текста, набор иконок, пакет для графиков, который используется на одном экране. Каждый по отдельности кажется недорогим. Вместе они могут замедлить первую загрузку сильнее, чем ожидает большинство команд.
Относитесь к зависимостям как к строкам бюджета, а не как к бесплатным дополнениям. Когда пакет попадает в приложение, задайте простой вопрос: сколько это стоит в JavaScript, CSS и будущих обновлениях?
- Решает ли этот пакет проблему, достаточно большую, чтобы оправдать его размер?
- Используется ли он везде или только на одной странице?
- Может ли браузер справиться без него?
- Есть ли более лёгкий пакет, который сделает то же самое?
- Не оставило ли обновление две версии в сборке?
Худшие нарушители — это часто тяжёлые пакеты, используемые в лёгких местах. Классический пример — полная библиотека графиков для одной небольшой админ-страницы. Если эту страницу открывают только несколько пользователей, загружайте её только на этом маршруте, а не отправляйте всем подряд. То же правило относится к редакторам, картам, просмотрщикам PDF и анимационным пакетам.
Важно и то, как вы импортируете код. Если вы тянете всю библиотеку ради одной функции, бандл расплачивается за это решение. Форматирование дат, глубокое копирование, debounce-помощники и небольшие математические утилиты часто попадают в приложение именно так. Во многих случаях достаточно импорта одной функции или небольшой локальной утилиты.
После обновлений проверяйте дубликаты пакетов. Команды часто получают две версии одной и той же библиотеки дат, формы или набора иконок, потому что одна часть приложения обновилась раньше, а другая отстала. Такой расход легко пропустить, пока вы не посмотрите на финальный бандл.
Не забывайте и про dev-помощники. Моковые данные, тестовые утилиты, отладочные логгеры, код Storybook и скрипты только для локальной работы должны оставаться только в среде разработки. Один случайный импорт может притащить лишний код в живое приложение.
У растущего продукта таким образом можно убрать 100 КБ и больше, не трогая дизайн. Один аккуратный аудит зависимостей раз в квартал часто экономит больше времени загрузки, чем неделя мелких правок кода.
Задайте понятные правила для изображений
Оптимизация React-бандла часто начинается с JavaScript, но правила для изображений могут дать реальное ускорение ещё быстрее. Обычно команды теряют контроль маленькими шагами. Большой баннер начинают использовать в маленькой карточке, фотографии товаров отправляют в полном размере камеры, и никто не замечает, что мобильные страницы становятся медленными.
Хорошая политика должна помещаться на один экран и оставлять мало места для споров. Если людям нужно каждый раз отдельно спрашивать про загрузку, правило слишком сложное.
Используйте ограничения, которые соответствуют тому, как приложение реально показывает изображения:
- Изображения карточек: до 1200 px по ширине, цель — 100–150 КБ
- Баннеры: до 1920 px по ширине, цель — 250–350 КБ
- Визуалы на весь экран: только настолько большие, насколько нужно макету, обычно меньше 500 КБ
- Фотографии: используйте WebP или AVIF, если они хорошо выглядят в меньшем размере
- Изображения ниже первого экрана: подгружайте лениво по умолчанию
Самая большая трата — это несоответствие размера. Если карточка отображает картинку шириной 320 px, отправлять файл в 2400 px просто бессмысленно. Браузер всё равно заплатит за эту загрузку, даже если пользователь никогда не увидит лишние детали.
Держите иконки и фотографии под разными правилами. Иконки должны быть маленькими и предсказуемыми. SVG хорошо подходит для большинства интерфейсных иконок, потому что остаётся чётким и часто весит меньше bitmap-файла. Фотографиям нужен другой подход, потому что там важнее детали, сжатие и кадрирование.
Небольшой пример помогает это почувствовать. Допустим, в сетке товаров 12 карточек. Если каждая карточка загружает изображение весом 400 КБ, одна только эта сетка добавляет почти 5 МБ. Если уменьшить каждую картинку до реального размера отображения и сжать её до 120 КБ, тот же блок падает примерно до 1,4 МБ. Пользователи чувствуют такую разницу сразу.
Запишите правила простыми словами, разместите их рядом с процессом загрузки и один раз разберите исключения. Это экономит больше времени, чем споры об изображениях на каждом спринте.
Простой пример из растущего React-продукта
Типичный случай выглядит так: небольшой SaaS-дашборд начинает просто, а потом обрастает графиками, фильтрами, экспортом, подсказками для онбординга и несколькими общими помощниками. Через год тяжёлым кажется даже экран входа. Пользователи ждут, прежде чем могут начать печатать, а первый экран появляется позже, чем должен.
В одной команде была панель, где код для графиков загружался на каждом маршруте. В начале это казалось логичным, потому что графики были частью основного продукта. Потом тот же бандл стал отправляться ещё и на страницу аккаунта, в настройки и на обычную маркетинговую страницу. Эти экраны вообще не нуждались в библиотеках для графиков, но пользователи всё равно платили за них.
В том же приложении был общий helper для форм, который импортировал большую библиотеку утилит ради двух маленьких функций. Сначала этого никто не заметил. Это был просто один helper. Потом им начали пользоваться всё новые формы, и лишний вес распространился по всему приложению. Обычно именно так и выглядит оптимизация React-бандла: это не магия, а уборка.
У маркетинговой страницы была своя проблема. Hero-изображение выглядело красиво, но было намного больше, чем требовал макет. Телефоны скачивали огромный файл только для того, чтобы показать его в намного меньшем блоке. На медленных соединениях это отодвигало первый видимый экран ещё на секунду или две.
Решение не потребовало переписывания с нуля. Команда разделила маршруты так, чтобы код графиков загружался только на страницах дашборда. Они заменили широкий импорт утилит на небольшие прямые импорты, а потом убрали один пакет, который дублировал встроенные возможности браузера. Ещё они уменьшили hero-изображение, экспортировали современные форматы и ввели простое правило: ни одно изображение не попадает в продукт, пока кто-то не знает его размер на экране.
После этого простые страницы стали заметно легче:
- экран входа и настройки перестали скачивать код графиков
- общий helper для форм перестал тянуть лишние утилиты
- маркетинговая страница стала отправлять более маленький hero-баннер
- первый экран начал появляться раньше на мобильных устройствах
Вот и есть практическая версия разбиения маршрутов, которое чаще всего нужно React-приложениям. Убирайте тяжёлый код с простых страниц, чистите импорты, которые тихо расползаются, и относитесь к слишком большим изображениям как к любой другой проблеме производительности.
Ошибки, которые сводят работу на нет
Оптимизация React-бандла часто стопорится потому, что команда чистит бандл, а через неделю снова добавляет тот же вес. Обычно проблема не в одном плохом решении. Она в наборе привычек, которые по отдельности кажутся безобидными.
Одна из частых ошибок — чрезмерное дробление. Если лениво загружать каждый маленький виджет, панель иконок или поле формы, вы меняете один большой бандл на путаный поток мелких запросов и лишних состояний загрузки. Делите по маршрутам или по крупным функциям. Оставляйте маленький общий UI вместе, если он действительно не нужен редко.
Команды также по привычке оставляют тяжёлые провайдеры в корне приложения. Редактор богатого текста, настройка графиков, состояние админки или клиент feature flags часто лежат слишком близко к верхнему уровню просто потому, что так аккуратно выглядит. Но тогда за этот код платит каждая страница, хотя он нужен только нескольким маршрутам.
Старые и новые пакеты, живущие бок о бок, могут незаметно свести на нет недели уборки. Это случается во время спешных миграций: одна команда добавляет date-fns, но не удаляет Moment, или новая библиотека графиков появляется, пока старая ещё обслуживает один забытый отчёт. Приложение продолжает работать, поэтому никто не замечает цену, пока время загрузки снова не поползёт вверх.
Правила для изображений ломаются по той же причине. Разработчики сжимают локальные ассеты, а загрузки из CMS проходят без изменений. Один слишком большой PNG от контент-команды может навредить первому экрану сильнее, чем поможет маленькая правка JavaScript. Задайте ограничения по размеру файла, ширине и формату, а потом настройте CMS так, чтобы она отклоняла или преобразовывала файлы, которые не подходят под правила.
Проверка только на быстром офисном интернете создаёт ложное ощущение прогресса. Страница, которая отлично ведёт себя на оптоволокне и новом ноутбуке, может тянуться на 4G или на дешёвом Android-телефоне. Замедляйте и сеть, и процессор. Иначе вы пропустите задержки из-за инициализации, декодирования изображений и слишком большого количества фрагментов.
У растущего продукта это особенно легко не заметить. Команда может разделить десятки маленьких компонентов и чувствовать себя продуктивной, а один провайдер аналитики в корне и две библиотеки дат всё ещё будут держать приложение тяжёлым. Поэтому работа над бандлом требует простого ритуала перед релизом:
- проверяйте дубликаты пакетов после каждой миграции
- задавайте вопросы каждому новому провайдеру на верхнем уровне
- просматривайте изображения из CMS перед публикацией
- тестируйте один медленный сетевой профиль и одно более слабое устройство
Большинство регрессий в бандле не требуют переписывания. Они появляются из обычных решений, к которым никто не возвращается, когда приложение начинает расти.
Короткая проверка перед каждым релизом
Большинство проблем с бандлом не приходят как один большой сбой. Они просачиваются через одну вспомогательную функцию, один новый пакет, один импорт маршрута или одно слишком большое изображение. Короткая проверка перед релизом помогает поймать это до того, как пользователи почувствуют проблему.
Используйте один и тот же быстрый ритуал каждый раз, особенно если приложение росло по функциям месяцами или годами.
- Открывайте основные маршруты в медленном мобильном профиле, а не на быстром ноутбучном соединении. Если первый экран кажется тяжёлым ещё до появления данных, начальный JavaScript, вероятно, слишком большой.
- Просматривайте отчёт по бандлу и сравнивайте его с предыдущим релизом. Если новый пакет добавил заметный кусок кода ради небольшой функции, остановитесь и спросите, нужен ли вам весь этот код.
- Проверяйте ленивые маршруты из чистой сессии. Их код должен оставаться вне первой загрузки, пока кто-то действительно не откроет эту страницу.
- Сопоставляйте размер изображений с тем, как они выглядят на экране. Если карточка показывает картинку шириной 400 px, отправлять огромный исходный файл — просто расточительство.
- Проверяйте хотя бы одну новую зависимость перед выпуском. Многие пакеты выглядят крошечными в запросе на изменение, а потом обходятся дорого, когда попадают в бандл.
Последняя проверка важнее, чем кажется командам. Библиотека дат, пакет графиков или редактор могут изменить время загрузки быстрее, чем неделя аккуратной уборки это компенсирует. Если можно, импортируйте только то, что используете, замените пакет на более лёгкий или перенесите функцию за ленивый маршрут.
Эта привычка не выглядит эффектно, но она работает. Хорошая оптимизация React-бандла часто строится на маленьких проверках перед каждым релизом, а не на одном драматичном переписывании. Пользователи замечают разницу на обычном телефоне в течение секунды-двух — именно там эта работа и окупается.
Что делать дальше
Начните с малого и назначьте срок. Если пытаться исправить всё приложение сразу, работа снова растянется на квартал.
Выберите на этой неделе один медленный маршрут и одну группу слишком больших изображений. Так у команды будет узкая цель, и результаты обычно появляются достаточно быстро, чтобы производительность перестали считать побочной задачей.
Простой план работает лучше, чем длинный список желаний:
- Выберите маршрут, который реальные пользователи открывают чаще всего, или маршрут, который кажется медленным даже при хорошем соединении. Разделите его, уберите рядом с ним мёртвый код и измерьте изменения.
- Найдите одну группу изображений, которая тащит слишком много веса, например карточки товаров, обложки блога или скриншоты дашборда. Задайте правила по размеру, формату и габаритам, а потом сначала примените их именно к этой группе.
- Добавьте лимиты на бандл в чек-лист релиза. Если часть маршрута или общий бандл вырастает выше лимита, кто-то должен объяснить почему до выпуска.
- Назначьте одного человека, ответственного за аудит зависимостей. Когда за это никто не отвечает, вспомогательные пакеты просто копятся и остаются на месте.
- Привлекайте внешнюю помощь, если уборка всё время откладывается. Внештатный CTO вроде Oleg Sotnikov может проверить архитектуру React, выбор зависимостей и привычки поставки без необходимости полного переписывания.
Именно здесь оптимизация React-бандла перестаёт быть разовой уборкой и становится частью того, как команда выпускает продукт. Короткий чек-лист, один ответственный и одна еженедельная цель могут сократить время загрузки сильнее, чем большое переписывание, которое так и не началось.
Если вы сделаете только одно действие прямо сейчас, пусть оно будет заметным. Добавьте медленный маршрут, исправление изображений и лимит бандла в следующий спринт. Когда работа над производительностью лежит в документе, команды её игнорируют. Когда она встроена в процесс релиза, команды её выполняют.