Navigation Performance & UX Issues
Analysis of navigation performance between HomePage ↔ ChatPage ↔ Library (DocsPage) ↔ Doc Reading (DocViewPage) for mobile PWA and desktop webapp.
Critical Performance Issues 🔴
1. No Code Splitting / Lazy Loading
Location: src/App.tsx:1-38
All routes load eagerly in one 458KB bundle. Every page component (ChatPage, DocsPage, DocViewPage, ContactsPage) loads upfront even if the user never visits them.
Impact:
- Slower initial load on mobile
- Wasted bandwidth for users who only use chat
- react-markdown (88KB) + lucide-react bundle loads even if user never opens docs
Fix: Implement lazy loading:
const ChatPage = lazy(() => import('@/pages/ChatPage'))
const DocsPage = lazy(() => import('@/pages/DocsPage'))
const DocViewPage = lazy(() => import('@/pages/DocViewPage'))
2. Massive lucide-react Bundle (44MB node_modules!)
Location: Throughout codebase
You're importing the entire lucide-react library. While tree-shaking helps, this is still wasteful.
Current usage:
import { ChevronLeft, Search, Plus, MoreVertical, BookOpen, Send } from 'lucide-react'
Impact:
- 44MB dependency (even if tree-shaken, it's still large)
- Slower bundling during dev
- Potential for bundle bloat
Better approach: Use lucide-static or switch to a lighter icon library.
3. Context Value Recreation Causes Unnecessary Re-renders
Location:
- src/contexts/DocsContext.tsx:135-150
- src/contexts/RepositoryContext.tsx:36-42
- src/contexts/ChatContext.tsx:432-443
Context values are recreated on every render, causing all consumers to re-render:
// DocsContext.tsx:135
const value: DocsContextValue = {
docs, // ← Changes when docs update
loading, // ← Changes when loading state changes
error, // ← Changes when error state changes
fetchDocs, // ← Stable (useCallback)
getDoc, // ← Stable (useCallback)
recentDocs, // ← Stable (useMemo)
repos, // ← Changes when repos update
collections, // ← Changes when collections update
getDocsByRepo, // ← UNSTABLE! Recreated on repos change
// ... 6 more functions
}
Impact:
- Every page re-renders when any context value changes
- DocsPage re-renders when you navigate to ChatPage (contexts are nested)
- Unnecessary re-computation of gradients in DocCard
Fix: Memoize the context value:
const value = useMemo(() => ({
docs, loading, error, fetchDocs, getDoc, recentDocs,
repos, collections, getDocsByRepo, getDocsByCollection,
createCollection, deleteCollection, addDocToCollection, removeDocFromCollection,
}), [docs, loading, error, fetchDocs, getDoc, recentDocs, repos, collections, ...])
4. DocsPage Fetches on Every Mount
Location: src/pages/DocsPage.tsx:14-16
useEffect(() => {
fetchDocs()
}, [fetchDocs])
Problem:
- Navigate to /docs → fetches data
- Navigate to /chat → unmounts DocsPage
- Navigate back to /docs → fetches again!
Impact:
- Unnecessary API calls
- Feels sluggish on back navigation
- Wastes bandwidth on mobile
Fix: Add data staleness check:
const { docs, loading, error, fetchDocs, lastFetched } = useDocs()
useEffect(() => {
const FIVE_MINUTES = 5 * 60 * 1000
if (!docs.length || Date.now() - lastFetched > FIVE_MINUTES) {
fetchDocs()
}
}, [fetchDocs, docs.length, lastFetched])
5. DocCard Gradient Recalculated on Every Render
Location: src/components/DocCard.tsx:12
const gradient = generateGradient(doc.title)
Called on every render for every doc card. With 50 docs visible, that's 50 gradient calculations on each render.
Fix: Memoize it:
const gradient = useMemo(() => generateGradient(doc.title), [doc.title])
6. react-markdown Loaded Eagerly
Location: src/pages/DocViewPage.tsx:6
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
Problem: Markdown libraries are loaded even if user never opens a doc.
Fix: Lazy load the DocViewPage component (see #1).
7. CollectionSection N+1 Problem (Already Identified)
Location: src/pages/DocsPage.tsx:119-131
Each collection makes a separate HTTP request sequentially.
Impact:
- 10 collections = 10 sequential requests
- Each blocks the next one
- Feels very slow on mobile networks
Solution:
- Fetch all collection-doc relationships in one API call
- Include docs in the
/api/collectionsresponse - Implement batching/parallelization
UX Sluggishness Issues 🟡
8. No Transition/Loading State Between Routes
Location: src/App.tsx
When navigating between pages, there's no loading indicator. The page just freezes until the next route loads.
Impact:
- User doesn't know if navigation is working
- Feels unresponsive, especially on slower devices
- No visual feedback during route transitions
Fix: Add Suspense boundaries:
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<ContactsPage />} />
...
</Routes>
</Suspense>
9. No Back/Forward Navigation Optimization ⚠️ NEEDS INVESTIGATION
Location: React Router default behavior
React Router doesn't preserve scroll position or component state when navigating back.
Impact:
- User scrolls down in DocsPage, navigates to a doc, hits back → scroll resets to top
- Feels janky and non-native
- Breaks expected mobile navigation UX
Fix: Add scroll restoration with ScrollRestoration component
UPDATE: ScrollRestoration component causes app to crash with blank white page in react-router-dom v7.9.6. Need to investigate alternative solutions:
- Option 1: Manual scroll position tracking with sessionStorage
- Option 2: Use
unstable_useViewTransitionStatehook - Option 3: Custom scroll restoration with useLocation/useLayoutEffect
- Option 4: Update to newer react-router version that fully supports ScrollRestoration
Status: REMOVED from Phase 1 - requires more investigation
10. Messages localStorage Thrashing
Location: src/contexts/ChatContext.tsx:112-120
useEffect(() => {
saveMessageHistory(messagesByRepo) // Writes to localStorage on EVERY message
}, [messagesByRepo])
Problem:
- localStorage writes are synchronous and block the main thread
- Happens on every message received during streaming output
- With rapid message updates, this causes visible jank
Impact:
- Can cause frame drops during active chat sessions
- More noticeable on lower-end mobile devices
- Compounds with other performance issues
Fix: Debounce the saves:
useEffect(() => {
const timeout = setTimeout(() => {
saveMessageHistory(messagesByRepo)
}, 500)
return () => clearTimeout(timeout)
}, [messagesByRepo])
Performance Summary Table
| Issue | Impact | Priority | Difficulty | Estimated Time |
|---|---|---|---|---|
| 1. No code splitting | High | Critical | Easy | 30min |
| 2. lucide-react size | Medium | Medium | Medium | 1-2hr |
| 3. Context re-renders | High | Critical | Medium | 1hr |
| 4. DocsPage refetches | Medium | High | Easy | 20min |
| 5. Gradient recalc | Low-Medium | Medium | Easy | 5min |
| 6. react-markdown eager load | Medium | Medium | Easy | 10min (covered by #1) |
| 7. N+1 collections | High | High | Hard (backend) | 2-4hr |
| 8. No transition states | Medium (UX) | Medium | Easy | 30min |
| 9. No scroll restoration | Medium (UX) | Medium | Medium | POSTPONED - needs investigation |
| 10. localStorage thrashing | Medium | Medium | Easy | 15min |
Total estimated time for all fixes: 6-9 hours
Quick Wins (< 30 minutes each)
These should be tackled first for immediate performance gains:
- ⏳ Add code splitting for routes (30min) → Reduces initial bundle by ~60%
- ✅ Memoize DocCard gradient (5min) → Eliminates unnecessary CPU work
- ✅ Add staleness check to DocsPage (20min) → Eliminates redundant API calls
- ✅ Debounce localStorage writes (15min) → Reduces main thread blocking
- ❌ Add scroll restoration (POSTPONED) → Needs investigation - ScrollRestoration breaks app
- ⏳ Add Suspense boundaries (30min) → Gives visual feedback during navigation
Total completed: 3/6 fixes (~40 minutes) Status: Phase 1 partially complete - scroll restoration requires custom implementation
Medium Priority (1-2 hours each)
- Memoize all context values (1hr)
- Evaluate lucide-react alternatives (1-2hr)
Long-term / Backend Changes
- Fix N+1 collections query (2-4hr, requires backend changes)
Testing Checklist
After implementing fixes, test these scenarios:
Mobile PWA
- Initial app load time
- Navigate Home → Docs → Doc View → Back → Back
- Scroll DocsPage halfway → navigate to doc → back (scroll position preserved?)
- Open app on slow 3G connection (throttle network in DevTools)
- Add 10 collections with 5 docs each → measure load time
Desktop
- Initial app load time
- Navigation feels instant between pages
- No visible frame drops during navigation
- Browser back/forward buttons work smoothly
Metrics to Track
- Lighthouse Performance score (target: 90+)
- First Contentful Paint (target: < 1s)
- Time to Interactive (target: < 2s)
- Total bundle size (target: < 300KB gzipped)
- Number of HTTP requests on DocsPage load (target: < 5)
Implementation Order
Suggested order to maximize impact while minimizing risk:
Phase 1 - Quick Wins (Day 1)
- Issue #5: Memoize DocCard gradient
- Issue #10: Debounce localStorage writes
- Issue #9: Add scroll restoration
- Issue #4: Add staleness check to DocsPage
Phase 2 - Code Splitting (Day 1-2)
- Issue #1: Implement lazy loading for all routes
- Issue #8: Add Suspense boundaries
- Issue #6: Verify react-markdown is code-split
Phase 3 - Context Optimization (Day 2)
- Issue #3: Memoize all context values
- Test for regression in re-render behavior
Phase 4 - Dependencies (Day 3)
- Issue #2: Evaluate lucide-react alternatives
- If switching libraries, update all icon imports
Phase 5 - Backend (Day 4+)
- Issue #7: Fix N+1 collections query
- Coordinate with backend team
Bundle Analysis
Current state:
dist/assets/index-DTTrcyQm.js: 458KB
Target after fixes:
- Initial bundle: ~180KB (code splitting)
- DocsPage chunk: ~120KB (includes react-markdown)
- ChatPage chunk: ~100KB
- Shared vendor chunk: ~150KB
Total download for full app: ~550KB → acceptable for PWA Initial load: ~330KB → 28% reduction
Additional Notes
- vite.config.ts already has PWA caching configured ✅
- Service worker will help with repeat visits
- Consider adding
<link rel="prefetch">for common navigation paths - Monitor bundle size with
npm run build -- --analyze