-
Notifications
You must be signed in to change notification settings - Fork 0
optimization
Version: 0.5.0 Last Updated: 2026-01-31 Target: Lighthouse 90+ scores across all metrics
- Overview
- Bundle Size Optimization
- Image Optimization
- Database Query Optimization
- Performance Monitoring
- Code Splitting
- Caching Strategies
- Runtime Performance
- Monitoring & Debugging
- Checklist
This guide provides comprehensive strategies for optimizing nself-chat performance across all aspects: bundle size, runtime performance, database queries, and user experience metrics (Core Web Vitals).
| Metric | Target | Current | Status |
|---|---|---|---|
| Lighthouse Performance | β₯90 | TBD | π‘ In Progress |
| Lighthouse Accessibility | β₯90 | TBD | π‘ In Progress |
| Lighthouse Best Practices | β₯90 | TBD | π‘ In Progress |
| Lighthouse SEO | β₯90 | TBD | π‘ In Progress |
| First Contentful Paint (FCP) | <1.8s | TBD | π‘ In Progress |
| Largest Contentful Paint (LCP) | <2.5s | TBD | π‘ In Progress |
| Cumulative Layout Shift (CLS) | <0.1 | TBD | π‘ In Progress |
| Total Bundle Size | <500KB | ~103KB initial | β Good |
# Generate bundle analysis
pnpm build:analyze
# This opens an interactive treemap showing all chunksKey Findings (as of 2026-01-31):
- Initial bundle: ~103KB (shared chunks)
- Largest route:
/chat/channel/[slug]at 262KB (581KB total) - Heavy dependencies identified:
- recharts (~100KB) - Used only in admin
- @tiptap/* (~50KB) - Rich text editor
- mediasoup (~40KB) - Video calls
- @tensorflow/* (~200KB+) - AI moderation
All heavy components are now dynamically imported:
// β
GOOD - Dynamic import with loading state
import dynamic from 'next/dynamic'
import { ChartSkeleton } from '@/components/ui/loading-skeletons'
const ActivityChart = dynamic(
() => import('@/components/admin/activity-chart'),
{
loading: () => <ChartSkeleton />,
ssr: false, // Charts don't need SSR
}
)
// β BAD - Static import of heavy component
import { ActivityChart } from '@/components/admin/activity-chart'Centralized Dynamic Imports: /src/lib/performance/dynamic-imports.ts
Components with dynamic imports:
- β Admin dashboard charts (recharts)
- β Rich text editor (TipTap)
- β Video/audio calls (WebRTC stack)
- β Emoji picker
- β File uploader
- β Thread panel (lazy loaded)
- β Member list (lazy loaded)
- β Swagger UI (API docs)
Configured in next.config.js:
experimental: {
optimizePackageImports: [
'lucide-react', // Icon library - tree-shaking
'@radix-ui/*', // UI components
'date-fns', // Only import used functions
'recharts', // Chart library
'framer-motion', // Animation library
],
}Implemented in next.config.js:
splitChunks: {
cacheGroups: {
framework: {
test: /[\\/]node_modules[\\/](react|react-dom|next)[\\/]/,
name: 'framework',
priority: 40,
},
ui: {
test: /[\\/]node_modules[\\/](@radix-ui|lucide-react)[\\/]/,
name: 'ui',
priority: 30,
},
graphql: {
test: /[\\/]node_modules[\\/](@apollo|graphql)[\\/]/,
name: 'graphql',
priority: 25,
},
charts: {
test: /[\\/]node_modules[\\/](recharts|d3-)[\\/]/,
name: 'charts',
priority: 20,
},
editor: {
test: /[\\/]node_modules[\\/](@tiptap|prosemirror-)[\\/]/,
name: 'editor',
priority: 20,
},
},
}Unused Dependencies Identified:
{
"to-remove": [
"@hookform/resolvers", // Not used (using custom validation)
"@noble/curves", // Unused crypto library
"canvas", // Server-side only
"dashjs", // Unused media library
"simple-peer", // Using mediasoup instead
"rxjs", // Not using observables
"tippy.js" // Using Radix tooltips
]
}Missing Dependencies to Install:
pnpm add web-vitals nanoid dataloaderAlways use next/image for automatic optimization:
// β
GOOD
import Image from 'next/image'
<Image
src="/logo.png"
alt="Logo"
width={200}
height={50}
placeholder="blur"
blurDataURL="data:image/svg+xml;base64,..."
priority // For LCP images
/>
// β BAD
<img src="/logo.png" alt="Logo" />Configured in next.config.js:
images: {
formats: ['image/avif', 'image/webp'], // Modern formats
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60,
}User avatars are frequently loaded:
<Avatar>
<AvatarImage
src={user.avatarUrl}
alt={user.displayName}
// Lazy load avatars below the fold
loading="lazy"
/>
<AvatarFallback>{user.displayName[0]}</AvatarFallback>
</Avatar>Images below the fold should lazy load:
<Image
src="/screenshot.png"
alt="Screenshot"
width={800}
height={600}
loading="lazy" // Don't block initial render
/>Serve appropriately sized images:
<Image
src="/hero.jpg"
alt="Hero"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>Migration: /.backend/migrations/014_performance_indexes.sql
Key Indexes Created:
-- Most common query: Get channel messages
CREATE INDEX idx_messages_channel_created
ON nchat_messages(channel_id, created_at DESC);
-- User lookup (login)
CREATE UNIQUE INDEX idx_users_email
ON nchat_users(email)
WHERE deleted_at IS NULL;
-- Channel slug lookup (URLs)
CREATE UNIQUE INDEX idx_channels_slug
ON nchat_channels(slug)
WHERE deleted_at IS NULL;
-- Full-text search
CREATE INDEX idx_messages_content_search
ON nchat_messages USING GIN (to_tsvector('english', content));Partial Indexes (reduce size, improve performance):
-- Only index recent messages (90 days)
CREATE INDEX idx_messages_recent
ON nchat_messages(channel_id, created_at DESC)
WHERE created_at > NOW() - INTERVAL '90 days';
-- Only index online users
CREATE INDEX idx_users_online
ON nchat_users(presence, last_seen_at DESC)
WHERE presence = 'online';DataLoader Pattern: /src/lib/performance/query-batching.ts
import { createUserLoader } from '@/lib/performance/query-batching'
const userLoader = createUserLoader(apolloClient)
// Instead of N queries
const users = await Promise.all(
userIds.map((id) => client.query({ query: GET_USER, variables: { id } }))
)
// Batch into 1 query
const users = await userLoader.loadMany(userIds)Pre-configured Loaders:
-
createUserLoader- Batch user queries -
createChannelLoader- Batch channel queries -
createMessageLoader- Batch message queries
Use minimal fragments for lists:
# β
GOOD - Only fetch needed fields
fragment UserListItem on nchat_users {
id
username
display_name
avatar_url
presence
}
# β BAD - Over-fetching
fragment UserFull on nchat_users {
id
username
display_name
avatar_url
email
role
presence
created_at
updated_at
settings
# ... 20+ more fields
}Always paginate large lists:
query GetMessages($channelId: uuid!, $limit: Int!, $offset: Int!) {
nchat_messages(
where: { channel_id: { _eq: $channelId } }
order_by: { created_at: desc }
limit: $limit
offset: $offset
) {
...MessageListItem
}
}-- Check index usage
SELECT * FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;
-- Find slow queries
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- Check index size
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC;Component: /src/lib/performance/web-vitals.tsx
import { WebVitalsTracker } from '@/lib/performance/web-vitals'
export default function RootLayout({ children }) {
return (
<html>
<body>
<WebVitalsTracker
enabled={true}
providers={['console', 'sentry', 'ga4']}
sampleRate={1.0} // 100% in dev, reduce in prod
debug={process.env.NODE_ENV === 'development'}
/>
{children}
</body>
</html>
)
}Metrics Tracked:
- β Largest Contentful Paint (LCP)
- β First Input Delay (FID)
- β Cumulative Layout Shift (CLS)
- β Interaction to Next Paint (INP)
- β Time to First Byte (TTFB)
- β First Contentful Paint (FCP)
Utilities: /src/lib/performance/monitoring.ts
import { performanceMonitor, measureAsync } from '@/lib/performance/monitoring'
// Measure async operations
const messages = await measureAsync('api_get_messages', () => fetchMessages(channelId), {
channelId,
})
// Measure component render
useEffect(() => {
const cleanup = usePerformanceMonitor('MessageList')
return cleanup
}, [])
// Track custom metrics
performanceMonitor.record('messages_loaded', messages.length, 'count')
// Get performance report
const report = performanceMonitor.export()Automatic alerts for threshold violations:
const THRESHOLDS = {
page_load: { warning: 3000, critical: 5000 },
api_call: { warning: 1000, critical: 3000 },
lcp: { warning: 2500, critical: 4000 },
// ... more thresholds
}Alerts are sent to:
- Console (development)
- Sentry (production)
- Google Analytics (optional)
- Custom handlers
Next.js automatically splits by route. Each page is a separate chunk.
Current Split (from build output):
-
/chat/channel/[slug]: 262KB (largest) -
/setup/[step]: 52.1KB -
/admin: Dynamic loaded -
/settings/*: Individual chunks
Use dynamic() for large components:
// Heavy component - only load when needed
const VideoCall = dynamic(() => import('@/components/calls/video-call'), {
ssr: false,
loading: () => <div>Loading call interface...</div>
})Split large libraries into separate chunks (see webpack config above).
import { preloadCriticalComponents } from '@/lib/performance/dynamic-imports'
// Preload likely-needed components
useEffect(() => {
if (user && user.role === 'admin') {
preloadAdminComponents()
}
}, [user])Headers configured in next.config.js:
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
}const apolloClient = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
nchat_messages: {
// Merge paginated results
keyArgs: ['where', 'order_by'],
merge(existing = [], incoming) {
return [...existing, ...incoming]
},
},
},
},
},
}),
})App config is cached in localStorage for instant startup:
const config = localStorage.getItem('app-config')
if (config) {
// Use cached config immediately
setConfig(JSON.parse(config))
}
// Fetch latest in background
fetchLatestConfig().then(updateConfig)// public/service-worker.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/offline',
'/manifest.json',
// ... critical assets
])
})
)
})Memoization:
// Memoize expensive computations
const sortedMessages = useMemo(() => messages.sort((a, b) => a.createdAt - b.createdAt), [messages])
// Memoize callbacks
const handleSend = useCallback(
(content: string) => {
sendMessage(channelId, content)
},
[channelId]
)
// Memoize components
const MessageItem = memo(
({ message }) => {
return <div>{message.content}</div>
},
(prev, next) => prev.message.id === next.message.id
)Virtual Scrolling:
import { useVirtualizer } from '@tanstack/react-virtual'
function MessageList({ messages }) {
const parentRef = useRef()
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80, // Estimated message height
overscan: 5, // Render 5 extra items
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<MessageItem message={messages[virtualRow.index]} />
</div>
))}
</div>
</div>
)
}import { debounce } from 'lodash'
// Debounce search input
const handleSearch = debounce((query: string) => {
performSearch(query)
}, 300)
// Throttle scroll events
const handleScroll = throttle(() => {
checkScrollPosition()
}, 100)import { lazy, Suspense } from 'react'
const HeavyComponent = lazy(() => import('./HeavyComponent'))
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
)
}Configuration: /.github/workflows/lighthouse-ci.yml
# Run locally
pnpm lighthouse
# Collect only
pnpm lighthouse:collect
# Assert thresholds
pnpm lighthouse:assert
# Upload to server
pnpm lighthouse:uploadThresholds (.lighthouserc.json):
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.9 }],
"categories:best-practices": ["error", { "minScore": 0.9 }],
"categories:seo": ["error", { "minScore": 0.9 }]
}
}
}
}# Generate bundle analysis
ANALYZE=true pnpm build
# Opens interactive visualizationChrome DevTools:
- Open DevTools (F12)
- Performance tab
- Click Record (β«)
- Perform actions
- Stop recording
- Analyze flame chart
React DevTools Profiler:
- Install React DevTools extension
- Click Profiler tab
- Click Record (β«)
- Interact with app
- Stop and analyze render times
# Check for slow requests in Chrome DevTools
# Network tab β Sort by Time- Bundle analyzer configured
- Dynamic imports for heavy components
- Webpack chunk splitting optimized
- Remove unused dependencies
- Package import optimization enabled
- Tree shaking configured
- All images use Next.js
<Image>component - AVIF/WebP formats enabled
- Lazy loading for below-fold images
- Blur placeholders for key images
- Responsive image sizes configured
- Avatar optimization implemented
- Indexes created for common queries
- Query batching implemented (DataLoader)
- Pagination on large lists
- Optimized GraphQL fragments
- Full-text search indexes
- Query performance monitoring
- Web Vitals tracking implemented
- Performance monitoring utilities
- Sentry integration for errors
- Lighthouse CI configured
- Custom metric tracking
- Performance alerts
- React.memo for expensive components
- useMemo for expensive computations
- useCallback for event handlers
- Virtual scrolling for long lists
- Debouncing for search inputs
- Lazy loading for modals/dialogs
- HTTP caching headers
- Apollo Client cache configured
- localStorage for app config
- Service Worker for offline support
- CDN for static assets
- Console logs removed (except errors/warnings)
- Source maps disabled in production
- Compression enabled
- Security headers configured
- Lighthouse scores 90+
- Performance budget defined
- Run Lighthouse: Get baseline scores
- Identify Bottlenecks: Use Performance DevTools
- Implement Virtual Scrolling: For message lists
- Optimize Images: Convert to WebP/AVIF
- Remove Unused Dependencies: Clean up package.json
- Setup Lighthouse CI: Automate performance testing
- Monitor Production: Track real user metrics
| Resource | Budget | Current | Status |
|---|---|---|---|
| JavaScript (initial) | < 200KB | 103KB | β |
| JavaScript (route) | < 300KB | 262KB | |
| CSS | < 50KB | TBD | π‘ |
| Images (page) | < 500KB | TBD | π‘ |
| Fonts | < 100KB | ~50KB | β |
| Total (initial) | < 500KB | ~153KB | β |
Last Updated: 2026-01-31 Next Review: Weekly during optimization sprint Owner: Development Team
nself-chat v0.3.0 | GitHub | Issues | Discussions | Demo
Edit this page | MIT License | Β© 2026
(See π Security section below for 2FA, PIN Lock, and security audits.)
(Search lives in π Reference below.)
- π¬ Advanced Messaging
- π E2EE Setup
- π Search Setup
- π Call Management
- πΊ Live Streaming
- π₯οΈ Screen Sharing
- πΉ Video Calling
- ποΈ Voice Calling
- π± Mobile Optimization
- π§ͺ Testing
- π i18n
- π API Overview
- π Complete Reference
- π» API Examples
- π€ Bot API
- π Auth API
- π GraphQL Schema
- π Deployment Overview
- π³ Docker
- βΈοΈ Kubernetes
- β Helm Charts
- β Production Checklist
- π Production Validation
- π’ Multi-Tenant
- ποΈ Architecture
- π Diagrams
- ποΈ Database Schema
- π Project Structure
- π TypeScript Types
- π SPORT Reference
- π 2FA
- π¬ Messaging
- π Call Management
- π Call State Machine
- π E2EE
- πΊ Live Streaming
- π± Mobile Calls
- π PIN Lock
- π Polls
- π₯οΈ Screen Sharing
- π Search
- π Social Media
- ποΈ Voice Calling
- π Security Overview
- π‘οΈ Security Audit
- β‘ Performance
- π Best Practices
- π 2FA
- π PIN Lock
- π E2EE
- π‘οΈ E2EE Audit
v1.0.0 β’ 2026