Skip to content

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:

UpgradePerformance Impact
React 19.1Automatic batching, concurrent rendering, faster reconciliation
Next.js 15.5Turbopack stable, improved code splitting, partial prerendering
Tailwind CSS 4Lightning CSS engine (~10x faster builds), smaller CSS output
Node.js 22V8 engine improvements, faster startup, better memory management
ESLint 9Flat 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:

typescript
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:

typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function ArtistList() {
  const { data: artists, isLoading } = useQuery({
    queryKey: ['artists'],
    queryFn: () => getArtists(session),
  });

  // ...
}

Future: Optimistic Updates

typescript
const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: createArtist,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['artists'] });
  },
});

Image Optimization

Next.js Image Component

typescript
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:

VariantSizeFormatUse Case
Thumbnail150×150WebPGrid views
Medium600pxWebPDetail pages
OriginalFullWebPDownloads

Next.js Image Configuration

javascript
// 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.

ResourceValue
Profilesonnance-cdn (Standard_AzureFrontDoor)
Endpointsonnance-images-hvcsepc8dkhrbjfp.z01.azurefd.net
Originsonnancewebappstorage00.blob.core.windows.net
CachingSAS params (sv, sp, se, sr, sig, spr) as cache key
Compressionimage/webp, jpeg, png, gif, svg
MetricWithout CDNWith CDN
Latency (EU)50-100ms10-20ms
Latency (US)150-200ms20-30ms
Cache hit ratio0%85-95%

How CDN URL Rewriting Works

The backend blob-storage.service.ts handles CDN URL rewriting transparently:

typescript
// 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, immutable

This 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:

typescript
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:

typescript
// 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.

typescript
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 SegmentSkeleton Type
(hydrogen)/loading.tsxGrid of card skeletons (artists/projects/tracks)
(hydrogen)/user/loading.tsxForm fields skeleton (settings pages)
(hydrogen)/wave/[trackId]/loading.tsxCover art + waveform + comments skeleton
(hydrogen)/artist/[artistId]/loading.tsxAvatar + bio + project grid skeleton
(hydrogen)/project/[projectId]/loading.tsxCover + track list skeleton
(other-pages)/loading.tsxMinimal centered pulse

Error Boundaries

Route-level error boundaries catch runtime errors gracefully:

FileScope
(hydrogen)/error.tsxMain app — shows retry + go home buttons with error digest
global-error.tsxRoot fallback — renders own <html>/<body> if root layout fails

Bundle Optimization

Standalone Output

The Next.js build uses standalone output for minimal Docker images:

javascript
// next.config.mjs
output: 'standalone',

Package Import Optimization

Barrel exports from large packages are tree-shaken at build time:

javascript
// 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/debounce not lodash)
  • 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:

yaml
# .github/workflows/deploy.yml
env:
  NODE_OPTIONS: '--max-old-space-size=4096'

Security Headers

Configured in next.config.mjs for all routes:

javascript
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-By header 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

typescript
// 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 thumbnailUrl in list views
  • [x] Use mediumUrl in 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.tsx Suspense boundaries for all route segments
  • [x] error.tsx + global-error.tsx error boundaries
  • [x] optimizePackageImports for 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-Policy restricting 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

  1. Open Chrome DevTools → Network tab
  2. Check for "disk cache" on repeat visits
  3. Verify Cache-Control headers on images
  4. Look for duplicate API requests (React Query should dedupe)
  5. Check Coverage tab for unused JavaScript

Optimization Roadmap

✅ Completed

OptimizationImpact
Azure Front Door CDN for images + audioGlobal 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 boundariesGraceful error recovery
optimizePackageImports (8 packages)Smaller JS bundles
Security headers (X-Frame, CSP, Referrer)Hardened frontend
poweredByHeader: falseReduced 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

PriorityOptimizationEffortImpact
🔴 HighMigrate server actions to React Query useQuery2 daysDeduplication, caching, optimistic UI
🟡 MediumOptimistic updates on mutations1 dayInstant UI feedback
🟡 MediumPrefetch data on link hover2 hoursFaster navigation
🟡 MediumLazy load WaveSurfer.js on demand2 hoursReduced initial bundle
🟢 LowService Worker for offline audio2 daysOffline capability
🟢 LowHLS/DASH adaptive streaming3 daysBetter large-file streaming

Troubleshooting

Images not caching?

Check:

  1. URL includes timestamp? → Remove cache-busting params
  2. Cache-Control header present? → Check backend blob upload
  3. Service worker interference? → Check registration

Duplicate API calls?

Check:

  1. Multiple QueryClientProviders? → Use single provider
  2. Missing queryKey? → Add unique keys
  3. 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

Ctrl-Audio Platform Documentation