Electron Bundle Optimization: Medal Case Study 40MB to 2.7MB
Case Study · July 2026

Electron Bundle Optimization:
How Medal Slashed 40MB to 2.7MB

A technical deep-dive into Medal Engineering's journey from a 40MB+ Electron renderer bundle to just 2.7MB — using Vite, Rolldown, code splitting, and aggressive dead-code elimination. Real numbers, real code, and actionable techniques for your Electron app.

Oleg Maximov July 9, 2026 10 min read

The Problem: 40MB+ Renderer Bundle

Medal is a desktop application built on Electron that lets gamers clip, edit, and share gameplay moments. When Rick Zhang joined the Medal Frontend team in 2024, the Electron renderer bundle was over 40MB. That's the JavaScript — not assets, not native modules — just the compiled frontend code that every user downloads and parses on every app launch.

A 40MB bundle means slow startup, delayed time-to-interactive, and a poor user experience on mid-range hardware. It also blocks feature iteration: every change required a full build and the team couldn't even see their code propagate to the screen during local development.

The team's CEO, Ken Colton, made the first decisive move: switch the local build to Vite for Hot Module Replacement. This was, in Rick's words, "the moment we discovered fire."

The Strategy: Macro First, Then Micro

Rather than tackling individual bloated components head-on, Rick approached the problem from the outside in: macro, then micro. First fix the developer experience and build tooling, then delete dead code, then optimize what remains.

Step 1: PNPM Monorepo + Vite

Medal migrated from a multi-repo setup with versioned component libraries to a PNPM monorepo. This eliminated version drift, merge conflicts, and the nightmare of knowing "what was shipped and when." Everything — components, hooks, utils, types — went into one repository with clear package boundaries.

With Vite powering local development, the team got instant HMR. Changes appeared on screen in milliseconds instead of tens of seconds. This was the foundation that made every subsequent optimization possible.

Step 2: Dead Code Elimination with AI Assistance

The team systematically identified and removed dead code. Rick used AI to correct import paths at scale and ensure every import was explicit — specifying the package, clear path, and file extension. This made it possible for tools to find truly dead code with confidence.

// Before: barrel imports that bundle everything
import { Button, Input, Modal, Dropdown } from '@/components';

// After: direct imports with file extensions
import { Button } from '@/components/Button/Button.tsx';
import { Input } from '@/components/Input/Input.tsx';

Direct paths and explicit file extensions let tree-shaking work properly. Relative imports remained, but barrel files were eliminated as a policy. Code consolidation across Electron and web (client-agnostic packages for utils, hooks, types) further reduced duplication.

Step 3: Vite in Production

Medal had been using Vite for local development but Rollup for production builds — two build systems, two configurations, two sources of truth. Unifying on Vite meant one build pipeline for both dev and production. More importantly, Vite's Glob Imports made code splitting dramatically easier.

The Results: Every Optimization, Byte by Byte

-2.6 MB
Barrel File Removal
Replaced re-export indexes with direct imports
-3.4 MB
Externalized .wav Assets
Sound files loaded on demand, not bundled
-4 MB
ESM Tree-Shaking
Preferring ESM over CJS for better dead code elimination
-13 MB
Dynamic i18n Imports
Translation files loaded only for the active locale
-5.4 MB
Route Code Splitting
Each route loads only its own dependencies
22 MB → 2.7 MB
Rolldown Migration
Rust-based bundler as a drop-in Rollup replacement

1. Removing Barrel Files: -2.6 MB

Barrel files are index.ts files that re-export everything from a directory. They seem convenient but they completely defeat tree-shaking: when you write import { Button } from '@/components', the bundler doesn't know which of the dozens of re-exported modules you'll actually use, so it bundles them all.

// Typical barrel file — bundles everything
export { Button } from './Button';
export { Input } from './Input';  
export { Modal } from './Modal';
export { Dropdown } from './Dropdown';
export { Tooltip } from './Tooltip';
// ... 30+ more exports

Medal adopted a strict "death to barrel files" policy. Every import must specify the exact file path. This alone saved 2.6 MB — more than most small applications' total bundle sizes.

2. Externalizing .wav Assets: -3.4 MB

Sound alert files (SoundAlertsData) in .wav format were being bundled into the renderer. Medal externalized these files — loaded them at runtime instead of inlining them into the JavaScript bundle. This saved 3.4 MB without any code changes to the audio playback logic.

// Vite config — externalize asset files
export default defineConfig({
  build: {
    rollupOptions: {
      external: [/\.wav$/],
    },
  },
});

3. ESM for Better Tree-Shaking: -4 MB

Rollup (and Rolldown) can tree-shake ESM imports much more effectively than CommonJS. CJS require() is dynamic and opaque — the bundler cannot statically analyze what's being used. ESM import declarations are static and analyzable. Medal's switch to ESM throughout the codebase unlocked an additional 4 MB in savings.

// CJS — bundler can't tree-shake
const lodash = require('lodash');

// ESM — bundler can eliminate unused exports
import { debounce } from 'lodash-es';

4. Dynamic i18n Imports: -13 MB

This was the single biggest win. Medal was bundling all translation files for every supported locale into the renderer. Users who only speak English were downloading French, German, Spanish, and a dozen other locale files.

import { defineAsyncComponent } from 'vue';

// Dynamic import — load translations only for the active locale
async function loadTranslations(locale: string) {
  const messages = await import(`./locales/${locale}.json`);
  i18n.global.setLocaleMessage(locale, messages.default);
}

Switching from static imports to dynamic import() for i18n files saved 13 MB — nearly a third of the original bundle. Each user now downloads only their locale's strings. This is a technique every multi-language application should adopt.

5. Route-Based Code Splitting: -5.4 MB

Medal migrated from React Router v5 to v7 to enable proper route-level code splitting. With React Router v7's lazy route support, each page loads only its own components, hooks, and dependencies — nothing from other pages leaks into the initial bundle.

import { createBrowserRouter } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    lazy: () => import('./pages/Home'),  // lazy-loaded
  },
  {
    path: '/clips',
    lazy: () => import('./pages/Clips'),  // separate chunk
  },
  {
    path: '/settings',
    lazy: () => import('./pages/Settings'),  // separate chunk
  },
]);

The React Router v5 → v7 migration was a major refactor — it touched nearly every file in the routing layer — but it was the prerequisite for route-level splitting. Combined with Vite's automatic chunk splitting, this saved 5.4 MB.

6. Rolldown: 22 MB → 2.7 MB

With the bundle down to ~22 MB from these optimizations, Rick set his sights on Rolldown — a Rust-based bundler designed as a drop-in replacement for Rollup. Rolldown offers dramatically faster builds and better tree-shaking through its native compilation.

The migration wasn't plug-and-play. Some dependencies conflicted with Rolldown's bundling system and had to be cleaned up first. But after that cleanup, the switch to Rolldown took the renderer bundle from 22 MB down to 2.7 MB — an 88% reduction on top of all previous optimizations.

// rolldown.config.js
import { defineConfig } from 'rolldown';

export default defineConfig({
  input: 'src/main.tsx',
  output: {
    format: 'esm',
    dir: 'dist/renderer',
  },
  // Rolldown automatically tree-shakes better than Rollup
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
  },
});

The Final Stack

After the optimization journey, Medal's frontend toolchain looks like this:

Vite

Local development with HMR

🦀

Rolldown

Production bundling (Rust)

📦

PNPM

Monorepo management

🧪

Vitest

Unit and integration tests

🎨

Tailwind + Base UI

Component library (migrated from Grommet)

🔄

React Router v7

Lazy route-based code splitting

What This Means for Your Electron App

Medal's journey is more than a feel-good story — every technique they used is applicable to existing Electron applications. Here's a practical checklist for your own bundle audit:

  1. Measure first. Run a bundle analysis (vite build --analyze or source-map-explorer) to find the biggest contributors.
  2. Kill barrel files. Audit your index.ts files and replace barrel imports with direct file imports.
  3. Externalize assets. Move large static files (audio, video, fonts) out of the bundle and load them at runtime.
  4. Go ESM-only. Ensure your dependencies are ESM-compatible. Drop CJS packages where alternatives exist.
  5. Lazy-load i18n. If your app supports multiple languages, dynamic-import locale files. This is almost always the biggest easy win.
  6. Split by route. Migrate to lazy routes. Every route should load its own chunk.
  7. Evaluate Rolldown. If you're on Rollup, Rolldown is a drop-in replacement that may halve your bundle at no code cost.

For a broader overview of how modern build tooling is evolving, see my hands-on guide to Vite+ Beta — the unified toolchain that integrates Vite, Vitest, Oxlint, and Rolldown into a single CLI. And for Electron-specific performance features coming to the platform itself, read about Electron 43 beta: startup improvements, caching, and LTO optimizations.

FAQ

What techniques did Medal use to reduce their Electron bundle from 40MB to 2.7MB?
Medal applied six major optimizations: removing barrel files (-2.6MB), externalizing .wav assets (-3.4MB), switching to ESM for better tree-shaking (-4MB), dynamic i18n imports (-13MB), route-based code splitting (-5.4MB), and switching from Rollup to Rolldown (22MB down to 2.7MB). The total reduction was over 93%.
How did Vite help Medal optimize their Electron app?
Vite provided Hot Module Replacement for faster local development and enabled easier code splitting through Glob Imports. Switching to Vite for production builds unified the build pipeline, making it possible to split code by route, by library, and by file type using Vite's built-in support. Vite's ESM-native dev server also encouraged the team to adopt ESM patterns, which improved tree-shaking downstream.
What is Rolldown and how did it help reduce bundle size?
Rolldown is a Rust-based bundler designed as a drop-in replacement for Rollup. It offers better tree-shaking through native compilation and smaller output bundles. Medal switched to Rolldown after cleaning up dependencies that conflicted with its bundling system. The switch alone took their renderer bundle from 22MB to 2.7MB, plus gave them significantly faster build times.
What are barrel files and why are they bad for bundle size?
Barrel files are index files that re-export modules from many sources (e.g., components/index.ts that re-exports every component). They defeat tree-shaking because bundlers cannot statically determine which specific exports are used — they include everything. Medal removed barrel files to save 2.6MB and adopted a strict no-barrel-files policy throughout the codebase.
How much did dynamic i18n imports save in bundle size?
Dynamic i18n imports saved 13MB — the single largest reduction. Instead of bundling all translation files upfront, Medal loaded only the active locale's strings at runtime. English users no longer downloaded French, German, Spanish, or other locale files. This technique is applicable to any multi-language application and is almost always the biggest easy win for bundle reduction.
Can these Electron optimization techniques be applied to any Electron app?
Yes — most of Medal's techniques apply universally: remove barrel files, prefer ESM for better tree-shaking, externalize assets and large files, use dynamic imports for i18n and optional libraries, implement route-based code splitting, and evaluate switching to Rolldown for production builds. These techniques work with any modern Electron + Vite or Electron + Rolldown setup.
What build tools does Medal use after the optimization?
Medal now uses Vite for local development and Rolldown for production builds, both in a PNPM monorepo. Tests run on Vitest. The UI stack migrated from Grommet + Styled Components to Tailwind and Shadcn/Radix UI, then further to Base UI — each migration reducing dependency weight and improving build performance.

Source: "W-Key in Frontend: Synergizing Technology and Product" by Rick Zhang, Medal Engineering (June 23, 2026). Featured in JS Weekly #793 (July 7, 2026).

Contact

Need help optimizing your Electron app?

I help teams reduce bundle size, improve startup performance, and modernize their Electron build toolchain. Free initial consultation.