Технический разбор того, как команда Medal Engineering сократила бандл Electron-приложения с 40+MB до 2.7MB — с помощью Vite, Rolldown, code splitting и агрессивного удаления мёртвого кода. Реальные цифры, код и применимые техники для вашего Electron-проекта.
Medal — это десктопное приложение на Electron, позволяющее геймерам записывать, редактировать и делиться игровыми моментами. Когда Рик Чжан присоединился к фронтенд-команде Medal в 2024 году, бандл рендерера Electron весил более 40MB. Речь идёт только о JavaScript — не об ассетах, не о нативных модулях — а о скомпилированном фронтенд-коде, который каждый пользователь загружает и парсит при каждом запуске.
Бандл в 40MB означает медленный запуск, задержку time-to-interactive и плохой пользовательский опыт на среднебюджетном железе. К тому же это блокировало итерацию функций — каждое изменение требовало полной сборки, и команда не могла увидеть свой код на экране в процессе локальной разработки.
Генеральный директор Кен Колтон сделал первый решительный шаг: переключить локальную сборку на Vite для Hot Module Replacement. По словам Рика, это был «момент, когда мы открыли огонь».
Вместо того чтобы атаковать раздутые компоненты в лоб, Рик подошёл к проблеме извне: сначала макро, потом микро. Сначала исправить developer experience и инструменты сборки, потом удалить мёртвый код, затем оптимизировать то, что осталось.
Medal мигрировала с мульти-репозитория с версионированными библиотеками компонентов на PNPM монорепозиторий. Это устранило расхождение версий, конфликты слияния и проблему «что и когда было опубликовано». Всё — компоненты, хуки, утилиты, типы — оказалось в одном репозитории с чёткими границами пакетов.
С Vite в локальной разработке команда получила мгновенный HMR. Изменения появлялись на экране за миллисекунды вместо десятков секунд. Это был фундамент, сделавший возможными все последующие оптимизации.
Команда систематически выявляла и удаляла неиспользуемый код. Рик использовал AI для массового исправления путей импорта, гарантируя, что каждый импорт явно указывает пакет, путь и расширение файла. Это позволило инструментам находить действительно мёртвый код с уверенностью.
// До: barrel-импорт, который собирает всё
import { Button, Input, Modal, Dropdown } from '@/components';
// После: прямые импорты с расширениями файлов
import { Button } from '@/components/Button/Button.tsx';
import { Input } from '@/components/Input/Input.tsx';
Прямые пути и явные расширения файлов позволили tree-shaking'у работать эффективно. Относительные импорты остались, но barrel-файлы были запрещены как политика. Консолидация кода между Electron и вебом (клиент-агностичные пакеты утилит, хуков, типов) дополнительно сократила дублирование.
Medal использовала Vite для локальной разработки, но Rollup для продакшен-сборок — две системы сборки, две конфигурации, две правды. Унификация на Vite означала один пайплайн и для разработки, и для продакшена. Что важнее, Glob Imports Vite сделали code splitting значительно проще.
Barrel-файлы — это index.ts, которые реэкспортируют всё из директории.
Они удобны, но полностью блокируют tree-shaking: когда вы пишете
import { Button } from '@/components', бандлер не знает, какие из десятков
реэкспортированных модулей вы будете использовать, и включает все.
// Типичный barrel-файл — собирает всё
export { Button } from './Button';
export { Input } from './Input';
export { Modal } from './Modal';
export { Dropdown } from './Dropdown';
export { Tooltip } from './Tooltip';
// ... ещё 30+ экспортов
Medal ввела строгую политику «смерть barrel-файлам». Каждый импорт должен указывать точный путь к файлу. Это само по себе сэкономило 2.6 MB — больше, чем полный размер многих небольших приложений.
Звуковые файлы SoundAlertsData в формате .wav встраивались в бандл рендерера. Medal вынесла эти файлы — теперь они загружаются в рантайме, а не встраиваются в JavaScript. Это сэкономило 3.4 MB без каких-либо изменений в логике воспроизведения.
// Конфиг Vite — выносим ассеты
export default defineConfig({
build: {
rollupOptions: {
external: [/\.wav$/],
},
},
});
Rollup (и Rolldown) могут эффективно tree-shake'ить ESM-импорты в отличие от CommonJS.
CJS require() динамический и непрозрачный — бандлер не может статически
проанализировать, что используется. ESM-декларации import статичны и
анализируемы. Переход Medal на ESM по всему коду дал дополнительные 4 MB экономии.
// CJS — бандлер не может tree-shake'ить
const lodash = require('lodash');
// ESM — бандлер может удалить неиспользуемые экспорты
import { debounce } from 'lodash-es';
Это была самая крупная победа. Medal встраивала все файлы переводов для каждого поддерживаемого языка в бандл. Пользователи, говорящие только по-русски, загружали французские, немецкие, испанские и дюжину других локалей.
// Динамический импорт — загружаем переводы только для активной локали
async function loadTranslations(locale: string) {
const messages = await import(`./locales/${locale}.json`);
i18n.global.setLocaleMessage(locale, messages.default);
}
Замена статических импортов на динамические import() для файлов i18n
сэкономила 13 MB — почти треть исходного бандла. Теперь каждый
пользователь загружает только строки своего языка. Это техника, которую стоит внедрить
любому многоязычному приложению.
Medal мигрировала с React Router v5 на v7 для поддержки ленивой загрузки маршрутов. С React Router v7 каждая страница загружает только свои компоненты, хуки и зависимости — ничего из других страниц не попадает в начальный бандл.
import { createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
lazy: () => import('./pages/Home'), // ленивая загрузка
},
{
path: '/clips',
lazy: () => import('./pages/Clips'), // отдельный чанк
},
{
path: '/settings',
lazy: () => import('./pages/Settings'), // отдельный чанк
},
]);
Миграция с React Router v5 на v7 была масштабным рефакторингом — она затронула почти каждый файл в слое маршрутизации — но стала обязательным условием для разделения по маршрутам. В сочетании с автоматическим разделением чанков Vite это сэкономило 5.4 MB.
Когда бандл сократился до ~22 MB, Рик сосредоточился на Rolldown — бандлере на Rust, спроектированном как замена Rollup. Rolldown обеспечивает значительно более быструю сборку и лучший tree-shaking благодаря нативной компиляции.
Миграция не была безболезненной. Некоторые зависимости конфликтовали с системой сборки Rolldown и их пришлось предварительно удалить. Но после этой очистки переход на Rolldown сократил бандл с 22 MB до 2.7 MB — на 88% поверх всех предыдущих оптимизаций.
// rolldown.config.js
import { defineConfig } from 'rolldown';
export default defineConfig({
input: 'src/main.tsx',
output: {
format: 'esm',
dir: 'dist/renderer',
},
// Rolldown автоматически tree-shake'ит лучше, чем Rollup
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
});
После оптимизации тулчейн Medal выглядит так:
Локальная разработка с HMR
Продакшен-сборка (Rust)
Монорепозиторий
Юнит и интеграционные тесты
Библиотека компонентов (миграция с Grommet)
Ленивая загрузка маршрутов
Путь Medal — это не просто вдохновляющая история. Каждая техника применима к существующим Electron-проектам. Вот практический чеклист для аудита вашего бандла:
vite build --analyze
или source-map-explorer), чтобы найти крупнейших потребителей.index.ts и замените
barrel-импорты прямыми импортами файлов.Для общего обзора того, как развиваются современные инструменты сборки, смотрите моё практическое руководство по Vite+ Beta — единому тулчейну, объединяющему Vite, Vitest, Oxlint и Rolldown в один CLI. А о новых возможностях производительности самой платформы Electron читайте в Electron 43 beta: улучшения запуска, кэширования и LTO.
Источник: "W-Key in Frontend: Synergizing Technology and Product" — Рик Чжан, Medal Engineering (23 июня 2026). Опубликовано в JS Weekly #793 (7 июля 2026).
Помогаю командам уменьшить размер бандла, улучшить скорость запуска и модернизировать сборку Electron. Бесплатная консультация.