Files
mylder-frontend/docs/performance-checklist.md
christiankrag ef31ed3564 Add health endpoint for Swarm health checks
The /health route returns JSON status for Docker Swarm
to verify container health during deployment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 13:27:25 +01:00

17 KiB

Performance Checklist for Mylder Platform

Core Web Vitals Targets

Production Goals (75th Percentile)

Metric Target Good Needs Improvement Poor
LCP (Largest Contentful Paint) <2.5s <2.5s 2.5s-4.0s >4.0s
FID (First Input Delay) <100ms <100ms 100ms-300ms >300ms
CLS (Cumulative Layout Shift) <0.1 <0.1 0.1-0.25 >0.25
INP (Interaction to Next Paint) <200ms <200ms 200ms-500ms >500ms
TTFB (Time to First Byte) <600ms <800ms 800ms-1800ms >1800ms
FCP (First Contentful Paint) <1.8s <1.8s 1.8s-3.0s >3.0s

Current vs Target Performance

Baseline (before optimization):

LCP: ~3.5s → Target: <2.5s (30% improvement needed)
FID: ~150ms → Target: <100ms
CLS: ~0.15 → Target: <0.1
TTFB: ~800ms → Target: <600ms (edge caching)

Measurement Tools

1. Real User Monitoring (RUM)

// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Analytics } from '@vercel/analytics/react';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <SpeedInsights />
        <Analytics />
      </body>
    </html>
  );
}

Alternative: Cloudflare Web Analytics (privacy-friendly, no cookies)

<script defer src='https://static.cloudflareinsights.com/beacon.min.js'
        data-cf-beacon='{"token": "YOUR_TOKEN"}'></script>

2. Lab Testing

# Lighthouse (local)
npx lighthouse https://mylder.io --view

# WebPageTest (global)
# https://www.webpagetest.org

# Chrome DevTools
# DevTools → Performance → Record → Analyze

3. Continuous Monitoring

# Lighthouse CI (in CI/CD pipeline)
npm install -g @lhci/cli
lhci autorun --collect.url=https://mylder.io

Next.js Optimization Settings

1. Production Build Configuration

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  // Enable React strict mode for better error handling
  reactStrictMode: true,

  // Optimize bundle size
  swcMinify: true,

  // Compress output
  compress: true,

  // Optimize images
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256],
    minimumCacheTTL: 2592000, // 30 days
    dangerouslyAllowSVG: false,
    contentDispositionType: 'attachment',
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'supabase.mylder.io',
        pathname: '/storage/v1/object/**',
      },
    ],
  },

  // Optimize fonts (automatic optimization)
  optimizeFonts: true,

  // Experimental features
  experimental: {
    // Optimize CSS
    optimizeCss: true,
    // Tree shaking for smaller bundles
    optimizePackageImports: ['@radix-ui/react-icons', 'lucide-react'],
  },

  // Custom headers for caching
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'X-DNS-Prefetch-Control',
            value: 'on',
          },
          {
            key: 'X-Frame-Options',
            value: 'SAMEORIGIN',
          },
        ],
      },
      {
        source: '/_next/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/_next/image/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=2592000, stale-while-revalidate=86400',
          },
        ],
      },
    ];
  },
};

export default nextConfig;

2. Bundle Analyzer

# Install
npm install -D @next/bundle-analyzer

# next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer';

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
});

export default withBundleAnalyzer(nextConfig);

# Run analysis
ANALYZE=true npm run build

Target: Total bundle size <500KB (gzipped)

3. Code Splitting Strategies

Dynamic Imports

// app/dashboard/page.tsx
import dynamic from 'next/dynamic';

// Lazy load heavy components
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false, // Client-side only if needed
});

export default function Dashboard() {
  return <HeavyChart data={data} />;
}

Route-Based Splitting

// Automatic with App Router
app/
  dashboard/      # Separate chunk
  settings/       # Separate chunk
  (marketing)/    # Separate chunk (route group)

Vendor Splitting

// next.config.ts
experimental: {
  optimizePackageImports: [
    '@radix-ui/react-icons',
    'lucide-react',
    'date-fns',
  ],
},

4. Server Components (Default)

// app/page.tsx (Server Component by default)
export default async function Home() {
  // Fetch at build time or request time
  const data = await fetch('https://api.mylder.io/data', {
    next: { revalidate: 3600 } // ISR: 1 hour cache
  });

  return <div>{/* No client-side JS for this component */}</div>;
}

Benefits:

  • 0KB JavaScript sent to client
  • Faster initial page load
  • Better SEO

5. Client Components (Opt-In)

// components/InteractiveButton.tsx
'use client';

import { useState } from 'react';

export default function InteractiveButton() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Rule: Only use 'use client' when you need:

  • Interactivity (useState, useEffect)
  • Browser APIs (localStorage, window)
  • Event handlers (onClick, onSubmit)

Image Optimization

1. Next.js Image Component

import Image from 'next/image';

// Optimized image with automatic sizing
<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1920}
  height={1080}
  priority // For LCP images
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/..." // Generated at build
/>

// Responsive image
<Image
  src="/hero.jpg"
  alt="Hero image"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  className="object-cover"
  priority
/>

2. Image Format Priority

  1. AVIF (best compression, 50% smaller than JPEG)
  2. WebP (wide support, 30% smaller than JPEG)
  3. JPEG/PNG (fallback)

3. Cloudflare Image Optimization

// next.config.ts
images: {
  loader: 'custom',
  loaderFile: './lib/cloudflare-image-loader.ts',
}

// lib/cloudflare-image-loader.ts
export default function cloudflareLoader({ src, width, quality }) {
  const params = [`width=${width}`, `quality=${quality || 75}`, 'format=auto'];
  return `https://mylder.io/cdn-cgi/image/${params.join(',')}/${src}`;
}

4. Lazy Loading Images

// Non-critical images (below fold)
<Image
  src="/feature.jpg"
  alt="Feature"
  width={800}
  height={600}
  loading="lazy" // Default, explicit here
/>

// Critical images (above fold, LCP)
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  priority // Preload immediately
/>

5. Responsive Images Checklist

  • Use Next.js Image component (automatic optimization)
  • Set explicit width/height (prevent CLS)
  • Use priority for LCP images
  • Use loading="lazy" for below-fold images
  • Provide blur placeholder for better UX
  • Compress images before upload (80-85% quality)
  • Use modern formats (AVIF, WebP)

Font Optimization

1. Next.js Font Optimization (Built-In)

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Prevent invisible text flash
  variable: '--font-inter',
  preload: true,
});

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
  weight: ['400', '700'], // Only load needed weights
});

export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable} ${robotoMono.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  );
}

Benefits:

  • Self-hosted fonts (no external requests)
  • Automatic subset optimization
  • Zero layout shift (font-display: swap)

2. Custom Fonts

import localFont from 'next/font/local';

const myFont = localFont({
  src: './fonts/MyFont.woff2',
  display: 'swap',
  variable: '--font-my-font',
});

3. Font Loading Best Practices

  • Use only 2-3 font families max
  • Load only needed weights (400, 700)
  • Use font-display: swap
  • Preload critical fonts
  • Self-host fonts (no Google Fonts CDN)

CSS Optimization

1. Tailwind CSS Production Config

// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
  content: [
    './app/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
  ],
  theme: {
    extend: {},
  },
  // Remove unused styles in production
  future: {
    hoverOnlyWhenSupported: true, // Better mobile UX
  },
};

export default config;

2. Critical CSS

Next.js automatically inlines critical CSS for first paint.

3. CSS Module Best Practices

// Use CSS Modules for component-specific styles
import styles from './Button.module.css';

export function Button() {
  return <button className={styles.button}>Click me</button>;
}

JavaScript Optimization

1. Tree Shaking

// Good: Import only what you need
import { format } from 'date-fns';

// Bad: Import entire library
import * as dateFns from 'date-fns';

2. Remove Unused Dependencies

# Analyze bundle
npx depcheck

# Remove unused
npm uninstall unused-package

3. Use Smaller Alternatives

Heavy Package Lightweight Alternative Savings
Moment.js (232KB) date-fns (13KB) 95%
Lodash (70KB) Lodash-es (24KB) 66%
Axios (30KB) Fetch API (native) 100%
React Icons (full) Lucide React (tree-shakeable) 80%

4. Debounce/Throttle Heavy Operations

// components/Search.tsx
'use client';

import { useState, useCallback } from 'react';
import { debounce } from 'lodash-es/debounce';

export function Search() {
  const [query, setQuery] = useState('');

  const handleSearch = useCallback(
    debounce(async (value: string) => {
      const results = await fetch(`/api/search?q=${value}`);
      // Handle results
    }, 300),
    []
  );

  return <input onChange={(e) => handleSearch(e.target.value)} />;
}

Caching Strategy

1. Static Generation (Fastest)

// app/blog/page.tsx
export const revalidate = false; // Static at build time

export default async function Blog() {
  const posts = await fetchPosts();
  return <PostList posts={posts} />;
}

2. Incremental Static Regeneration (ISR)

// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidate every hour

export async function generateStaticParams() {
  const posts = await fetchPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function Post({ params }) {
  const post = await fetchPost(params.slug);
  return <Article post={post} />;
}

3. Server-Side Rendering (SSR)

// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'; // Always server-render

export default async function Dashboard() {
  const data = await fetchUserData(); // Per-request
  return <DashboardContent data={data} />;
}

4. Client-Side Caching

// Use SWR or React Query for client-side data fetching
'use client';

import useSWR from 'swr';

export function Profile() {
  const { data, error } = useSWR('/api/user', fetcher, {
    revalidateOnFocus: false,
    revalidateOnReconnect: false,
    refreshInterval: 60000, // 1 minute
  });

  if (error) return <div>Error</div>;
  if (!data) return <div>Loading...</div>;
  return <div>{data.name}</div>;
}

5. HTTP Cache Headers

// app/api/data/route.ts
export async function GET() {
  const data = await fetchData();

  return new Response(JSON.stringify(data), {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
    },
  });
}

Cache Invalidation Strategy

1. On-Demand Revalidation

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';

export async function POST(request: Request) {
  const { path, tag } = await request.json();

  if (path) {
    revalidatePath(path);
  }

  if (tag) {
    revalidateTag(tag);
  }

  return Response.json({ revalidated: true });
}

2. Webhook Triggers (n8n)

// n8n workflow: CMS Update → POST /api/revalidate
{
  "path": "/blog",
  "tag": "posts"
}

3. Cloudflare Cache Purge

# On deploy (CI/CD)
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  --data '{"purge_everything":true}'

4. Selective Purge

# Purge specific URLs
--data '{"files":["https://mylder.io/blog","https://mylder.io/about"]}'

# Purge by tag
--data '{"tags":["blog-posts"]}'

Performance Testing Workflow

1. Pre-Deploy Checks

# Run before every deployment
npm run build       # Must succeed
npm run lint        # No errors
npm run typecheck   # No type errors

# Optional but recommended
ANALYZE=true npm run build  # Check bundle size
npx lighthouse https://staging.mylder.io --view

2. Post-Deploy Verification

# Production smoke tests
curl -I https://mylder.io  # Check TTFB
curl -I https://mylder.io/_next/static/[hash]/[file].js  # Check caching

# Full performance audit
npx lighthouse https://mylder.io --view

# Real user monitoring
# Check Cloudflare Analytics or Vercel Analytics dashboard

3. Continuous Monitoring

# Set up alerts in Cloudflare/monitoring tool
- LCP > 3s → Alert
- Error rate > 1% → Alert
- TTFB > 1s → Alert

Quick Wins Checklist

Immediate (1 day)

  • Enable Cloudflare CDN (see cloudflare-setup.md)
  • Add proper cache headers to static assets
  • Use Next.js Image component everywhere
  • Enable reactStrictMode and swcMinify
  • Remove unused dependencies

Short-term (1 week)

  • Implement ISR for blog/content pages
  • Lazy load heavy components
  • Optimize images (compress, convert to WebP/AVIF)
  • Set up bundle analyzer
  • Add loading states and skeletons (improve perceived performance)

Medium-term (1 month)

  • Implement edge caching for auth (see cloudflare-workers-roadmap.md)
  • Set up real user monitoring (RUM)
  • Optimize third-party scripts (defer/async)
  • Implement stale-while-revalidate for APIs
  • Add Lighthouse CI to deployment pipeline

Long-term (3 months)

  • Move 70% traffic to edge (Cloudflare Workers)
  • Implement edge personalization
  • Set up A/B testing at edge
  • Achieve Core Web Vitals targets consistently
  • Reduce VPS load by 60%+

Common Performance Issues & Fixes

Issue: High LCP (>4s)

Causes:

  • Large hero image not optimized
  • Server response time (TTFB) too high
  • Render-blocking resources

Fixes:

  • Use Next.js Image with priority for LCP image
  • Enable Cloudflare CDN for faster TTFB
  • Inline critical CSS
  • Preload fonts

Issue: High CLS (>0.25)

Causes:

  • Images without width/height
  • Web fonts loading late
  • Dynamic content injections

Fixes:

  • Set explicit dimensions on images
  • Use font-display: swap
  • Reserve space for dynamic content
  • Avoid inserting content above existing content

Issue: High FID/INP (>300ms)

Causes:

  • Heavy JavaScript execution
  • Long tasks blocking main thread
  • Unoptimized event handlers

Fixes:

  • Code split large bundles
  • Debounce/throttle event handlers
  • Use Web Workers for heavy computation
  • Optimize third-party scripts

Issue: Slow TTFB (>1s)

Causes:

  • Slow server response
  • No CDN
  • Database queries during render

Fixes:

  • Enable Cloudflare CDN
  • Use ISR instead of SSR
  • Move database queries to edge (D1)
  • Implement caching layers

Monitoring Dashboard Setup

Cloudflare Web Analytics

<!-- Add to app/layout.tsx <head> -->
<script defer src='https://static.cloudflareinsights.com/beacon.min.js'
        data-cf-beacon='{"token": "YOUR_TOKEN"}'></script>

Custom Performance Monitoring

// app/layout.tsx
'use client';

import { useEffect } from 'react';
import { usePathname } from 'next/navigation';

export function PerformanceMonitor() {
  const pathname = usePathname();

  useEffect(() => {
    // Measure Web Vitals
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
      getCLS(console.log);
      getFID(console.log);
      getFCP(console.log);
      getLCP(console.log);
      getTTFB(console.log);
    });
  }, [pathname]);

  return null;
}

Resources