UNPKG

@agentman/chat-widget

Version:

Agentman Chat Widget for easy integration with web applications

257 lines (219 loc) 6.55 kB
# Chat Widget Optimization Recommendations ## Current State Analysis ### Bundle Size Issues - **Current size**: 282 KB (exceeds recommended 244 KB limit) - **Major contributors**: 1. **morphdom** (~30KB) - Used only for streaming 2. **uuid** (~25KB) - Used only for generating conversation IDs 3. **SVG assets** (~30KB) - Large inline SVG logo 4. **Abstraction layers** - Multiple renderer interfaces and managers ### Performance Impact - Users on slow connections will experience delayed widget loading - The widget is larger than many entire web applications - Most features aren't used by all users (streaming, file uploads, etc.) ## Recommended Optimizations ### 1. Remove External Dependencies (Saves ~55KB) #### Replace morphdom with native DOM manipulation ```typescript // Instead of morphdom, use targeted updates private updateMessageContent(container: HTMLElement, content: string) { const contentEl = container.querySelector('.message-content'); if (contentEl) { // Preserve images const images = contentEl.querySelectorAll('img'); const imageMap = new Map(); images.forEach(img => { imageMap.set(img.src, img); }); // Update content contentEl.innerHTML = content; // Restore loaded images const newImages = contentEl.querySelectorAll('img'); newImages.forEach(img => { const cached = imageMap.get(img.src); if (cached && cached.complete) { img.replaceWith(cached); } }); } } ``` #### Replace uuid with native crypto ```typescript // Instead of uuid v4 private generateId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } // Or use crypto.randomUUID() where available private generateId(): string { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID(); } return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } ``` ### 2. Optimize SVG Assets (Saves ~25KB) #### Option A: External SVG loading ```typescript // Load SVG on demand private async loadLogo(): Promise<string> { const response = await fetch('/assets/logo.svg'); return response.text(); } ``` #### Option B: Simplified SVG ```typescript // Use a much simpler logo export const simpleLogo = ` <svg viewBox="0 0 24 24" fill="currentColor"> <circle cx="12" cy="12" r="10"/> <path d="M8 14s1.5 2 4 2 4-2 4-2" stroke="white" fill="none"/> </svg>`; ``` ### 3. Simplify Streaming Architecture (Saves ~20KB) #### Merge renderers into a single implementation ```typescript export class UnifiedMessageRenderer { private isStreaming = false; render(message: Message, container: HTMLElement): void { if (this.isStreaming) { this.streamingUpdate(message, container); } else { container.innerHTML = this.renderMessage(message); } } private streamingUpdate(message: Message, container: HTMLElement): void { // Simple, efficient update logic const content = container.querySelector('.content'); if (content) { content.textContent = message.content; // Handle markdown in next tick to avoid blocking requestAnimationFrame(() => { this.applyMarkdown(content); }); } } } ``` ### 4. Code Splitting & Lazy Loading (Saves ~50KB initial) #### Lazy load marked.js ```typescript // Already implemented, good! ``` #### Lazy load streaming functionality ```typescript // Load streaming only when needed private async enableStreaming() { if (!this.streamingModule) { this.streamingModule = await import('./streaming'); } return this.streamingModule; } ``` #### Lazy load file upload functionality ```typescript // Load file handling only when files are selected private async handleFileSelect(files: FileList) { const { FileUploadManager } = await import('./FileUploadManager'); this.uploadManager = new FileUploadManager(...); } ``` ### 5. Remove Unnecessary Abstractions #### Consolidate managers - Merge `StyleManager`, `ThemeManager` into `ConfigManager` - Merge `MessageRenderer` interfaces into single implementation - Remove factory patterns for single implementations #### Simplify state management ```typescript // Instead of complex pub/sub, use simple callbacks class SimpleState { private state: ChatState; private onChange: (state: ChatState) => void; update(updates: Partial<ChatState>) { this.state = { ...this.state, ...updates }; this.onChange(this.state); } } ``` ### 6. Production Build Optimizations #### Webpack configuration improvements ```javascript module.exports = { // ... existing config optimization: { minimize: true, sideEffects: false, usedExports: true, splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, priority: 10, reuseExistingChunk: true } } } }, // Tree shaking module: { rules: [ { test: /\.tsx?$/, use: [{ loader: 'ts-loader', options: { transpileOnly: true, // Faster builds compilerOptions: { module: 'esnext' // Better tree shaking } } }] } ] } }; ``` ## Implementation Priority ### Phase 1: Quick Wins (1-2 days) 1. Replace uuid with native solution 2. Simplify SVG assets 3. Remove unused code and types ### Phase 2: Streaming Simplification (2-3 days) 1. Remove morphdom dependency 2. Merge renderer implementations 3. Simplify streaming logic ### Phase 3: Architecture Cleanup (3-4 days) 1. Consolidate managers 2. Implement code splitting 3. Optimize build configuration ## Expected Results ### Bundle Size Reduction - **Current**: 282 KB - **After Phase 1**: ~230 KB (-18%) - **After Phase 2**: ~180 KB (-36%) - **After Phase 3**: ~150 KB (-47%) ### Performance Improvements - 50% faster initial load - 30% less memory usage - Better performance on low-end devices ## Alternative: Minimal Core + Plugins Consider a plugin architecture: ```typescript // Core: 50KB class ChatWidgetCore { // Basic messaging only } // Plugins loaded on demand const plugins = { streaming: () => import('./plugins/streaming'), fileUpload: () => import('./plugins/fileUpload'), markdown: () => import('./plugins/markdown'), persistence: () => import('./plugins/persistence') }; ``` This would provide: - Ultra-fast initial load - Pay-for-what-you-use model - Better long-term maintainability - Easier testing and updates