Widget Bundle Size Optimization for ChatGPT Apps
Bundle size directly impacts the performance and user experience of your ChatGPT app widgets. OpenAI's widget runtime imposes strict constraints on structuredContent payloads (ideally under 4k tokens), and large JavaScript bundles slow initial render times, increase network transfer costs, and degrade the conversational experience. Every kilobyte matters when users expect instant responses within the chat flow.
This guide provides production-ready strategies for minimizing widget bundle size through tree shaking, aggressive minification, modern compression techniques, dependency optimization, and intelligent code splitting. You'll learn how to analyze your bundle composition, identify optimization opportunities, and implement automated size budgets that prevent regression. By the end, you'll have a comprehensive build pipeline that delivers lean, performant widgets that load in milliseconds and seamlessly integrate with ChatGPT's conversational interface.
Whether you're building inline cards, fullscreen experiences, or picture-in-picture widgets, these optimization techniques will help you achieve sub-50KB bundles without sacrificing functionality or developer experience.
Tree Shaking: Eliminate Dead Code Automatically
Tree shaking is the foundation of modern bundle optimization. This process analyzes your ES module imports and eliminates unused code at build time, dramatically reducing final bundle size. Unlike CommonJS modules, ES6 modules have static structure that enables bundlers to determine exactly which exports are used and which can be safely removed.
Effective tree shaking requires proper configuration of your build tool, careful dependency selection, and awareness of side effects that prevent elimination. Vite and Rollup provide excellent tree shaking by default, but you must avoid patterns that break static analysis.
Advanced Vite Production Configuration with Tree Shaking:
// vite.config.ts - Production-optimized build configuration
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
import path from 'path';
export default defineConfig({
plugins: [
react({
// Remove React DevTools in production
babel: {
plugins: [
['transform-react-remove-prop-types', { removeImport: true }]
]
}
}),
visualizer({
filename: './dist/stats.html',
gzipSize: true,
brotliSize: true
})
],
build: {
target: 'es2020',
outDir: 'dist',
assetsDir: 'assets',
// Enable tree shaking
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info', 'console.debug'],
passes: 2,
// Remove unused code
dead_code: true,
// Remove unreachable code
conditionals: true,
// Evaluate constant expressions
evaluate: true,
// Collapse single-use variables
collapse_vars: true,
// Reduce variable names
reduce_vars: true,
// Remove unused imports
unused: true
},
mangle: {
safari10: true,
toplevel: true,
properties: {
regex: /^_private/
}
},
format: {
comments: false,
// Remove whitespace
beautify: false,
// ASCII only for compatibility
ascii_only: true
}
},
rollupOptions: {
output: {
manualChunks: (id) => {
// Separate vendor chunks for better caching
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom')) {
return 'react-vendor';
}
if (id.includes('@openai')) {
return 'openai-vendor';
}
return 'vendor';
}
},
// Optimize chunk naming
chunkFileNames: 'chunks/[name]-[hash].js',
entryFileNames: '[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]'
},
// Mark external dependencies (load from CDN)
external: [],
// Tree shaking configuration
treeshake: {
moduleSideEffects: (id, external) => {
// Preserve side effects for CSS and polyfills
if (id.includes('.css') || id.includes('polyfill')) {
return true;
}
// No side effects for most modules
return false;
},
// Remove unused properties from exports
propertyReadSideEffects: false,
// More aggressive tree shaking
unknownGlobalSideEffects: false,
// Optimize pure annotations
annotations: true
}
},
// Enable source maps for debugging (excluded from bundle)
sourcemap: 'hidden',
// Chunk size warning threshold (KB)
chunkSizeWarningLimit: 50,
// Enable CSS code splitting
cssCodeSplit: true,
// Inline assets smaller than 4KB
assetsInlineLimit: 4096,
// Report compressed size
reportCompressedSize: true,
// Use esbuild for faster builds in development
commonjsOptions: {
transformMixedEsModules: true,
// Improve tree shaking for CommonJS
ignoreTryCatch: false
}
},
// Optimize dependency pre-bundling
optimizeDeps: {
include: ['react', 'react-dom'],
exclude: ['@openai/apps-sdk'],
esbuildOptions: {
target: 'es2020',
// Preserve pure annotations for tree shaking
pure: ['console.log', 'console.debug'],
// Enable tree shaking in deps
treeShaking: true
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils')
},
// Prefer ES modules for better tree shaking
mainFields: ['module', 'jsnext:main', 'jsnext']
}
});
Package.json Side Effects Configuration:
// package.json - Declare side effects for optimal tree shaking
{
"name": "chatgpt-widget-optimized",
"version": "1.0.0",
"type": "module",
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.ts",
"./src/global-setup.ts"
],
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./components": {
"import": "./dist/components.js"
},
"./utils": {
"import": "./dist/utils.js"
}
},
"scripts": {
"build": "vite build",
"build:analyze": "vite build && open dist/stats.html",
"build:size": "npm run build && size-limit"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"rollup-plugin-visualizer": "^5.12.0",
"size-limit": "^11.0.1",
"terser": "^5.26.0",
"vite": "^5.0.10"
},
"size-limit": [
{
"path": "dist/index.js",
"limit": "50 KB",
"gzip": true
},
{
"path": "dist/components.js",
"limit": "30 KB",
"gzip": true
}
]
}
Tree shaking is most effective when you import only the specific functions you need (import { useState } from 'react') rather than entire modules (import * as React from 'react'). Modern libraries like Lodash-ES, Ramda, and date-fns provide ES module builds specifically optimized for tree shaking.
For comprehensive guidance on building ChatGPT apps with optimal architecture, see our Complete Guide to Building ChatGPT Applications.
Minification & Compression: Reduce Transfer Size
Minification removes unnecessary characters from source code without changing functionality—whitespace, comments, verbose variable names—while compression algorithms like gzip and Brotli further reduce transfer size by identifying repeating patterns. Together, these techniques typically achieve 70-90% size reduction compared to development builds.
Terser provides industry-leading JavaScript minification with advanced optimizations like dead code elimination, constant folding, and scope hoisting. Brotli compression (supported by all modern browsers) typically achieves 15-20% better compression than gzip with minimal CPU overhead.
Advanced Compression Middleware for MCP Server:
// src/middleware/compression.ts - Production compression middleware
import { Request, Response, NextFunction } from 'express';
import compression from 'compression';
import { createReadStream, statSync } from 'fs';
import { createBrotliCompress, createGzip } from 'zlib';
import { pipeline } from 'stream';
import path from 'path';
interface CompressionOptions {
threshold: number;
level: number;
brotli: boolean;
cache: boolean;
}
export class CompressionMiddleware {
private cache: Map<string, Buffer> = new Map();
private options: CompressionOptions;
constructor(options: Partial<CompressionOptions> = {}) {
this.options = {
threshold: 1024, // Compress files > 1KB
level: 6, // Compression level (0-9)
brotli: true, // Prefer Brotli when available
cache: true, // Cache compressed responses
...options
};
}
/**
* Express middleware factory
*/
middleware() {
return (req: Request, res: Response, next: NextFunction) => {
const acceptEncoding = req.headers['accept-encoding'] || '';
const originalSend = res.send.bind(res);
res.send = (body: any): Response => {
// Skip compression for small responses
if (!body || body.length < this.options.threshold) {
return originalSend(body);
}
// Check cache first
const cacheKey = this.getCacheKey(req.path, acceptEncoding);
const cached = this.cache.get(cacheKey);
if (cached && this.options.cache) {
this.setCompressionHeaders(res, acceptEncoding);
return originalSend(cached);
}
// Compress response
const compressed = this.compress(body, acceptEncoding);
if (compressed) {
if (this.options.cache) {
this.cache.set(cacheKey, compressed);
}
this.setCompressionHeaders(res, acceptEncoding);
return originalSend(compressed);
}
return originalSend(body);
};
next();
};
}
/**
* Compress data with best available algorithm
*/
private compress(
data: string | Buffer,
acceptEncoding: string
): Buffer | null {
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
// Prefer Brotli (better compression)
if (this.options.brotli && acceptEncoding.includes('br')) {
return this.brotliCompress(buffer);
}
// Fallback to gzip
if (acceptEncoding.includes('gzip')) {
return this.gzipCompress(buffer);
}
// No compression supported
return null;
}
/**
* Brotli compression (best ratio)
*/
private brotliCompress(buffer: Buffer): Buffer {
const { brotliCompressSync } = require('zlib');
return brotliCompressSync(buffer, {
params: {
[require('zlib').constants.BROTLI_PARAM_QUALITY]: this.options.level,
[require('zlib').constants.BROTLI_PARAM_MODE]:
require('zlib').constants.BROTLI_MODE_TEXT
}
});
}
/**
* Gzip compression (universal support)
*/
private gzipCompress(buffer: Buffer): Buffer {
const { gzipSync } = require('zlib');
return gzipSync(buffer, {
level: this.options.level
});
}
/**
* Set appropriate response headers
*/
private setCompressionHeaders(
res: Response,
acceptEncoding: string
): void {
if (this.options.brotli && acceptEncoding.includes('br')) {
res.setHeader('Content-Encoding', 'br');
} else if (acceptEncoding.includes('gzip')) {
res.setHeader('Content-Encoding', 'gzip');
}
res.setHeader('Vary', 'Accept-Encoding');
// Enable browser caching for compressed assets
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
/**
* Generate cache key from path and encoding
*/
private getCacheKey(path: string, encoding: string): string {
const preferredEncoding = this.options.brotli && encoding.includes('br')
? 'br'
: encoding.includes('gzip')
? 'gzip'
: 'none';
return `${path}:${preferredEncoding}`;
}
/**
* Clear compression cache
*/
clearCache(): void {
this.cache.clear();
}
/**
* Get cache statistics
*/
getCacheStats() {
return {
entries: this.cache.size,
sizeBytes: Array.from(this.cache.values())
.reduce((sum, buf) => sum + buf.length, 0)
};
}
}
// Usage in Express app
import express from 'express';
const app = express();
const compressionMiddleware = new CompressionMiddleware({
threshold: 1024,
level: 9, // Maximum compression for production
brotli: true,
cache: true
});
app.use(compressionMiddleware.middleware());
export { CompressionMiddleware };
Always serve compressed assets with appropriate Content-Encoding and Vary headers to ensure proper browser caching and compression negotiation. Pre-compress static assets at build time for even faster delivery.
Learn more about overall widget performance in our Widget Performance Optimization Guide.
Dependency Optimization: Choose Lightweight Alternatives
Third-party dependencies often account for 80-90% of bundle size. Choosing lightweight alternatives and using CDN externals can dramatically reduce your widget bundle. Modern bundlers provide tools to analyze dependency contributions and identify optimization opportunities.
Replace heavy libraries with focused alternatives: use date-fns instead of moment.js (98% smaller), axios instead of full HTTP clients, and native browser APIs instead of utility libraries where possible.
Bundle Analysis and Dependency Replacement Script:
// scripts/optimize-dependencies.js - Analyze and optimize dependencies
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
class DependencyOptimizer {
constructor(projectRoot = process.cwd()) {
this.projectRoot = projectRoot;
this.packageJsonPath = path.join(projectRoot, 'package.json');
this.packageJson = this.loadPackageJson();
this.recommendations = [];
}
loadPackageJson() {
return JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8'));
}
/**
* Analyze bundle and identify heavy dependencies
*/
async analyzeBundleSize() {
console.log('📊 Analyzing bundle composition...\n');
// Build with stats
execSync('npm run build', { stdio: 'inherit' });
const stats = this.parseBundleStats();
const heavyDeps = this.identifyHeavyDependencies(stats);
console.log('\n🔍 Heavy Dependencies (>10KB gzipped):\n');
heavyDeps.forEach(dep => {
console.log(` ${dep.name}: ${this.formatSize(dep.size)}`);
const alternative = this.findLighterAlternative(dep.name);
if (alternative) {
this.recommendations.push({
current: dep.name,
alternative: alternative.name,
savings: dep.size - alternative.size
});
}
});
return this.recommendations;
}
/**
* Parse bundle statistics from Vite output
*/
parseBundleStats() {
const statsPath = path.join(this.projectRoot, 'dist/stats.json');
if (!fs.existsSync(statsPath)) {
throw new Error('Bundle stats not found. Run build with --json flag');
}
return JSON.parse(fs.readFileSync(statsPath, 'utf8'));
}
/**
* Identify dependencies over size threshold
*/
identifyHeavyDependencies(stats, thresholdKB = 10) {
const modules = stats.modules || [];
const depSizes = new Map();
modules.forEach(mod => {
const match = mod.identifier.match(/node_modules\/([^/]+)/);
if (match) {
const depName = match[1];
const currentSize = depSizes.get(depName) || 0;
depSizes.set(depName, currentSize + mod.size);
}
});
return Array.from(depSizes.entries())
.map(([name, size]) => ({ name, size }))
.filter(dep => dep.size > thresholdKB * 1024)
.sort((a, b) => b.size - a.size);
}
/**
* Suggest lighter alternatives for common heavy dependencies
*/
findLighterAlternative(depName) {
const alternatives = {
'moment': { name: 'date-fns', size: 12000, note: '98% smaller' },
'lodash': { name: 'lodash-es', size: 25000, note: 'Tree-shakeable' },
'axios': { name: 'ky', size: 8000, note: 'Modern fetch wrapper' },
'react-router': { name: 'wouter', size: 5000, note: 'Minimalist router' },
'uuid': { name: 'nanoid', size: 2000, note: '40% smaller' },
'crypto-js': { name: 'crypto.subtle API', size: 0, note: 'Native browser API' },
'jquery': { name: 'native DOM API', size: 0, note: 'No dependency' }
};
return alternatives[depName] || null;
}
/**
* Apply recommended optimizations
*/
async applyOptimizations(recommendations) {
console.log('\n✨ Applying optimizations...\n');
for (const rec of recommendations) {
console.log(` Replacing ${rec.current} with ${rec.alternative}...`);
// Uninstall old dependency
try {
execSync(`npm uninstall ${rec.current}`, { stdio: 'pipe' });
} catch (e) {
console.warn(` Warning: Could not uninstall ${rec.current}`);
}
// Install alternative (if not native API)
if (!rec.alternative.includes('API') && !rec.alternative.includes('native')) {
try {
execSync(`npm install ${rec.alternative}`, { stdio: 'pipe' });
console.log(` ✅ Installed ${rec.alternative}`);
} catch (e) {
console.error(` ❌ Failed to install ${rec.alternative}`);
}
}
}
const totalSavings = recommendations.reduce((sum, r) => sum + r.savings, 0);
console.log(`\n💰 Total estimated savings: ${this.formatSize(totalSavings)}\n`);
}
/**
* Format bytes as human-readable size
*/
formatSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
/**
* Generate optimization report
*/
generateReport() {
const reportPath = path.join(this.projectRoot, 'OPTIMIZATION_REPORT.md');
const report = `# Bundle Optimization Report
Generated: ${new Date().toISOString()}
## Recommendations
${this.recommendations.map(rec => `
### Replace ${rec.current} with ${rec.alternative}
- **Savings**: ${this.formatSize(rec.savings)}
- **Migration**: Update imports from \`${rec.current}\` to \`${rec.alternative}\`
`).join('\n')}
## Next Steps
1. Review code for usage of deprecated dependencies
2. Update imports to new libraries
3. Test thoroughly before deployment
4. Run bundle analysis again to verify savings
`;
fs.writeFileSync(reportPath, report);
console.log(`📄 Report saved to: ${reportPath}`);
}
}
// CLI interface
if (require.main === module) {
const optimizer = new DependencyOptimizer();
optimizer.analyzeBundleSize()
.then(recommendations => {
if (recommendations.length === 0) {
console.log('✅ No heavy dependencies found. Bundle is optimized!');
return;
}
optimizer.generateReport();
console.log('\n📋 Review OPTIMIZATION_REPORT.md for details');
console.log('💡 Run with --apply flag to automatically apply changes\n');
if (process.argv.includes('--apply')) {
return optimizer.applyOptimizations(recommendations);
}
})
.catch(err => {
console.error('❌ Optimization failed:', err.message);
process.exit(1);
});
}
module.exports = { DependencyOptimizer };
Run this analysis before every major release to identify new heavy dependencies introduced by team members or transitive dependencies updated by npm.
For build-time optimization strategies, see our Vite Build Optimization Guide.
Code Splitting: Load Only What's Needed
Code splitting divides your bundle into multiple chunks loaded on-demand, drastically reducing initial load time. React's lazy() and Suspense enable component-based splitting, while Vite's dynamic import() provides route-based splitting with automatic chunk generation.
Strategic splitting reduces initial bundles to under 50KB while keeping total bundle size manageable. Split by route (load dashboard code only when users navigate there), by component (load modals only when opened), and by vendor (separate React from business logic for better caching).
Advanced Code Splitting Configuration:
// src/App.tsx - Intelligent code splitting with lazy loading
import React, { Suspense, lazy } from 'react';
import { ErrorBoundary } from './components/ErrorBoundary';
import { LoadingSpinner } from './components/LoadingSpinner';
// Critical components (loaded immediately)
import { Navigation } from './components/Navigation';
import { Footer } from './components/Footer';
// Route-based code splitting (lazy loaded)
const HomePage = lazy(() => import('./pages/HomePage'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
// Component-based code splitting (loaded on interaction)
const ProfileModal = lazy(() => import('./components/ProfileModal'));
const NotificationCenter = lazy(() => import('./components/NotificationCenter'));
// Vendor code splitting (separate chunk for better caching)
const ChartComponent = lazy(() =>
import(/* webpackChunkName: "charts" */ './components/Chart')
);
export const App: React.FC = () => {
const [showProfile, setShowProfile] = React.useState(false);
return (
<ErrorBoundary>
<Navigation />
<main>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
{/* Lazy load modal only when opened */}
{showProfile && (
<Suspense fallback={null}>
<ProfileModal onClose={() => setShowProfile(false)} />
</Suspense>
)}
</main>
<Footer />
</ErrorBoundary>
);
};
Vite Manual Chunks Configuration:
// vite.config.ts - Advanced chunk splitting strategy
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
// React ecosystem (changes infrequently, cache aggressively)
if (id.includes('react') || id.includes('react-dom')) {
return 'react-vendor';
}
// OpenAI SDK (ChatGPT-specific)
if (id.includes('@openai/apps-sdk')) {
return 'openai-sdk';
}
// UI libraries (Material-UI, etc)
if (id.includes('@mui') || id.includes('@emotion')) {
return 'ui-vendor';
}
// Charts and visualization (heavy, rarely loaded)
if (id.includes('chart.js') || id.includes('d3')) {
return 'charts-vendor';
}
// Utilities (lodash, date-fns, etc)
if (id.includes('lodash') || id.includes('date-fns')) {
return 'utils-vendor';
}
// All other node_modules
if (id.includes('node_modules')) {
return 'vendor';
}
// Application code (split by feature)
if (id.includes('/src/pages/')) {
const pageName = id.split('/src/pages/')[1].split('/')[0];
return `page-${pageName}`;
}
},
// Optimize chunk loading
chunkFileNames: (chunkInfo) => {
// Use content hash for long-term caching
return 'chunks/[name]-[hash].js';
}
}
}
}
});
Code splitting works best when combined with route-based lazy loading. Load your home page immediately, but defer dashboard, settings, and admin panels until users navigate to them.
For additional lazy loading strategies, read our Widget Lazy Loading Optimization Guide.
Bundle Analysis: Visualize and Monitor Size
Bundle analysis tools visualize your bundle composition, identify optimization opportunities, and enforce size budgets through automated checks. These tools integrate with CI/CD pipelines to prevent bundle size regression before code reaches production.
The rollup-plugin-visualizer generates interactive treemaps showing every module's contribution to final bundle size. size-limit enforces strict size budgets and fails CI builds when bundles exceed thresholds.
Bundle Analyzer Setup with CI Integration:
// scripts/bundle-analysis.ts - Comprehensive bundle analysis toolkit
import { visualizer } from 'rollup-plugin-visualizer';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
interface BundleSizeReport {
totalSize: number;
gzipSize: number;
brotliSize: number;
chunks: ChunkReport[];
violations: SizeViolation[];
}
interface ChunkReport {
name: string;
size: number;
gzipSize: number;
modules: number;
}
interface SizeViolation {
chunk: string;
currentSize: number;
limitSize: number;
overage: number;
}
class BundleAnalyzer {
private distPath: string;
private budgets: Map<string, number>;
constructor(distPath = './dist') {
this.distPath = distPath;
this.budgets = new Map([
['index', 50 * 1024], // 50KB main bundle
['react-vendor', 40 * 1024], // 40KB React
['vendor', 30 * 1024], // 30KB other vendors
['openai-sdk', 20 * 1024], // 20KB OpenAI SDK
]);
}
/**
* Analyze bundle and generate comprehensive report
*/
async analyze(): Promise<BundleSizeReport> {
console.log('📊 Analyzing bundle composition...\n');
const chunks = this.getChunkStats();
const totalSize = chunks.reduce((sum, c) => sum + c.size, 0);
const gzipSize = this.getTotalGzipSize();
const brotliSize = this.getTotalBrotliSize();
const violations = this.checkSizeBudgets(chunks);
const report: BundleSizeReport = {
totalSize,
gzipSize,
brotliSize,
chunks,
violations
};
this.printReport(report);
this.saveReport(report);
return report;
}
/**
* Get statistics for each chunk
*/
private getChunkStats(): ChunkReport[] {
const jsFiles = fs.readdirSync(this.distPath)
.filter(f => f.endsWith('.js'));
return jsFiles.map(file => {
const filePath = path.join(this.distPath, file);
const stats = fs.statSync(filePath);
const gzipSize = this.getGzipSize(filePath);
return {
name: file.replace(/-[a-f0-9]+\.js$/, ''), // Remove hash
size: stats.size,
gzipSize,
modules: this.countModulesInChunk(filePath)
};
});
}
/**
* Calculate gzip size for a file
*/
private getGzipSize(filePath: string): number {
const { gzipSync } = require('zlib');
const content = fs.readFileSync(filePath);
return gzipSync(content, { level: 9 }).length;
}
/**
* Get total bundle size with Brotli compression
*/
private getTotalBrotliSize(): number {
const { brotliCompressSync } = require('zlib');
const jsFiles = fs.readdirSync(this.distPath)
.filter(f => f.endsWith('.js'));
return jsFiles.reduce((total, file) => {
const content = fs.readFileSync(path.join(this.distPath, file));
const compressed = brotliCompressSync(content, {
params: {
[require('zlib').constants.BROTLI_PARAM_QUALITY]: 11
}
});
return total + compressed.length;
}, 0);
}
/**
* Get total gzip size
*/
private getTotalGzipSize(): number {
const jsFiles = fs.readdirSync(this.distPath)
.filter(f => f.endsWith('.js'));
return jsFiles.reduce((total, file) => {
const gzipSize = this.getGzipSize(path.join(this.distPath, file));
return total + gzipSize;
}, 0);
}
/**
* Count modules in a chunk (approximate)
*/
private countModulesInChunk(filePath: string): number {
const content = fs.readFileSync(filePath, 'utf8');
// Count webpack module markers or imports
const matches = content.match(/\/\*\*\* \.\/node_modules/g);
return matches ? matches.length : 0;
}
/**
* Check size budgets and identify violations
*/
private checkSizeBudgets(chunks: ChunkReport[]): SizeViolation[] {
const violations: SizeViolation[] = [];
chunks.forEach(chunk => {
const budget = this.budgets.get(chunk.name);
if (budget && chunk.gzipSize > budget) {
violations.push({
chunk: chunk.name,
currentSize: chunk.gzipSize,
limitSize: budget,
overage: chunk.gzipSize - budget
});
}
});
return violations;
}
/**
* Print formatted report to console
*/
private printReport(report: BundleSizeReport): void {
console.log('📦 Bundle Size Report\n');
console.log(`Total Size: ${this.formatSize(report.totalSize)}`);
console.log(`Gzip Size: ${this.formatSize(report.gzipSize)}`);
console.log(`Brotli Size: ${this.formatSize(report.brotliSize)}`);
console.log(`Compression Ratio: ${((1 - report.brotliSize / report.totalSize) * 100).toFixed(1)}%\n`);
console.log('📊 Chunks:\n');
report.chunks
.sort((a, b) => b.gzipSize - a.gzipSize)
.forEach(chunk => {
console.log(` ${chunk.name}:`);
console.log(` Size: ${this.formatSize(chunk.size)}`);
console.log(` Gzip: ${this.formatSize(chunk.gzipSize)}`);
console.log(` Modules: ${chunk.modules}\n`);
});
if (report.violations.length > 0) {
console.log('⚠️ Size Budget Violations:\n');
report.violations.forEach(v => {
console.log(` ${v.chunk}:`);
console.log(` Current: ${this.formatSize(v.currentSize)}`);
console.log(` Limit: ${this.formatSize(v.limitSize)}`);
console.log(` Overage: ${this.formatSize(v.overage)}\n`);
});
} else {
console.log('✅ All chunks within size budgets\n');
}
}
/**
* Save report to JSON file
*/
private saveReport(report: BundleSizeReport): void {
const reportPath = path.join(this.distPath, 'bundle-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`📄 Report saved to: ${reportPath}\n`);
}
/**
* Format bytes as human-readable
*/
private formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
/**
* Enforce budgets in CI (exit 1 if violations)
*/
enforceInCI(): boolean {
const report = this.analyze();
if (report.violations.length > 0) {
console.error('❌ Bundle size budget exceeded. Build failed.\n');
process.exit(1);
}
console.log('✅ Bundle size within budgets. Build passed.\n');
return true;
}
}
// CLI usage
if (require.main === module) {
const analyzer = new BundleAnalyzer();
if (process.argv.includes('--ci')) {
analyzer.enforceInCI();
} else {
analyzer.analyze();
}
}
export { BundleAnalyzer };
Size Budget Enforcement in CI:
// .github/workflows/bundle-size.yml
name: Bundle Size Check
on: [pull_request]
jobs:
size-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build bundle
run: npm run build
- name: Check bundle size
run: npm run analyze:ci
- name: Upload bundle report
uses: actions/upload-artifact@v3
with:
name: bundle-report
path: dist/bundle-report.json
Integrate bundle analysis into your CI/CD pipeline to catch size regressions before they reach production. Set aggressive budgets (50KB main bundle, 40KB React vendor chunk) and enforce them on every pull request.
Conclusion: Build Lightning-Fast ChatGPT Widgets
Widget bundle size optimization is critical for ChatGPT app performance and user experience. By implementing aggressive tree shaking, modern compression, strategic dependency management, intelligent code splitting, and automated bundle analysis, you can achieve sub-50KB bundles that load in milliseconds and integrate seamlessly with ChatGPT's conversational interface.
Start with tree shaking configuration to eliminate dead code automatically, then apply Terser minification and Brotli compression for maximum size reduction. Audit your dependencies regularly and replace heavy libraries with focused alternatives or native browser APIs. Implement strategic code splitting to defer non-critical code, and enforce strict size budgets through automated CI checks.
Start Building Optimized ChatGPT Apps Today
MakeAIHQ.com provides production-ready build configurations, automated bundle optimization, and performance monitoring out of the box. Our platform automatically applies these optimization techniques to every ChatGPT app you build, ensuring your widgets load instantly and deliver exceptional user experiences.
Create your first optimized ChatGPT app in minutes with our Instant App Wizard—no webpack configuration, no manual optimization, just production-ready code that passes OpenAI's performance requirements on first submission.
Frequently Asked Questions
Q: What's the ideal bundle size for ChatGPT widgets? A: Target under 50KB gzipped for main bundles, under 40KB for vendor chunks. Inline widgets should load in under 500ms on 3G networks.
Q: Should I use Brotli or gzip compression? A: Use both—serve Brotli to modern browsers (15-20% better compression) with gzip fallback for legacy clients. Pre-compress at build time for fastest delivery.
Q: How often should I analyze bundle size? A: Analyze on every pull request with automated CI checks. Run comprehensive audits monthly to identify new optimization opportunities.
Q: Can I use CDN externals for React? A: Yes, but measure carefully—CDN latency may exceed bundle transfer time for small apps. Use for multi-page apps where cache hits provide value.
Q: What's the difference between tree shaking and minification? A: Tree shaking removes unused exports at the module level (Rollup/Webpack), while minification removes whitespace/renames variables within used code (Terser).
Related Resources:
- Complete Guide to Building ChatGPT Applications
- Widget Lazy Loading Optimization
- Widget Performance Optimization
- Vite Build Optimization
External Resources: