# Hostkeeper V2 — Performance & Stability > **Status**: V2 Planning Complete > **Last Updated**: 2026-06-29 > **Goal**: "Tidak lemot dan bisa stabil digunakan" — fast, responsive, zero crashes. --- ## 1. Performance Targets | Metric | Target | How to Measure | |--------|--------|----------------| | App startup | < 2 seconds | Electron ready → UI visible | | API response | < 50ms | Time from request to response | | Terminal latency | < 10ms | Keystroke → character appears | | SFTP listing | < 500ms | Directory load time | | Memory usage | < 200 MB | Steady state (10 tabs open) | | CPU usage | < 5% | Idle state | | Scroll (1000 hosts) | 60 fps | No jank during scroll | | SSH reconnect | < 3 seconds | Auto-reconnect after disconnect | --- ## 2. Go Backend Performance ### 2.1 GoFiber Configuration ```go app := fiber.New(fiber.Config{ // Timeouts (prevent hung connections) ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, // Concurrency (limit goroutines) Concurrency: 256, // Body limit (prevent large payload attacks) BodyLimit: 10 * 1024 * 1024, // 10MB // Buffer sizes (optimize for typical requests) ReadBufferSize: 4096, WriteBufferSize: 4096, // Disable startup banner DisableStartupMessage: false, // JSON encoder (fastest) JSONEncoder: json.Marshal, JSONDecoder: json.Unmarshal, }) ``` ### 2.2 Connection Pooling (SSH) ```go // app/backend/ws/pool.go type SessionPool struct { mu sync.RWMutex sessions map[string]*pooledSession // key: hostID maxIdle time.Duration maxSize int } type pooledSession struct { client *ssh.Client lastUsed time.Time refCount int } func NewSessionPool(maxSize int, maxIdle time.Duration) *SessionPool { pool := &SessionPool{ sessions: make(map[string]*pooledSession), maxIdle: maxIdle, maxSize: maxSize, } // Cleanup goroutine: close idle connections go func() { ticker := time.NewTicker(30 * time.Second) for range ticker.C { pool.cleanup() } }() return pool } func (p *SessionPool) Get(hostID string) (*pooledSession, error) { p.mu.RLock() sess, exists := p.sessions[hostID] p.mu.RUnlock() if exists { sess.lastUsed = time.Now() sess.refCount++ return sess, nil } // Create new connection p.mu.Lock() defer p.mu.Unlock() // Double-check after acquiring write lock if sess, exists := p.sessions[hostID]; exists { sess.lastUsed = time.Now() sess.refCount++ return sess, nil } // Evict if at capacity if len(p.sessions) >= p.maxSize { p.evictOldest() } // Create new session host, err := storage.GetHost(context.Background(), hostID) if err != nil { return nil, err } client := ssh.NewClient(host, 30*time.Second) if err := client.Connect(context.Background()); err != nil { return nil, err } sess = &pooledSession{ client: client, lastUsed: time.Now(), refCount: 1, } p.sessions[hostID] = sess return sess, nil } func (p *SessionPool) Put(hostID string) { p.mu.Lock() defer p.mu.Unlock() if sess, exists := p.sessions[hostID]; exists { sess.refCount-- if sess.refCount <= 0 { // Don't close immediately — keep for reuse sess.refCount = 0 } } } func (p *SessionPool) cleanup() { p.mu.Lock() defer p.mu.Unlock() for key, sess := range p.sessions { if sess.refCount == 0 && time.Since(sess.lastUsed) > p.maxIdle { sess.client.Close() delete(p.sessions, key) } } } ``` ### 2.3 Buffer Pooling ```go // app/backend/ws/buffers.go var bufferPool = sync.Pool{ New: func() interface{} { buf := make([]byte, 4096) return &buf }, } func GetBuffer() *[]byte { return bufferPool.Get().(*[]byte) } func PutBuffer(buf *[]byte) { // Reset buffer (*buf) = (*buf)[:0] bufferPool.Put(buf) } ``` ### 2.4 Goroutine Management ```go // Limit concurrent SSH sessions var sshSemaphore = make(chan struct{}, 50) // max 50 concurrent sessions func handleTerminal(c *websocket.Conn) { // Acquire semaphore select { case sshSemaphore <- struct{}{}: defer func() { <-sshSemaphore }() default: c.WriteJSON(fiber.Map{ "type": "error", "message": "Too many connections", }) return } // ... handle session } ``` --- ## 3. Frontend Performance ### 3.1 Code Splitting (Lazy Loading) ```typescript // app/frontend/src/router.tsx import { lazy, Suspense } from 'react'; const HostListScreen = lazy(() => import('./screens/HostListScreen')); const TerminalScreen = lazy(() => import('./screens/TerminalScreen')); const SFTPScreen = lazy(() => import('./screens/SFTPScreen')); const SettingsScreen = lazy(() => import('./screens/SettingsScreen')); const router = createBrowserRouter([ { path: '/', element: , children: [ { index: true, element: ( }> ), }, // ... other routes ], }, ]); ``` ### 3.2 Virtual Scrolling (Host List) ```typescript // app/frontend/src/components/VirtualList.tsx import { useVirtualizer } from '@tanstack/react-virtual'; interface VirtualListProps { items: T[]; renderItem: (item: T) => React.ReactNode; estimateSize?: number; } function VirtualList({ items, renderItem, estimateSize = 48 }: VirtualListProps) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => estimateSize, overscan: 10, // Render 10 items outside viewport }); return (
{virtualizer.getVirtualItems().map(virtualRow => (
{renderItem(items[virtualRow.index])}
))}
); } ``` ### 3.3 React.memo for Expensive Components ```typescript // Prevent re-render when props haven't changed const HostItem = React.memo(function HostItem({ host, isActive, onClick }: HostItemProps) { return (
{/* ... */}
); }, (prev, next) => { return prev.host.id === next.host.id && prev.isActive === next.isActive; }); ``` ### 3.4 useMemo for Expensive Computations ```typescript function HostList({ hosts, filter }: { hosts: Host[]; filter: string }) { const filteredHosts = useMemo(() => { if (!filter) return hosts; return hosts.filter(h => h.name.toLowerCase().includes(filter.toLowerCase()) || h.hostname.toLowerCase().includes(filter.toLowerCase()) ); }, [hosts, filter]); return ; } ``` ### 3.5 useCallback for Event Handlers ```typescript function HostItem({ host, onSelect }: { host: Host; onSelect: (id: string) => void }) { const handleClick = useCallback(() => { onSelect(host.id); }, [host.id, onSelect]); return
{/* ... */}
; } ``` ### 3.6 Zustand Selector Optimization ```typescript // Bad: re-renders on ANY store change const hosts = useHostStore(state => state.hosts); // Good: only re-renders when hosts change const hosts = useHostStore(state => state.hosts); // Good: only re-renders when selectedHost changes const selectedId = useHostStore(state => state.selectedHost); // Good: granular selector const hostName = useHostStore(state => { const host = state.hosts.find(h => h.id === state.selectedHost); return host?.name; }); ``` --- ## 4. WebSocket Stability ### 4.1 Keepalive ```typescript // Client side (React) class TerminalWebSocket { private ws: WebSocket; private pingInterval: NodeJS.Timeout; connect(url: string) { this.ws = new WebSocket(url); // Send ping every 30 seconds this.pingInterval = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(new Uint8Array([0])); // ping frame } }, 30000); } disconnect() { clearInterval(this.pingInterval); this.ws.close(1000, 'Client closing'); } } ``` ```go // Server side (Go) func handleTerminal(c *websocket.Conn) { // Set pong handler c.SetPongHandler(func(string) error { c.SetReadDeadline(time.Now().Add(60 * time.Second)) return nil }) // Read loop (detects disconnect) for { _, _, err := c.ReadMessage() if err != nil { break } } } ``` ### 4.2 Auto-Reconnect ```typescript // app/frontend/src/hooks/useTerminalReconnect.ts function useTerminalReconnect(ws: TerminalWebSocket) { const [reconnectAttempts, setReconnectAttempts] = useState(0); const maxReconnectAttempts = 5; useEffect(() => { if (ws.status === 'disconnected' && reconnectAttempts < maxReconnectAttempts) { const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); const timer = setTimeout(() => { setReconnectAttempts(prev => prev + 1); ws.reconnect(); }, delay); return () => clearTimeout(timer); } }, [ws.status, reconnectAttempts]); return { reconnectAttempts, maxReconnectAttempts }; } ``` ### 4.3 Error Boundaries ```typescript // app/frontend/src/components/ErrorBoundary.tsx class ErrorBoundary extends React.Component { state = { hasError: false, error: null }; static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, info: React.ErrorInfo) { console.error('Terminal error:', error, info); } render() { if (this.state.hasError) { return (

Something went wrong

); } return this.props.children; } } ``` --- ## 5. Memory Management ### 5.1 Terminal Cleanup ```typescript function XTermWrapper({ tabId }: { tabId: string }) { useEffect(() => { const term = new Terminal({ scrollback: 10000, // Limit scrollback buffer }); return () => { term.dispose(); // Free memory on unmount }; }, []); } ``` ### 5.2 WebSocket Cleanup ```typescript useEffect(() => { const ws = new WebSocket(url); return () => { ws.close(); // Close on unmount }; }, [hostId]); ``` ### 5.3 Image/Icon Lazy Loading ```typescript // Only load icons when needed const Icon = lazy(() => import('./Icon')); // Or use intersection observer function LazyIcon({ name }: { name: string }) { const [loaded, setLoaded] = useState(false); const ref = useRef(null); useEffect(() => { const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { setLoaded(true); observer.disconnect(); } }); if (ref.current) observer.observe(ref.current); return () => observer.disconnect(); }, []); return
{loaded && }
; } ``` --- ## 6. Crash Recovery ### 6.1 Go Panic Recovery ```go // Already built into GoFiber app.Use(recover.New()) // Custom panic handler app.Use(recover.New(recover.Config{ Handler: func(c *fiber.Ctx, err error) { log.Printf("PANIC: %v\n%s", err, debug.Stack()) c.Status(500).JSON(fiber.Map{ "error": "Internal server error", "code": "PANIC", }) }, })) ``` ### 6.2 Electron Crash Reporter ```typescript // app/electron/main.ts import { crashReporter } from 'electron'; crashReporter.start({ productName: 'Hostkeeper', submitURL: '', // No remote reporting (privacy) uploadToServer: false, compress: true, }); ``` ### 6.3 State Persistence ```typescript // Save critical state to localStorage window.addEventListener('beforeunload', () => { const state = { sidebarWidth: useUIStore.getState().sidebarWidth, activeTab: useTabStore.getState().activeTab, theme: useUIStore.getState().theme, }; localStorage.setItem('hostkeeper-ui-state', JSON.stringify(state)); }); // Restore on load const savedState = localStorage.getItem('hostkeeper-ui-state'); if (savedState) { const state = JSON.parse(savedState); useUIStore.getState().setSidebarWidth(state.sidebarWidth); useUIStore.getState().setTheme(state.theme); } ``` --- ## 7. Logging ### 7.1 Structured Logging (Go) ```go import "github.com/gofiber/fiber/v2/log" // Request logging (built into GoFiber) app.Use(logger.New(logger.Config{ Format: "${time} ${method} ${path} ${status} ${latency}\n", Done: func(c *fiber.Ctx, log string) { // Custom log processing if c.Response().StatusCode() >= 500 { log.Error(log) } }, })) // Application logging log.Info("Server started on port", port) log.Warn("Connection pool full") log.Error("SSH connection failed", err) ``` ### 7.2 Frontend Logging ```typescript // Only log in development const logger = { info: (...args: any[]) => { if (import.meta.env.DEV) console.log('[INFO]', ...args); }, warn: (...args: any[]) => { if (import.meta.env.DEV) console.warn('[WARN]', ...args); }, error: (...args: any[]) => { console.error('[ERROR]', ...args); // Always log errors }, }; ``` --- ## 8. Monitoring ### 8.1 Health Check Endpoint ```go app.Get("/api/health", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "status": "ok", "version": version, "uptime": time.Since(startTime).Seconds(), "connections": sessionPool.ActiveCount(), "memory": getMemoryUsage(), }) }) ``` ### 8.2 Metrics (Optional) ```go // Simple metrics middleware var ( requestCount = prometheus.NewCounter("requests_total") requestLatency = prometheus.NewHistogram("request_latency_seconds") ) app.Use(func(c *fiber.Ctx) error { start := time.Now() err := c.Next() duration := time.Since(start).Seconds() requestCount.Inc() requestLatency.Observe(duration) return err }) ``` --- ## 9. Testing for Performance ### 9.1 Load Testing (Go) ```bash # Install hey (HTTP load testing) go install github.com/rakyll/hey@latest # Test API endpoints hey -n 1000 -c 50 http://localhost:8080/api/hosts hey -n 1000 -c 50 http://localhost:8080/api/keys hey -n 1000 -c 50 http://localhost:8080/api/snippets ``` ### 9.2 Terminal Latency Test ```typescript // Measure keystroke to display latency function measureTerminalLatency(term: Terminal): number { const start = performance.now(); term.write('x'); // Write character // Measure time until character is rendered return performance.now() - start; } ``` ### 9.3 Memory Profiling ```go import "runtime" // Log memory usage var memStats runtime.MemStats runtime.ReadMemStats(&memStats) log.Printf("Memory: %d MB allocated, %d MB sys", memStats.Alloc/1024/1024, memStats.Sys/1024/1024, ) ``` --- ## 10. Common Performance Pitfalls | Issue | Symptom | Fix | |-------|---------|-----| | Memory leak in terminals | Memory grows over time | Dispose terminals on unmount | | WebSocket not closing | Connections pile up | Close on component unmount | | Large JSON responses | Slow API | Pagination, field filtering | | No connection pooling | Slow SSH reconnect | Use SessionPool | | Synchronous SFTP | UI freezes | Use goroutines + channels | | Re-render storms | Jank, low FPS | React.memo, useMemo, Zustand selectors | | No code splitting | Slow initial load | Lazy load screens | | Oversized bundles | Slow startup | Tree shaking, purging |