Chrome Extension Size Optimization — Developer Guide
9 min readExtension Size Optimization
Learn how to reduce your Chrome extension’s bundle size for faster installs, better performance, and smoother Chrome Web Store reviews.
Overview
Smaller extensions offer tangible benefits beyond just saving disk space:
- Faster installations: Users can install your extension more quickly, especially on slower connections
- Reduced memory footprint: Smaller bundles use less memory when loaded
- Faster updates: Update propagation is quicker when the delta is smaller
- Better CWS review: Google may scrutinize large extensions more closely during review
Target bundle sizes: background/service worker under 100KB, content scripts under 50KB.
Measuring Size
Before optimizing, you need accurate measurements of your extension’s size.
Checking Package Size
# Build your extension, then check the CRX/ZIP size
npm run build
ls -lh dist/*.zip # or *.crx depending on your build
Bundle Analysis
Use visualization tools to understand what’s contributing to your bundle size:
# Webpack bundle analyzer
npx webpack-bundle-analyzer dist/manifest.json
# Or for esbuild
npx esbuild-visualizer --metadata=dist/.esbuild-meta.json --filename=dist/visualizer.html
These tools generate treemaps showing which dependencies and modules contribute most to size.
Chrome DevTools
Use Chrome DevTools to see actual loaded file sizes:
- Load your extension in
chrome://extensions - Enable Developer mode
- Click on your extension’s service worker or background page
- Open DevTools and check the Sources panel for file sizes
Tree Shaking
Tree shaking removes unused code from your final bundle, dramatically reducing size.
Enable ES Modules
Use ES modules with import/export for effective tree shaking:
// GOOD: Named imports enable tree shaking
import { debounce, throttle } from './utils.js';
// GOOD: Selective imports
import { formatDate } from 'date-fns';
// BAD: Namespace import prevents tree shaking
import * as utils from './utils.js';
// BAD: CommonJS prevents tree shaking
const { debounce } = require('./utils.js');
Build Tool Configuration
Modern bundlers like esbuild, webpack, and Vite support tree shaking automatically when you use ES modules:
// esbuild.config.js
export default {
bundle: true,
format: 'esm', // Use ES modules
treeShaking: true, // Explicitly enable (default in esbuild)
minifyWhitespace: true,
minifyIdentifiers: true,
minifySyntax: true,
};
Avoid CommonJS (require(), module.exports) as it prevents effective tree shaking.
Code Splitting
Code splitting separates your bundle into smaller chunks that load on-demand.
Separate Entry Points
Each extension context should have its own entry point:
// webpack.config.js - separate entry points
module.exports = {
entry: {
background: './src/background.ts',
popup: './src/popup.tsx',
options: './src/options.tsx',
content: './src/content.ts',
},
};
This ensures the popup doesn’t load content script code, and vice versa.
Dynamic Imports
Load features on-demand rather than at startup:
// BAD: Load everything at startup
import { HeavyChart } from './charts.js';
import { Analytics } from './analytics.js';
// GOOD: Load only when needed
async function showChart() {
const { HeavyChart } = await import('./charts.js');
return new HeavyChart(container);
}
// Click handler loads the module on-demand
button.addEventListener('click', showChart);
Lazy Load Extension Pages
For options pages or rarely-used features, use dynamic imports:
// content-script.ts - lazy load heavy feature
if (userClicksAdvancedFeature) {
const { initAdvancedFeature } = await import('./advanced-feature.js');
initAdvancedFeature();
}
Dependency Management
Dependencies often form the largest portion of extension bundles.
Audit Dependencies
Regularly review what you’re importing:
# List installed packages
npm list --depth=0
# Check for unused packages
npx depcruise --validate . | grep "unused"
Replace Heavy Libraries
Many popular libraries have lighter alternatives:
| Heavy Library | Lighter Alternative |
|---|---|
| moment.js | date-fns or dayjs |
| lodash | Native JS or lodash-es |
| axios | fetch (native) or ky |
| underscore | Native JS |
// GOOD: date-fns tree-shakes to ~2KB per function
import { format, parseISO } from 'date-fns';
// GOOD: lodash-es supports tree shaking
import { debounce, throttle } from 'lodash-es';
// BAD: moment.js is ~300KB minified
import moment from 'moment';
Prefer No-Dependency Solutions
For simple tasks, write your own utilities:
// Instead of importing a library for simple tasks
// GOOD: Simple debounce implementation
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
Bundle Dependencies
Don’t use CDN links for dependencies. Bundle everything:
<!-- BAD: External CDN (fails offline, adds latency) -->
<script src="https://cdn.example.com/library.js"></script>
<!-- GOOD: Bundled -->
<script src="library.bundle.js"></script>
Asset Optimization
Images and other assets can significantly impact extension size.
Image Compression
# Convert PNG to WebP (smaller file size)
cwebp input.png -o output.webp
# Or use sharp for batch processing
npx sharp -i "icons/*.png" -o "icons/" -f webp
Use SVG Icons
SVG icons are scalable and typically smaller than PNG equivalents:
<!-- GOOD: SVG scales to any size, ~500 bytes -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M8 0L0 8l8 8 8-8z"/>
</svg>
<!-- BAD: PNG at multiple sizes -->
<img src="icon16.png" width="16" height="16">
<img src="icon32.png" width="32" height="32">
Include Only Required Icon Sizes
Only include icon sizes your extension actually uses:
icons/
├── icon16.png # Toolbar, context menus
├── icon32.png # Toolbar @2x
├── icon48.png # Extensions page
└── icon128.png # Web Store listing
Remove unused icons from your icons field in manifest.json.
Remove Unused Assets
# Find unused images in your source
npx unimported
Regularly audit and remove images, fonts, or other assets you no longer use.
Minification
Minification removes unnecessary characters from code without changing functionality.
Configure Build Tools
// esbuild - built-in minification
esbuild src/background.ts --bundle --minify --outfile=dist/background.js
// webpack - TerserPlugin for JS, CssMinimizerPlugin for CSS
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new TerserPlugin({ terserOptions: { compress: drop_console: true } })],
},
};
Remove Console Logs in Production
// webpack.config.js - drop console.* calls in production
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
});
Source Maps
Generate source maps for debugging but exclude them from the CRX package:
// webpack.config.js
module.exports = {
devtool: 'source-map', // Generate source maps
// Source maps are separate .map files
};
// Exclude from CRX: they won't be bundled automatically
Content Script Size
Content scripts run on every matching page, so keeping them minimal is crucial.
Minimal Content Scripts
// GOOD: Lightweight content script
// content.ts - just inject the core functionality
import { init } from './core.js';
init();
// Lazy load heavy modules
async function onMessage(request) {
if (request.action === 'showUI') {
const { showUI } = await import('./heavy-ui.js');
showUI();
}
}
Message-Based Architecture
Keep content scripts thin and delegate heavy work to the service worker:
// content-script.ts - minimal
document.addEventListener('click', (e) => {
if (e.target.matches('.process-this')) {
chrome.runtime.sendMessage({
action: 'processElement',
data: { outerHTML: e.target.outerHTML }
});
}
});
Minimal CSS Injection
// Inject only the CSS you need
const style = document.createElement('style');
style.textContent = `
.extension-feature { display: none; }
.extension-highlight { background: yellow; }
`;
document.head.appendChild(style);
Before/After Checklist
Run through this checklist before publishing:
- Tree shaking enabled (using ES modules with named imports)
- Unused dependencies removed (audit with
npm list --depth=0) - Images optimized (WebP format, SVG icons where possible)
- Code minified (production builds only)
- Source maps generated but excluded from package
- Separate entry points per context (popup, options, content scripts)
- Dynamic imports used for non-critical features
- Console logs removed from production builds
- Bundle size under 100KB for background, under 50KB for content scripts
- No unused icon sizes included
See Also
- Performance Optimization — Comprehensive performance guide
- CI/CD Pipeline — Automate builds and publishing
- Size Limits Reference — Official size limits and quotas
Related Articles
Related Articles
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.