Performance Optimization
Strategies for optimal page load times and user experience
Overview
WAVIC implements multiple layers of caching, preloading, and optimization to ensure fast load times across the platform, crucial for a music collaboration tool where users frequently browse projects and stream audio.
Stack Optimization (February 2026)
The following upgrades directly improve runtime and build performance:
| Upgrade | Performance Impact |
|---|---|
| React 19.1 | Automatic batching, concurrent rendering, faster reconciliation |
| Next.js 15.5 | Turbopack stable, improved code splitting, partial prerendering |
| Tailwind CSS 4 | Lightning CSS engine (~10x faster builds), smaller CSS output |
| Node.js 22 | V8 engine improvements, faster startup, better memory management |
| ESLint 9 | Flat config — faster lint times |
Caching Architecture
┌──────────────────────────────────────────────────────────────────────┐
│ CACHING LAYERS │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ Browser React Query Next.js Backend CDN │
│ Cache (Data Cache) (Image/Route) Headers Edge │
│ ┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌────────┐ ┌─────┐ │
│ │ images │ │ API │ │ ISR/SSG │ │ 1-year │ │Azure│ │
│ │ static │◄──│ responses │◄──│ image │◄──│ cache │◄│Front│ │
│ │ assets │ │ 5min stale │ │ optimize │ │control │ │Door │ │
│ └─────────┘ └─────────────┘ └──────────┘ └────────┘ └─────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘React Query Integration
Provider Setup
Located at src/lib/react-query-provider.tsx:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes
refetchOnWindowFocus: true, // Refresh on tab focus
refetchOnReconnect: true, // Refresh on network recovery
},
},
});Current State
The React Query provider is configured globally but data fetching currently uses Server Actions ('use server' functions with native fetch) and React Context providers (notifications, wave player). This is the migration target pattern:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ArtistList() {
const { data: artists, isLoading } = useQuery({
queryKey: ['artists'],
queryFn: () => getArtists(session),
});
// ...
}Future: Optimistic Updates
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createArtist,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['artists'] });
},
});Image Optimization
Next.js Image Component
import Image from 'next/image';
// Use Next.js Image for automatic optimization
<Image
src={artist.image}
alt={artist.name}
width={150}
height={150}
placeholder="blur"
blurDataURL={artist.blurPlaceholder}
/>Image Variants
The backend generates multiple sizes:
| Variant | Size | Format | Use Case |
|---|---|---|---|
| Thumbnail | 150×150 | WebP | Grid views |
| Medium | 600px | WebP | Detail pages |
| Original | Full | WebP | Downloads |
Next.js Image Configuration
// next.config.mjs
images: {
minimumCacheTTL: 2592000, // 30 days
formats: ['image/webp', 'image/avif'],
deviceSizes: [640, 828, 1200, 1920],
imageSizes: [150, 300, 600],
remotePatterns: [
{ protocol: 'https', hostname: '*.blob.core.windows.net' },
{ protocol: 'https', hostname: '*.azurefd.net' },
],
}CDN & Edge Caching
Azure Front Door (Deployed ✅)
All images and audio files are served through Azure Front Door for global edge caching.
| Resource | Value |
|---|---|
| Profile | sonnance-cdn (Standard_AzureFrontDoor) |
| Endpoint | sonnance-images-hvcsepc8dkhrbjfp.z01.azurefd.net |
| Origin | sonnancewebappstorage00.blob.core.windows.net |
| Caching | SAS params (sv, sp, se, sr, sig, spr) as cache key |
| Compression | image/webp, jpeg, png, gif, svg |
| Metric | Without CDN | With CDN |
|---|---|---|
| Latency (EU) | 50-100ms | 10-20ms |
| Latency (US) | 150-200ms | 20-30ms |
| Cache hit ratio | 0% | 85-95% |
How CDN URL Rewriting Works
The backend blob-storage.service.ts handles CDN URL rewriting transparently:
// Backend: getCdnUrl() replaces blob hostname with CDN hostname
// Applied to: uploadFile(), uploadImageWithVariants(), signUrl()
// When CDN_URL env var is set:
// https://sonnancewebappstorage00.blob.core.windows.net/uploads/file.webp?sas=...
// Becomes:
// https://sonnance-images-hvcsepc8dkhrbjfp.z01.azurefd.net/uploads/file.webp?sas=...The frontend receives CDN URLs directly from the API — no client-side URL rewriting needed.
Cache Headers (Backend)
All uploaded files (images AND audio) include:
Cache-Control: public, max-age=31536000, immutableThis enables:
- Browser caching — 1 year cache
- CDN edge caching — Front Door caches at edge PoP
- Immutable — No revalidation needed (content-addressed URLs with SAS tokens)
Preloading & Async Loading
Dynamic Imports (Code Splitting)
Heavy components are dynamically imported to reduce initial bundle size:
import dynamic from 'next/dynamic';
// Modals loaded on-demand
const ProjectModal = dynamic(() => import('@/components/modal/project-modal'), {
ssr: false,
});
// Drawers loaded on-demand
const Drawer = dynamic(() => import('rizzui').then((mod) => mod.Drawer), {
ssr: false,
});Currently dynamically imported:
- Modal dialogs (ArtistModal, ProjectModal, ShareModal, SettingsDrawer)
- Table filters (Drawer, TableFilter, TablePagination)
- Support widgets (NeedSupport)
- SVG icons (React.lazy)
Font Preloading
Fonts are preloaded via Next.js next/font:
// src/app/fonts.ts
import { Inter, Lexend_Deca } from 'next/font/google';
export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
export const lexendDeca = Lexend_Deca({
subsets: ['latin'],
variable: '--font-lexend',
});Route Prefetching
Next.js 15 automatically prefetches linked routes on viewport intersection. Use <Link> components for navigation to benefit from this.
import Link from 'next/link';
// Automatic prefetch when link enters viewport
<Link href={`/p/${project.shortId}`}>
{project.name}
</Link>
// Disable prefetch for rarely-visited routes
<Link href="/user/profile-settings" prefetch={false}>
Settings
</Link>Route Loading States (Suspense Boundaries)
Each route segment has a loading.tsx that renders a skeleton UI during navigation:
| Route Segment | Skeleton Type |
|---|---|
(hydrogen)/loading.tsx | Grid of card skeletons (artists/projects/tracks) |
(hydrogen)/user/loading.tsx | Form fields skeleton (settings pages) |
(hydrogen)/wave/[trackId]/loading.tsx | Cover art + waveform + comments skeleton |
(hydrogen)/artist/[artistId]/loading.tsx | Avatar + bio + project grid skeleton |
(hydrogen)/project/[projectId]/loading.tsx | Cover + track list skeleton |
(other-pages)/loading.tsx | Minimal centered pulse |
Error Boundaries
Route-level error boundaries catch runtime errors gracefully:
| File | Scope |
|---|---|
(hydrogen)/error.tsx | Main app — shows retry + go home buttons with error digest |
global-error.tsx | Root fallback — renders own <html>/<body> if root layout fails |
Bundle Optimization
Standalone Output
The Next.js build uses standalone output for minimal Docker images:
// next.config.mjs
output: 'standalone',Package Import Optimization
Barrel exports from large packages are tree-shaken at build time:
// next.config.mjs
experimental: {
optimizePackageImports: [
'react-icons', // Thousands of icons — only import what's used
'rizzui', // UI library
'framer-motion', // Animation library
'jotai', // State management
'@tanstack/react-query',
'@tanstack/react-table',
'react-hook-form',
'react-hot-toast',
],
}This reduces bundle size by eliminating unused exports during compilation.
Tree Shaking
- RizzUI v2 supports tree shaking — only imported components are bundled
- Lodash is imported per-function (
lodash/debouncenotlodash) - React Icons uses ES module imports for per-icon tree shaking
CI Build Memory
GitHub Actions builds use increased heap size for the larger v4 stack:
# .github/workflows/deploy.yml
env:
NODE_OPTIONS: '--max-old-space-size=4096'Security Headers
Configured in next.config.mjs for all routes:
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), geolocation=(), microphone=()' },
],
},
{
source: '/fonts/(.*)',
headers: [
{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
],
},
];
}X-Powered-Byheader is disabled (poweredByHeader: false)- Font files are served with 1-year immutable cache
- API routes are not cached
Audio Streaming Optimization
Waveform Lazy Loading
WaveSurfer.js instances are created only when audio components mount, not during SSR.
Audio Preloading Strategy
// Preload next track in playlist
const preloadAudio = (url: string) => {
const audio = new Audio();
audio.preload = 'metadata'; // Only fetch headers + duration
audio.src = url;
};Recommendations for Large Audio Files
- Serve audio through CDN (Azure Front Door) for edge caching
- Use
preload="metadata"for track listings (don't buffer full audio) - Use
preload="auto"only for the currently playing track - Consider HLS/DASH adaptive streaming for files > 50MB (future)
Performance Checklist
Images
- [x] Use
thumbnailUrlin list views - [x] Use
mediumUrlin detail pages - [x] Add blur placeholder for progressive loading
- [x] 1-year cache headers on all uploaded files (images + audio)
- [x] WebP conversion on upload
- [x] Azure Front Door CDN deployed for global delivery
- [ ] Lazy load below-fold images with
loading="lazy"
Data Fetching
- [x] React Query with 5-minute staleTime
- [x] Cache invalidation on mutations
- [x] Background refetch on window focus
- [ ] Optimistic updates for instant UI feedback
- [ ] Prefetch data on hover for anticipated navigation
Code Splitting
- [x] Dynamic imports for modals and drawers
- [x] Route-based code splitting (automatic in Next.js 15)
- [x] Standalone output for optimized production builds
- [x]
loading.tsxSuspense boundaries for all route segments - [x]
error.tsx+global-error.tsxerror boundaries - [x]
optimizePackageImportsfor react-icons, rizzui, framer-motion, etc. - [ ] Lazy load WaveSurfer.js and Three.js (3D) only when needed
Security & Headers
- [x]
X-Content-Type-Options: nosniff - [x]
X-Frame-Options: DENY - [x]
Referrer-Policy: strict-origin-when-cross-origin - [x]
Permissions-Policyrestricting camera/geo/mic - [x]
poweredByHeader: false - [x] Font assets cached 1 year (immutable)
CDN & Caching
- [x] Cache-Control headers on blob storage (1 year, immutable)
- [x] Next.js image optimization with 30-day cache TTL
- [x] Azure Front Door CDN deployed (
sonnance-images-*.z01.azurefd.net) - [x] CDN_URL env var set on prod + dev Container Apps
- [ ] Service Worker for offline-first audio caching (future)
Performance Monitoring
Lighthouse Metrics
Target scores:
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
- TTFB (Time to First Byte): < 800ms
DevTools Analysis
- Open Chrome DevTools → Network tab
- Check for "disk cache" on repeat visits
- Verify
Cache-Controlheaders on images - Look for duplicate API requests (React Query should dedupe)
- Check Coverage tab for unused JavaScript
Optimization Roadmap
✅ Completed
| Optimization | Impact |
|---|---|
| Azure Front Door CDN for images + audio | Global edge caching, latency 5-10x reduction |
Cache-Control: immutable on all uploaded files (images + audio) | Browser + CDN caching |
loading.tsx route Suspense boundaries (6 files) | Better perceived performance |
error.tsx + global-error.tsx error boundaries | Graceful error recovery |
optimizePackageImports (8 packages) | Smaller JS bundles |
| Security headers (X-Frame, CSP, Referrer) | Hardened frontend |
poweredByHeader: false | Reduced attack surface |
| Font caching (1 year immutable) | Faster repeat visits |
Next.js minimumCacheTTL (30 days) | Reduced image re-optimization |
| Image format/size optimization (WebP) | Smaller image payloads |
🔜 Pending
| Priority | Optimization | Effort | Impact |
|---|---|---|---|
| 🔴 High | Migrate server actions to React Query useQuery | 2 days | Deduplication, caching, optimistic UI |
| 🟡 Medium | Optimistic updates on mutations | 1 day | Instant UI feedback |
| 🟡 Medium | Prefetch data on link hover | 2 hours | Faster navigation |
| 🟡 Medium | Lazy load WaveSurfer.js on demand | 2 hours | Reduced initial bundle |
| 🟢 Low | Service Worker for offline audio | 2 days | Offline capability |
| 🟢 Low | HLS/DASH adaptive streaming | 3 days | Better large-file streaming |
Troubleshooting
Images not caching?
Check:
- URL includes timestamp? → Remove cache-busting params
- Cache-Control header present? → Check backend blob upload
- Service worker interference? → Check registration
Duplicate API calls?
Check:
- Multiple QueryClientProviders? → Use single provider
- Missing queryKey? → Add unique keys
- staleTime too low? → Increase to 5+ minutes
Build running out of memory?
The production build requires ~3-4GB heap. Ensure NODE_OPTIONS=--max-old-space-size=4096 is set in CI environment.
Last Updated: February 2026