Performance Best Practices β
Learn how to optimize your BoardAPI integration for maximum performance and scalability.
Overview β
BoardAPI is designed to handle large-scale collaborative boards with thousands of objects and hundreds of concurrent users. This guide covers best practices for optimal performance.
Key Performance Metrics β
| Metric | Target | Production Results |
|---|---|---|
| HTTP API Latency (p95) | <300ms | β 245ms |
| Object Creation | <50ms | β 38ms |
| Cursor Updates | <20ms | β 12ms |
| Initial Load Time | <1s | β 0.8s |
| Max Concurrent Users | 200+ | β 1000+ |
Board Size Limits & Recommendations β
Recommended Limits β
| Resource | Recommended | Hard Limit | Performance Impact |
|---|---|---|---|
| Objects per board | 1,000 | 10,000 | Rendering slowdown beyond 1K |
| Concurrent users | 50 | 200 | Network overhead increases linearly |
| Board dimensions | 5000x5000px | Unlimited | Memory usage grows with size |
| File uploads | 10MB each | 50MB | Affects transfer time |
| WebSocket messages/sec | 100 | 500 | CPU usage increases |
Performance Tiers β
Small Boards (0-100 objects)
- No optimization needed
- Full real-time sync
- Instant loading
Medium Boards (100-1,000 objects)
- Lazy loading recommended
- Viewport-based rendering
- ~2-3 second load time
Large Boards (1,000-10,000 objects)
- Lazy loading required
- Chunking system active
- Consider pagination or frames
Extra Large Boards (10,000+ objects)
- Split into multiple boards
- Use Frames for organization
- Implement custom pagination
Object Count Optimization β
Reducing Object Overhead β
1. Combine Small Objects
Instead of creating separate objects for each element, group related items:
// β Bad: 100 individual text objects
for (let i = 0; i < 100; i++) {
await createObject({ type: 'text', text: `Item ${i}` });
}
// β
Good: Single rich text object
await createObject({
type: 'text',
text: 'Item 1\nItem 2\n...\nItem 100'
});2. Use Frames for Organization
Split large boards into logical sections using Frames:
// Create frames for different sections
const lectureFrame = await createFrame({
name: 'Lecture Content',
width: 1920,
height: 1080
});
const exerciseFrame = await createFrame({
name: 'Practice Exercises',
width: 1920,
height: 1080
});3. Archive Inactive Objects
Remove objects that are no longer needed:
// Delete old objects before creating new ones
const oldObjects = await getObjectsByTag('session-2024-11-01');
await deleteObjects(oldObjects.map(o => o.id));Lazy Loading Implementation β
BoardAPI automatically implements viewport-based lazy loading for boards with 100+ objects. The system only renders objects visible in the current viewport plus a 20% buffer.
How it works:
- Objects outside viewport: Not rendered (zero DOM nodes)
- Objects entering viewport: Rendered with 200ms delay
- Objects leaving viewport: Cleaned up after 5s
Client-side optimization:
// Use pagination for object lists
const { objects, total, page } = await getObjects({
boardId: 'your-board-uuid',
page: 1,
limit: 50 // Fetch 50 at a time
});
// Only render visible objects
const visibleObjects = objects.filter(obj =>
isInViewport(obj.position, currentViewport)
);WebSocket Connection Management β
Optimizing Real-Time Updates β
1. Throttle Cursor Updates
Don't send cursor position on every mouse move:
// β Bad: Sends 60+ events/sec
canvas.addEventListener('mousemove', (e) => {
socket.emit('cursor:move', { x: e.clientX, y: e.clientY });
});
// β
Good: Throttled to 20 events/sec
let lastCursorUpdate = 0;
const THROTTLE_MS = 50; // 20 updates/sec
canvas.addEventListener('mousemove', (e) => {
const now = Date.now();
if (now - lastCursorUpdate < THROTTLE_MS) return;
lastCursorUpdate = now;
socket.emit('cursor:move', { x: e.clientX, y: e.clientY });
});2. Batch Object Updates
Combine multiple changes into a single WebSocket message:
// β Bad: 3 separate events
socket.emit('object:update', { id: 1, x: 100 });
socket.emit('object:update', { id: 1, y: 200 });
socket.emit('object:update', { id: 1, rotation: 45 });
// β
Good: Single batched update
socket.emit('object:update', {
id: 1,
x: 100,
y: 200,
rotation: 45
});3. Use Compression
BoardAPI automatically compresses WebSocket messages larger than 1KB using perMessageDeflate. This reduces bandwidth by ~30% for large object updates.
To verify compression is working:
// Check message size in browser DevTools Network tab
// Look for "Sec-WebSocket-Extensions: permessage-deflate"Connection Pooling β
For high-traffic applications, reuse WebSocket connections:
// β
Single connection for multiple boards
const socket = io('https://api.boardapi.io/whiteboard', {
query: { token: yourJWT }
});
// Join multiple boards over same connection
socket.emit('board:join', { boardUuid: 'board-1' });
socket.emit('board:join', { boardUuid: 'board-2' });Caching Strategies β
HTTP Caching β
BoardAPI implements Redis caching with the following TTLs:
| Resource | Cache TTL | Invalidation |
|---|---|---|
| Board state | 10 minutes | On any update |
| Board list | 1 minute | On create/delete |
| Templates | 1 hour | Manual invalidation |
| User metadata | 5 minutes | On profile update |
Client-side caching:
// Cache board metadata in localStorage
const cachedBoard = localStorage.getItem(`board:${boardUuid}`);
if (cachedBoard) {
const { data, timestamp } = JSON.parse(cachedBoard);
// Use cache if less than 5 minutes old
if (Date.now() - timestamp < 5 * 60 * 1000) {
return data;
}
}
// Fetch fresh data
const board = await fetchBoard(boardUuid);
localStorage.setItem(`board:${boardUuid}`, JSON.stringify({
data: board,
timestamp: Date.now()
}));Database Query Optimization β
BoardAPI uses chunking for large boards, dividing the canvas into 512x512px tiles:
βββββββββββββββββββββββββ
β Canvas (2000x2000px) β
β ββββββ¬βββββ¬βββββ β
β β0,0 β1,0 β2,0 β β Each chunk:
β ββββββΌβββββΌβββββ€ β 512x512px
β β0,1 β1,1 β2,1 β β Stored separately
β ββββββΌβββββΌβββββ€ β in database
β β0,2 β1,2 β2,2 β β
β ββββββ΄βββββ΄βββββ β
βββββββββββββββββββββββββBenefits:
- Only load visible chunks (80% faster queries)
- Smaller JSONB documents (faster inserts)
- Parallel chunk loading
Monitoring Integration β
Real-Time Performance Monitoring β
BoardAPI exposes monitoring endpoints for observability:
# Get performance statistics
GET /monitoring/performance/stats
# Response
{
"timestamp": "2025-11-28T10:00:00Z",
"stats": {
"totalRequests": 1523,
"successRate": 99.3,
"averageDuration": 45,
"p50": 32,
"p95": 120,
"p99": 280
}
}Integration with monitoring services:
// Poll performance stats
setInterval(async () => {
const stats = await fetch('https://api.boardapi.io/monitoring/performance/stats');
const data = await stats.json();
// Send to your monitoring service (Datadog, New Relic, etc.)
if (data.stats.p95 > 500) {
alert('High latency detected!');
}
}, 60000); // Check every minuteCustom Metrics β
Track your own performance metrics:
// Measure board load time
const startTime = Date.now();
await loadBoard(boardUuid);
const loadTime = Date.now() - startTime;
console.log(`Board loaded in ${loadTime}ms`);
// Log to your analytics
analytics.track('board_load_time', {
boardUuid,
duration: loadTime,
objectCount: board.objects.length
});Troubleshooting Slow Boards β
Common Issues & Solutions β
Issue: Board loads slowly (>3 seconds)
Solutions:
- Check object count:
GET /boards/:uuid/stats - Enable lazy loading (automatic for 100+ objects)
- Split into multiple frames
- Consider archiving old content
Issue: Real-time updates lag
Solutions:
- Verify WebSocket compression is enabled
- Reduce cursor update frequency (20/sec max)
- Batch object updates
- Check network latency:
socket.io.engine.ping
// Check WebSocket ping
socket.io.engine.on('ping', () => {
console.log('Ping:', socket.io.engine.pingInterval);
});
// Typical values:
// <50ms: Excellent
// 50-150ms: Good
// 150-300ms: Acceptable
// >300ms: Poor (consider CDN/edge deployment)Issue: High memory usage
Solutions:
- Clear old event listeners
- Dispose of unused canvases
- Implement pagination for participant lists
- Use virtual scrolling for long lists
// Clean up event listeners when leaving board
socket.off('object:created');
socket.off('object:updated');
socket.off('object:deleted');
// Dispose of canvas
excalidrawAPI.resetScene();Issue: Slow API responses
Solutions:
- Use pagination:
GET /boards?page=1&limit=50 - Fetch only needed columns:
GET /boards?fields=id,title,status - Enable HTTP caching headers
- Consider GraphQL for selective field fetching (roadmap)
Performance Debugging Checklist β
β‘ Check object count (target: <1,000)
β‘ Verify WebSocket ping (<150ms)
β‘ Monitor HTTP request duration (p95 <300ms)
β‘ Check browser DevTools Performance tab
β‘ Review Network tab for large payloads
β‘ Verify lazy loading is active (100+ objects)
β‘ Check Redis cache hit rate (target: >70%)
β‘ Monitor database query time (<50ms)
β‘ Review error rate (target: <1%)
β‘ Check frame rate (target: 60fps)Production Deployment Tips β
Infrastructure Recommendations β
1. Use CDN for Static Assets
Serve static files from a CDN:
# Configure CDN origin
https://cdn.yourdomain.com/boards/*
# BoardAPI serves assets with cache headers
Cache-Control: public, max-age=31536000, immutable2. Database Connection Pooling
Configure PostgreSQL connection pool:
# .env
DB_POOL_SIZE=20
DB_POOL_MAX=50
DB_POOL_MIN=53. Redis for Caching
Enable Redis for production:
# .env
REDIS_HOST=your-redis-host
REDIS_PORT=6379
REDIS_PASSWORD=your-password
CACHE_TTL=600 # 10 minutes4. Load Balancing
Distribute WebSocket connections across multiple servers:
ββββββββββββ
β Client β
ββββββ¬ββββββ
β
βΌ
ββββββββββββββββ
β Load Balancerβ (Sticky sessions for WebSocket)
ββββββ¬βββββ¬βββββ
β β
βΌ βΌ
βββββββ βββββββ
βWS-1 β βWS-2 β
βββββββ βββββββScaling Strategies β
Horizontal Scaling
- Add more BoardAPI instances
- Use sticky sessions for WebSocket
- Share Redis cache across instances
Vertical Scaling
- Increase CPU for real-time processing
- Add RAM for caching
- Use SSD for database
Database Scaling
- Enable read replicas for queries
- Partition boards by organization
- Archive old boards regularly
Best Practices Summary β
Do's β β
- Throttle cursor updates to 20/sec
- Batch object updates when possible
- Use Frames to organize large boards
- Enable lazy loading for 100+ objects
- Monitor performance metrics
- Cache board metadata client-side
- Use compression for large payloads
- Archive inactive content
- Implement pagination for lists
- Test with production-like data
Don'ts β β
- Don't send events on every mousemove
- Don't load all objects upfront
- Don't create 10,000+ objects on one board
- Don't skip connection cleanup
- Don't ignore performance metrics
- Don't fetch full board state for lists
- Don't disable WebSocket compression
- Don't use long-polling when WebSocket available
- Don't hardcode viewport dimensions
- Don't skip load testing
Next Steps β
- WebSocket API - Real-time events reference
- Frames - Organize large boards
- Monitoring API - Track performance
- Quota & Limits - Understand your limits
Performance Audit Service β
Need help optimizing your BoardAPI integration? Our team can audit your implementation and provide tailored recommendations.
Contact: performance@boardapi.io