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.2Automatic batching, concurrent rendering, faster reconciliation
Next.js 16.1Turbopack default bundler, FS caching, View Transitions API
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
    },
  },
});

Active Usage

React Query hooks are used for sidebar and navigation data, eliminating redundant API calls and skeleton flash on repeat visits:

HookQuery KeyLocationData
useRecentTracks()['recent-tracks']src/hooks/use-recent-tracks.tsRecent track spaces (sidebar + wave page)
useSidebarArtists()['sidebar-artists']src/hooks/use-sidebar-artists.tsOwn + collaborator artists (sidebar)

How caching eliminates loading flicker:

typescript
// isLoading = true ONLY when there's no cached data (first visit ever).
// On subsequent renders, cached data is shown instantly while
// React Query silently revalidates in the background.
const { data: recentTracks = [], isLoading } = useRecentTracks();

// Show skeleton only on first load — never on navigation clicks
if (isLoading) return <MenuSkeleton />;

Cache invalidation after mutations:

typescript
// After creating an artist, invalidate the sidebar cache.
// React Query refetches in the background — no skeleton shown.
const invalidateArtists = useInvalidateSidebarArtists();
invalidateArtists(); // triggers background refetch for all consumers

Shared cache across components:

All consumers of the same queryKey share a single cache entry. The sidebar (desktop), drawer (mobile), and wave dashboard all read from ['recent-tracks'] — the first component to mount fetches the data, and all others get it instantly from cache.

Remaining Migration

Some pages still use raw fetch() with useState (artist dashboard, project dashboard). These should be migrated to useQuery hooks following the same pattern as the sidebar hooks.

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';
import { isAzureUrl } from '@/utils/url-helpers';

// Use Next.js Image — skip optimization for Azure CDN images
<Image
  src={artist.image}
  alt={artist.name}
  width={150}
  height={150}
  unoptimized={isAzureUrl(artist.image)}
/>

Azure URL bypass: Images served through Azure Blob Storage or Front Door CDN are already optimized. Setting unoptimized={true} for these URLs avoids SAS token double-encoding and ensures preloaded URLs match rendered URLs. The isAzureUrl() helper in src/utils/url-helpers.ts detects both blob.core.windows.net and .azurefd.net hostnames.

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
  qualities: [100, 75],
  formats: ['image/webp'],
  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)

Image Preloading (Skeleton → Content Transition)

Images are preloaded into the browser cache before hiding the skeleton, preventing the "content loads but images pop in later" flash:

typescript
import { preloadImages } from '@/utils/url-helpers';

// In dashboard/artist data fetching:
const imageUrls = artists.map((a) => a.image).filter(Boolean);
await preloadImages(imageUrls); // Warms browser cache
setIsLoading(false); // Skeleton hides only after images are cached

Key details:

  • preloadImages() uses new Image() to download images into the browser cache
  • 5-second timeout prevents broken images from blocking the UI forever
  • Session-stable timestamps (appendTimestamp()) ensure preloaded URLs match rendered URLs
  • Content containers use animate-fade-in CSS class (200ms ease-out) to mask paint delay

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 16 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 16)
  • [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

| 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 | | Image preloading before skeleton hide | No image pop-in flash | | Azure URL bypass (isAzureUrl + unoptimized) | Consistent preload/render URLs | | Session-stable cache-busting timestamps | Browser cache works across re-renders | | CSS fade-in transition on skeleton → content | Smooth visual transition | | Next.js 15 → 16 upgrade (Turbopack default, React 19.2) | Faster builds, smaller bundles | | Unified skeleton shimmer effect (skeleton.tsx) | Consistent loading states, reduced CSS | | Enhanced artist/project loading skeletons | Better layout match, less CLS on load | | Console.log cleanup in core navigation flow | Cleaner console, reduced noise in dev | | WaveSurfer AbortError handling (all 3 wave-form components) | No unhandled promise rejections | | Font preload optimization (PPMonumentExtended → preload: false) | Eliminated unused preload warnings | | Sidebar React Query migration (recent tracks + artists) | No skeleton flash on navigation, shared cache |

🔜 Pending

PriorityOptimizationEffortImpact
🔴 HighMigrate dashboard pages to React Query useQuery1 dayDeduplication, 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