Phase 6: Polish & Optimization (Weeks 21-24)

Table of Contents

  1. Overview
  2. Implementation Steps
  3. Background & Context
  4. Practical Examples
  5. Verification & Testing
  6. Troubleshooting
  7. Deployment & Monitoring

Overview

Phase 6 is the final phase focused on optimizing performance, adding modern web features, ensuring accessibility, and preparing the application for production deployment. This phase transforms the KidPix application into a modern, performant, and accessible web application.

Learning Focus: Performance optimization, PWA development, accessibility (a11y), monitoring, deployment strategies, and production best practices.

Duration: 4 weeks
Difficulty: Expert
Prerequisites: Completed Phases 1-5, understanding of web performance and accessibility principles

Implementation Steps

Step 6.1: Performance Optimization

Goal: Optimize the application for 60fps drawing and fast load times.

Create src/performance/PerformanceOptimizer.ts:

interface PerformanceMetrics {
  frameTime: number[];
  drawCalls: number;
  memoryUsage: number;
  bundleSize: number;
  loadTime: number;
}

export class PerformanceOptimizer {
  private static instance: PerformanceOptimizer;
  private metrics: PerformanceMetrics = {
    frameTime: [],
    drawCalls: 0,
    memoryUsage: 0,
    bundleSize: 0,
    loadTime: 0,
  };

  private frameStartTime = 0;
  private observer?: PerformanceObserver;

  static getInstance(): PerformanceOptimizer {
    if (!PerformanceOptimizer.instance) {
      PerformanceOptimizer.instance = new PerformanceOptimizer();
    }
    return PerformanceOptimizer.instance;
  }

  initialize(): void {
    this.setupPerformanceObserver();
    this.measureLoadTime();
    this.startFrameMonitoring();
  }

  // Canvas optimization
  optimizeCanvas(canvas: HTMLCanvasElement): void {
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    // Optimize canvas context
    ctx.imageSmoothingEnabled = false; // Pixel-perfect rendering

    // Use appropriate image smoothing quality when needed
    if ("imageSmoothingQuality" in ctx) {
      (ctx as any).imageSmoothingQuality = "low"; // Faster rendering
    }

    // Set up efficient composite operations
    ctx.globalCompositeOperation = "source-over";
  }

  // Drawing optimization with dirty rectangles
  optimizeDrawing(
    canvas: HTMLCanvasElement,
    drawFunction: (ctx: CanvasRenderingContext2D, dirtyRect?: DOMRect) => void,
    dirtyRect?: DOMRect,
  ): void {
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    this.startFrame();

    if (dirtyRect) {
      // Only clear and redraw the dirty area
      ctx.save();
      ctx.beginPath();
      ctx.rect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height);
      ctx.clip();
      ctx.clearRect(
        dirtyRect.x,
        dirtyRect.y,
        dirtyRect.width,
        dirtyRect.height,
      );

      drawFunction(ctx, dirtyRect);

      ctx.restore();
    } else {
      // Full canvas redraw
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      drawFunction(ctx);
    }

    this.endFrame();
    this.metrics.drawCalls++;
  }

  // Batch canvas operations
  batchCanvasOperations(
    canvas: HTMLCanvasElement,
    operations: Array<(ctx: CanvasRenderingContext2D) => void>,
  ): void {
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    this.startFrame();

    // Batch all operations in a single frame
    ctx.save();
    operations.forEach((operation) => operation(ctx));
    ctx.restore();

    this.endFrame();
    this.metrics.drawCalls += operations.length;
  }

  // Memory optimization
  optimizeMemory(): void {
    // Clean up unused canvases
    this.cleanupUnusedCanvases();

    // Force garbage collection if available
    if ("gc" in window) {
      (window as any).gc();
    }

    // Measure memory usage
    if ("memory" in performance) {
      this.metrics.memoryUsage = (performance as any).memory.usedJSHeapSize;
    }
  }

  // Bundle size optimization
  lazy<T>(factory: () => Promise<T>): () => Promise<T> {
    let cached: T | null = null;
    return async () => {
      if (cached === null) {
        cached = await factory();
      }
      return cached;
    };
  }

  // Performance monitoring
  private setupPerformanceObserver(): void {
    if ("PerformanceObserver" in window) {
      this.observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        entries.forEach((entry) => {
          if (entry.entryType === "measure") {
            console.log(`Performance: ${entry.name} took ${entry.duration}ms`);
          }
        });
      });

      this.observer.observe({ entryTypes: ["measure", "navigation"] });
    }
  }

  private measureLoadTime(): void {
    if ("performance" in window && "timing" in performance) {
      const timing = performance.timing;
      this.metrics.loadTime = timing.loadEventEnd - timing.navigationStart;
    }
  }

  private startFrameMonitoring(): void {
    const measureFrame = () => {
      this.startFrame();
      requestAnimationFrame(() => {
        this.endFrame();
        measureFrame();
      });
    };
    measureFrame();
  }

  private startFrame(): void {
    this.frameStartTime = performance.now();
  }

  private endFrame(): void {
    const frameTime = performance.now() - this.frameStartTime;
    this.metrics.frameTime.push(frameTime);

    // Keep only last 100 frames
    if (this.metrics.frameTime.length > 100) {
      this.metrics.frameTime.shift();
    }
  }

  private cleanupUnusedCanvases(): void {
    // Find and remove orphaned canvas elements
    const canvases = document.querySelectorAll("canvas");
    canvases.forEach((canvas) => {
      if (!canvas.parentNode) {
        canvas.remove();
      }
    });
  }

  getMetrics(): PerformanceMetrics {
    return { ...this.metrics };
  }

  getAverageFrameTime(): number {
    if (this.metrics.frameTime.length === 0) return 0;
    const sum = this.metrics.frameTime.reduce((a, b) => a + b, 0);
    return sum / this.metrics.frameTime.length;
  }

  getFPS(): number {
    const avgFrameTime = this.getAverageFrameTime();
    return avgFrameTime > 0 ? 1000 / avgFrameTime : 0;
  }
}

Create src/hooks/usePerformance.ts:

import { useEffect, useRef, useState } from "react";
import { PerformanceOptimizer } from "../performance/PerformanceOptimizer";

export function usePerformance() {
  const optimizer = useRef(PerformanceOptimizer.getInstance());
  const [metrics, setMetrics] = useState(optimizer.current.getMetrics());

  useEffect(() => {
    optimizer.current.initialize();

    const interval = setInterval(() => {
      setMetrics(optimizer.current.getMetrics());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  const optimizeCanvas = (canvas: HTMLCanvasElement) => {
    optimizer.current.optimizeCanvas(canvas);
  };

  const optimizeDrawing = (
    canvas: HTMLCanvasElement,
    drawFunction: (ctx: CanvasRenderingContext2D) => void,
    dirtyRect?: DOMRect,
  ) => {
    optimizer.current.optimizeDrawing(canvas, drawFunction, dirtyRect);
  };

  return {
    metrics,
    fps: optimizer.current.getFPS(),
    averageFrameTime: optimizer.current.getAverageFrameTime(),
    optimizeCanvas,
    optimizeDrawing,
  };
}

Step 6.2: Progressive Web App (PWA) Implementation

Goal: Transform KidPix into a PWA with offline capabilities and native app features.

Create public/manifest.json:

{
  "name": "KidPix - Creative Drawing for Kids",
  "short_name": "KidPix",
  "description": "A fun and creative drawing application for children with wacky brushes, sounds, and effects",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#ff6b6b",
  "orientation": "any",
  "categories": ["education", "entertainment", "graphics"],
  "lang": "en-US",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/desktop-1.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide",
      "label": "KidPix main drawing interface"
    },
    {
      "src": "/screenshots/mobile-1.png",
      "sizes": "390x844",
      "type": "image/png",
      "form_factor": "narrow",
      "label": "KidPix mobile interface"
    }
  ],
  "shortcuts": [
    {
      "name": "New Drawing",
      "short_name": "New",
      "description": "Start a new drawing",
      "url": "/?action=new",
      "icons": [{ "src": "/icons/new-96x96.png", "sizes": "96x96" }]
    },
    {
      "name": "Gallery",
      "short_name": "Gallery",
      "description": "View saved drawings",
      "url": "/?action=gallery",
      "icons": [{ "src": "/icons/gallery-96x96.png", "sizes": "96x96" }]
    }
  ]
}

Create public/sw.js (Service Worker):

const CACHE_NAME = "kidpix-v1.0.0";
const STATIC_CACHE = "kidpix-static-v1";
const DYNAMIC_CACHE = "kidpix-dynamic-v1";

// Files to cache immediately
const STATIC_ASSETS = [
  "/",
  "/index.html",
  "/manifest.json",
  "/static/js/bundle.js",
  "/static/css/main.css",
  // Core images and sounds
  "/assets/img/kidpix.png",
  "/assets/snd/kidpix-tool-pencil.wav",
  // Essential tool icons
  "/assets/img/tool-menu-wacky-brush-70.png",
];

// Install event - cache static assets
self.addEventListener("install", (event) => {
  console.log("Service Worker installing...");

  event.waitUntil(
    caches
      .open(STATIC_CACHE)
      .then((cache) => {
        console.log("Caching static assets");
        return cache.addAll(STATIC_ASSETS);
      })
      .then(() => {
        console.log("Static assets cached");
        return self.skipWaiting();
      })
      .catch((error) => {
        console.error("Error caching static assets:", error);
      }),
  );
});

// Activate event - clean up old caches
self.addEventListener("activate", (event) => {
  console.log("Service Worker activating...");

  event.waitUntil(
    caches
      .keys()
      .then((cacheNames) => {
        return Promise.all(
          cacheNames.map((cacheName) => {
            if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
              console.log("Deleting old cache:", cacheName);
              return caches.delete(cacheName);
            }
          }),
        );
      })
      .then(() => {
        console.log("Service Worker activated");
        return self.clients.claim();
      }),
  );
});

// Fetch event - serve from cache, fallback to network
self.addEventListener("fetch", (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // Skip cross-origin requests
  if (url.origin !== location.origin) {
    return;
  }

  // Handle different types of requests
  if (request.destination === "document") {
    // HTML pages - cache first, fallback to network
    event.respondWith(cacheFirst(request, STATIC_CACHE));
  } else if (
    request.destination === "image" ||
    request.destination === "audio"
  ) {
    // Assets - cache first with dynamic caching
    event.respondWith(cacheFirstWithDynamicCaching(request));
  } else {
    // Other resources - network first, fallback to cache
    event.respondWith(networkFirst(request));
  }
});

// Cache strategies
async function cacheFirst(request, cacheName) {
  try {
    const cache = await caches.open(cacheName);
    const cached = await cache.match(request);

    if (cached) {
      console.log("Serving from cache:", request.url);
      return cached;
    }

    console.log("Cache miss, fetching:", request.url);
    const response = await fetch(request);

    if (response.ok) {
      cache.put(request, response.clone());
    }

    return response;
  } catch (error) {
    console.error("Cache first strategy failed:", error);
    return new Response("Offline - content not available", { status: 503 });
  }
}

async function networkFirst(request) {
  try {
    console.log("Network first for:", request.url);
    const response = await fetch(request);

    if (response.ok) {
      const cache = await caches.open(DYNAMIC_CACHE);
      cache.put(request, response.clone());
    }

    return response;
  } catch (error) {
    console.log("Network failed, trying cache:", request.url);
    const cache = await caches.open(DYNAMIC_CACHE);
    const cached = await cache.match(request);

    if (cached) {
      return cached;
    }

    return new Response("Offline - content not available", { status: 503 });
  }
}

async function cacheFirstWithDynamicCaching(request) {
  try {
    // Try static cache first
    const staticCache = await caches.open(STATIC_CACHE);
    const staticCached = await staticCache.match(request);

    if (staticCached) {
      return staticCached;
    }

    // Try dynamic cache
    const dynamicCache = await caches.open(DYNAMIC_CACHE);
    const dynamicCached = await dynamicCache.match(request);

    if (dynamicCached) {
      return dynamicCached;
    }

    // Fetch and cache
    const response = await fetch(request);

    if (response.ok) {
      dynamicCache.put(request, response.clone());
    }

    return response;
  } catch (error) {
    console.error("Cache first with dynamic caching failed:", error);
    return new Response("Asset not available offline", { status: 503 });
  }
}

// Background sync for saving drawings
self.addEventListener("sync", (event) => {
  if (event.tag === "save-drawing") {
    event.waitUntil(syncDrawings());
  }
});

async function syncDrawings() {
  try {
    // Get pending drawings from IndexedDB
    const pendingDrawings = await getPendingDrawings();

    for (const drawing of pendingDrawings) {
      try {
        await uploadDrawing(drawing);
        await removePendingDrawing(drawing.id);
      } catch (error) {
        console.error("Failed to sync drawing:", error);
      }
    }
  } catch (error) {
    console.error("Background sync failed:", error);
  }
}

// Push notifications for updates
self.addEventListener("push", (event) => {
  const options = {
    body: event.data ? event.data.text() : "New features available!",
    icon: "/icons/icon-192x192.png",
    badge: "/icons/badge-72x72.png",
    tag: "kidpix-update",
    requireInteraction: false,
    actions: [
      {
        action: "open",
        title: "Open KidPix",
        icon: "/icons/open-24x24.png",
      },
      {
        action: "dismiss",
        title: "Dismiss",
        icon: "/icons/dismiss-24x24.png",
      },
    ],
  };

  event.waitUntil(self.registration.showNotification("KidPix Update", options));
});

// Handle notification clicks
self.addEventListener("notificationclick", (event) => {
  event.notification.close();

  if (event.action === "open") {
    event.waitUntil(clients.openWindow("/"));
  }
});

Create src/hooks/usePWA.ts:

import { useEffect, useState } from "react";

interface PWAState {
  isInstalled: boolean;
  isInstallable: boolean;
  isOnline: boolean;
  updateAvailable: boolean;
}

export function usePWA() {
  const [state, setState] = useState<PWAState>({
    isInstalled: false,
    isInstallable: false,
    isOnline: navigator.onLine,
    updateAvailable: false,
  });

  const [deferredPrompt, setDeferredPrompt] = useState<any>(null);

  useEffect(() => {
    // Check if running as PWA
    const isInstalled =
      window.matchMedia("(display-mode: standalone)").matches ||
      (window.navigator as any).standalone === true;

    setState((prev) => ({ ...prev, isInstalled }));

    // Listen for install prompt
    const handleBeforeInstallPrompt = (e: Event) => {
      e.preventDefault();
      setDeferredPrompt(e);
      setState((prev) => ({ ...prev, isInstallable: true }));
    };

    // Listen for app installed
    const handleAppInstalled = () => {
      setDeferredPrompt(null);
      setState((prev) => ({
        ...prev,
        isInstalled: true,
        isInstallable: false,
      }));
    };

    // Listen for online/offline
    const handleOnline = () =>
      setState((prev) => ({ ...prev, isOnline: true }));
    const handleOffline = () =>
      setState((prev) => ({ ...prev, isOnline: false }));

    // Listen for service worker updates
    const handleControllerChange = () => {
      setState((prev) => ({ ...prev, updateAvailable: true }));
    };

    window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
    window.addEventListener("appinstalled", handleAppInstalled);
    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.addEventListener(
        "controllerchange",
        handleControllerChange,
      );
    }

    return () => {
      window.removeEventListener(
        "beforeinstallprompt",
        handleBeforeInstallPrompt,
      );
      window.removeEventListener("appinstalled", handleAppInstalled);
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);

      if ("serviceWorker" in navigator) {
        navigator.serviceWorker.removeEventListener(
          "controllerchange",
          handleControllerChange,
        );
      }
    };
  }, []);

  const installApp = async () => {
    if (!deferredPrompt) return false;

    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;

    if (outcome === "accepted") {
      setDeferredPrompt(null);
      setState((prev) => ({ ...prev, isInstallable: false }));
      return true;
    }

    return false;
  };

  const reloadApp = () => {
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.getRegistration().then((registration) => {
        if (registration?.waiting) {
          registration.waiting.postMessage({ type: "SKIP_WAITING" });
        }
      });
    }
    window.location.reload();
  };

  return {
    ...state,
    installApp,
    reloadApp,
  };
}

Step 6.3: Accessibility Implementation

Goal: Ensure KidPix is accessible to users with disabilities.

Create src/accessibility/AccessibilityManager.ts:

interface AccessibilityOptions {
  highContrast: boolean;
  reduceMotion: boolean;
  fontSize: "small" | "medium" | "large";
  keyboardNavigation: boolean;
  screenReader: boolean;
  colorBlindFriendly: boolean;
}

export class AccessibilityManager {
  private static instance: AccessibilityManager;
  private options: AccessibilityOptions;
  private announcer: HTMLElement;

  private constructor() {
    this.options = this.loadOptions();
    this.announcer = this.createAnnouncer();
    this.detectPreferences();
    this.applyOptions();
  }

  static getInstance(): AccessibilityManager {
    if (!AccessibilityManager.instance) {
      AccessibilityManager.instance = new AccessibilityManager();
    }
    return AccessibilityManager.instance;
  }

  // Screen reader announcements
  announce(message: string, priority: "polite" | "assertive" = "polite"): void {
    this.announcer.setAttribute("aria-live", priority);
    this.announcer.textContent = message;

    // Clear after announcement
    setTimeout(() => {
      this.announcer.textContent = "";
    }, 1000);
  }

  // Keyboard navigation support
  setupKeyboardNavigation(): void {
    document.addEventListener("keydown", this.handleKeyboard.bind(this));

    // Add visible focus indicators
    const style = document.createElement("style");
    style.textContent = `
      .keyboard-navigation *:focus {
        outline: 3px solid #4A90E2 !important;
        outline-offset: 2px !important;
      }

      .keyboard-navigation .tool-button:focus {
        background-color: #E3F2FD !important;
        border: 2px solid #2196F3 !important;
      }
    `;
    document.head.appendChild(style);
  }

  // High contrast mode
  setHighContrast(enabled: boolean): void {
    this.options.highContrast = enabled;

    if (enabled) {
      document.body.classList.add("high-contrast");
      this.injectHighContrastStyles();
    } else {
      document.body.classList.remove("high-contrast");
    }

    this.saveOptions();
    this.announce(`High contrast ${enabled ? "enabled" : "disabled"}`);
  }

  // Reduce motion
  setReduceMotion(enabled: boolean): void {
    this.options.reduceMotion = enabled;

    if (enabled) {
      document.body.classList.add("reduce-motion");
      this.injectReduceMotionStyles();
    } else {
      document.body.classList.remove("reduce-motion");
    }

    this.saveOptions();
    this.announce(`Animations ${enabled ? "reduced" : "restored"}`);
  }

  // Font size adjustment
  setFontSize(size: "small" | "medium" | "large"): void {
    this.options.fontSize = size;

    document.body.classList.remove("font-small", "font-medium", "font-large");
    document.body.classList.add(`font-${size}`);

    this.injectFontSizeStyles();
    this.saveOptions();
    this.announce(`Font size set to ${size}`);
  }

  // Color blind friendly colors
  setColorBlindFriendly(enabled: boolean): void {
    this.options.colorBlindFriendly = enabled;

    if (enabled) {
      document.body.classList.add("color-blind-friendly");
      this.injectColorBlindStyles();
    } else {
      document.body.classList.remove("color-blind-friendly");
    }

    this.saveOptions();
    this.announce(
      `Color blind friendly mode ${enabled ? "enabled" : "disabled"}`,
    );
  }

  // Canvas accessibility
  makeCanvasAccessible(canvas: HTMLCanvasElement, description: string): void {
    canvas.setAttribute("role", "img");
    canvas.setAttribute("aria-label", description);
    canvas.setAttribute("tabindex", "0");

    // Add keyboard interaction
    canvas.addEventListener("keydown", (e) => {
      if (e.key === "Enter" || e.key === " ") {
        const rect = canvas.getBoundingClientRect();
        const centerX = rect.width / 2;
        const centerY = rect.height / 2;

        // Simulate click at center
        const clickEvent = new MouseEvent("click", {
          clientX: rect.left + centerX,
          clientY: rect.top + centerY,
          bubbles: true,
        });
        canvas.dispatchEvent(clickEvent);

        this.announce("Canvas activated");
      }
    });
  }

  // Tool accessibility
  makeToolAccessible(
    element: HTMLElement,
    toolName: string,
    description: string,
  ): void {
    element.setAttribute("role", "button");
    element.setAttribute("aria-label", `${toolName}: ${description}`);
    element.setAttribute("tabindex", "0");

    // Add keyboard activation
    element.addEventListener("keydown", (e) => {
      if (e.key === "Enter" || e.key === " ") {
        e.preventDefault();
        element.click();
        this.announce(`${toolName} selected`);
      }
    });
  }

  // Detect user preferences
  private detectPreferences(): void {
    // Check for prefers-reduced-motion
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      this.options.reduceMotion = true;
    }

    // Check for prefers-contrast
    if (window.matchMedia("(prefers-contrast: high)").matches) {
      this.options.highContrast = true;
    }

    // Check for screen reader
    this.options.screenReader = this.detectScreenReader();
  }

  private detectScreenReader(): boolean {
    // Simple screen reader detection
    return !!(
      navigator.userAgent.match(/NVDA|JAWS|VoiceOver|ChromeVox/i) ||
      window.speechSynthesis ||
      document.querySelector("[aria-live]")
    );
  }

  private handleKeyboard(e: KeyboardEvent): void {
    // Global keyboard shortcuts
    if (e.ctrlKey || e.metaKey) {
      switch (e.key) {
        case "z":
          e.preventDefault();
          this.triggerUndo();
          break;
        case "y":
          e.preventDefault();
          this.triggerRedo();
          break;
        case "s":
          e.preventDefault();
          this.triggerSave();
          break;
        case "n":
          e.preventDefault();
          this.triggerNew();
          break;
      }
    }

    // Tool navigation
    if (e.key >= "1" && e.key <= "9") {
      const toolIndex = parseInt(e.key) - 1;
      this.selectToolByIndex(toolIndex);
    }
  }

  private triggerUndo(): void {
    const event = new CustomEvent("kidpix:undo");
    document.dispatchEvent(event);
    this.announce("Undo");
  }

  private triggerRedo(): void {
    const event = new CustomEvent("kidpix:redo");
    document.dispatchEvent(event);
    this.announce("Redo");
  }

  private triggerSave(): void {
    const event = new CustomEvent("kidpix:save");
    document.dispatchEvent(event);
    this.announce("Saving drawing");
  }

  private triggerNew(): void {
    const event = new CustomEvent("kidpix:new");
    document.dispatchEvent(event);
    this.announce("New drawing started");
  }

  private selectToolByIndex(index: number): void {
    const tools = document.querySelectorAll(".tool-button");
    if (tools[index]) {
      (tools[index] as HTMLElement).click();
    }
  }

  private createAnnouncer(): HTMLElement {
    const announcer = document.createElement("div");
    announcer.setAttribute("aria-live", "polite");
    announcer.setAttribute("aria-atomic", "true");
    announcer.className = "sr-only";
    announcer.style.cssText = `
      position: absolute !important;
      width: 1px !important;
      height: 1px !important;
      padding: 0 !important;
      margin: -1px !important;
      overflow: hidden !important;
      clip: rect(0, 0, 0, 0) !important;
      white-space: nowrap !important;
      border: 0 !important;
    `;
    document.body.appendChild(announcer);
    return announcer;
  }

  private injectHighContrastStyles(): void {
    const style = document.createElement("style");
    style.id = "high-contrast-styles";
    style.textContent = `
      .high-contrast {
        filter: contrast(150%) brightness(110%);
      }

      .high-contrast .tool-button {
        border: 2px solid #000 !important;
        background-color: #fff !important;
        color: #000 !important;
      }

      .high-contrast .tool-button.active {
        background-color: #000 !important;
        color: #fff !important;
      }

      .high-contrast canvas {
        border: 3px solid #000 !important;
      }
    `;
    document.head.appendChild(style);
  }

  private injectReduceMotionStyles(): void {
    const style = document.createElement("style");
    style.id = "reduce-motion-styles";
    style.textContent = `
      .reduce-motion *,
      .reduce-motion *::before,
      .reduce-motion *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
      }
    `;
    document.head.appendChild(style);
  }

  private injectFontSizeStyles(): void {
    const sizes = {
      small: "14px",
      medium: "16px",
      large: "20px",
    };

    const style = document.createElement("style");
    style.id = "font-size-styles";
    style.textContent = `
      .font-${this.options.fontSize} {
        font-size: ${sizes[this.options.fontSize]} !important;
      }

      .font-${this.options.fontSize} .tool-button {
        font-size: ${sizes[this.options.fontSize]} !important;
        padding: ${this.options.fontSize === "large" ? "12px" : "8px"} !important;
      }
    `;
    document.head.appendChild(style);
  }

  private injectColorBlindStyles(): void {
    const style = document.createElement("style");
    style.id = "color-blind-styles";
    style.textContent = `
      .color-blind-friendly .color-red { background-color: #d73027 !important; }
      .color-blind-friendly .color-green { background-color: #1a9850 !important; }
      .color-blind-friendly .color-blue { background-color: #313695 !important; }
      .color-blind-friendly .color-yellow { background-color: #f46d43 !important; }
      .color-blind-friendly .color-orange { background-color: #a50026 !important; }
      .color-blind-friendly .color-purple { background-color: #762a83 !important; }
    `;
    document.head.appendChild(style);
  }

  private applyOptions(): void {
    if (this.options.highContrast) this.setHighContrast(true);
    if (this.options.reduceMotion) this.setReduceMotion(true);
    if (this.options.keyboardNavigation) this.setupKeyboardNavigation();
    if (this.options.colorBlindFriendly) this.setColorBlindFriendly(true);
    this.setFontSize(this.options.fontSize);
  }

  private loadOptions(): AccessibilityOptions {
    const saved = localStorage.getItem("kidpix-accessibility");
    if (saved) {
      return { ...this.getDefaultOptions(), ...JSON.parse(saved) };
    }
    return this.getDefaultOptions();
  }

  private saveOptions(): void {
    localStorage.setItem("kidpix-accessibility", JSON.stringify(this.options));
  }

  private getDefaultOptions(): AccessibilityOptions {
    return {
      highContrast: false,
      reduceMotion: false,
      fontSize: "medium",
      keyboardNavigation: true,
      screenReader: false,
      colorBlindFriendly: false,
    };
  }

  getOptions(): AccessibilityOptions {
    return { ...this.options };
  }
}

Step 6.4: Bundle Optimization and Code Splitting

Goal: Optimize bundle size and implement code splitting for faster load times.

Update vite.config.ts for optimization:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { visualizer } from "rollup-plugin-visualizer";

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      filename: "dist/bundle-analysis.html",
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
  build: {
    target: "es2015",
    outDir: "dist",
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          // Vendor libraries
          "vendor-react": ["react", "react-dom"],

          // Audio libraries
          "vendor-audio": ["tone", "web-audio-api"],

          // Canvas utilities
          "vendor-canvas": ["konva", "fabric"],

          // Tool groups
          "tools-basic": [
            "./src/tools/PencilTool",
            "./src/tools/EraserTool",
            "./src/tools/LineTool",
          ],

          "tools-shapes": [
            "./src/tools/CircleTool",
            "./src/tools/SquareTool",
            "./src/tools/PolygonTool",
          ],

          "tools-brushes": [
            "./src/tools/BrushTool",
            "./src/brushes/BubblesBrush",
            "./src/brushes/SplatterBrush",
          ],

          "tools-effects": [
            "./src/tools/EffectsTool",
            "./src/tools/FilterTool",
            "./src/textures/NoiseTexture",
          ],
        },
      },
    },

    // Compression and optimization
    minify: "terser",
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },

    // Asset optimization
    assetsInlineLimit: 4096, // 4kb
    cssCodeSplit: true,

    // Performance hints
    chunkSizeWarningLimit: 1000, // 1MB
  },

  // Development optimizations
  server: {
    host: true,
    port: 5173,
    open: true,
  },

  // Performance monitoring
  esbuild: {
    target: "es2015",
    drop: ["console", "debugger"], // Remove in production
  },
});

Create src/utils/LazyLoad.tsx:

import React, { Suspense, lazy } from 'react';

// Lazy loading wrapper with error boundary
interface LazyComponentProps {
  children: React.ReactNode;
  fallback?: React.ReactNode;
  errorFallback?: React.ReactNode;
}

class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ReactNode },
  { hasError: boolean }
> {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(): { hasError: boolean } {
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Lazy load error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }

    return this.props.children;
  }
}

export const LazyComponent: React.FC<LazyComponentProps> = ({
  children,
  fallback = <div>Loading...</div>,
  errorFallback = <div>Error loading component</div>
}) => {
  return (
    <ErrorBoundary fallback={errorFallback}>
      <Suspense fallback={fallback}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
};

// Preloading utility
export const preloadComponent = (importFunc: () => Promise<any>) => {
  const componentImport = importFunc();

  // Optionally, you can preload on mouse enter or other events
  return componentImport;
};

// Lazy load with preloading
export const lazyWithPreload = (importFunc: () => Promise<any>) => {
  const LazyComponent = lazy(importFunc);

  // Add preload method to component
  (LazyComponent as any).preload = () => preloadComponent(importFunc);

  return LazyComponent;
};

// Usage examples:
// Basic lazy loading
export const LazyToolsPanel = lazy(() => import('../components/ToolsPanel'));

// Lazy loading with preload capability
export const LazyBrushSelector = lazyWithPreload(() => import('../components/BrushSelector'));

// Preload on hover
export const PreloadOnHover: React.FC<{ children: React.ReactNode; preload: () => void }> = ({
  children,
  preload
}) => {
  return (
    <div onMouseEnter={preload}>
      {children}
    </div>
  );
};

Step 6.5: Monitoring and Analytics

Goal: Implement performance monitoring and user analytics.

Create src/monitoring/MonitoringManager.ts:

interface PerformanceEntry {
  name: string;
  duration: number;
  timestamp: number;
  metadata?: Record<string, any>;
}

interface UserInteraction {
  type: "tool_select" | "draw" | "save" | "load" | "error";
  tool?: string;
  duration?: number;
  timestamp: number;
  metadata?: Record<string, any>;
}

interface ErrorReport {
  message: string;
  stack?: string;
  url: string;
  line?: number;
  column?: number;
  timestamp: number;
  userAgent: string;
  metadata?: Record<string, any>;
}

export class MonitoringManager {
  private static instance: MonitoringManager;
  private performanceEntries: PerformanceEntry[] = [];
  private userInteractions: UserInteraction[] = [];
  private errorReports: ErrorReport[] = [];
  private sessionId: string;
  private userId?: string;

  private constructor() {
    this.sessionId = this.generateSessionId();
    this.setupErrorHandling();
    this.setupPerformanceObserver();
    this.startSessionTracking();
  }

  static getInstance(): MonitoringManager {
    if (!MonitoringManager.instance) {
      MonitoringManager.instance = new MonitoringManager();
    }
    return MonitoringManager.instance;
  }

  // Performance tracking
  trackPerformance(
    name: string,
    duration: number,
    metadata?: Record<string, any>,
  ): void {
    const entry: PerformanceEntry = {
      name,
      duration,
      timestamp: Date.now(),
      metadata,
    };

    this.performanceEntries.push(entry);

    // Send to analytics if critical performance issue
    if (duration > 100) {
      // Slow operation
      this.sendToAnalytics("performance_slow", entry);
    }
  }

  // User interaction tracking
  trackInteraction(interaction: Omit<UserInteraction, "timestamp">): void {
    const fullInteraction: UserInteraction = {
      ...interaction,
      timestamp: Date.now(),
    };

    this.userInteractions.push(fullInteraction);
    this.sendToAnalytics("user_interaction", fullInteraction);
  }

  // Error tracking
  trackError(error: Error, metadata?: Record<string, any>): void {
    const errorReport: ErrorReport = {
      message: error.message,
      stack: error.stack,
      url: window.location.href,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      metadata,
    };

    this.errorReports.push(errorReport);
    this.sendToAnalytics("error", errorReport);
  }

  // Custom event tracking
  trackEvent(eventName: string, properties?: Record<string, any>): void {
    this.sendToAnalytics(eventName, {
      ...properties,
      timestamp: Date.now(),
      sessionId: this.sessionId,
      userId: this.userId,
    });
  }

  // Feature usage tracking
  trackFeatureUsage(
    feature: string,
    action: string,
    metadata?: Record<string, any>,
  ): void {
    this.trackEvent("feature_usage", {
      feature,
      action,
      ...metadata,
    });
  }

  // Performance metrics
  getPerformanceMetrics(): {
    averageFrameTime: number;
    slowOperations: PerformanceEntry[];
    memoryUsage?: number;
    bundleSize?: number;
  } {
    const frameTimes = this.performanceEntries
      .filter((entry) => entry.name === "frame_time")
      .map((entry) => entry.duration);

    const averageFrameTime =
      frameTimes.length > 0
        ? frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length
        : 0;

    const slowOperations = this.performanceEntries
      .filter((entry) => entry.duration > 50)
      .sort((a, b) => b.duration - a.duration);

    const metrics = {
      averageFrameTime,
      slowOperations,
    } as any;

    // Add memory usage if available
    if ("memory" in performance) {
      metrics.memoryUsage = (performance as any).memory.usedJSHeapSize;
    }

    return metrics;
  }

  // Session management
  setUserId(userId: string): void {
    this.userId = userId;
  }

  getSessionId(): string {
    return this.sessionId;
  }

  // Data export for debugging
  exportData(): {
    sessionId: string;
    userId?: string;
    performance: PerformanceEntry[];
    interactions: UserInteraction[];
    errors: ErrorReport[];
    metrics: any;
  } {
    return {
      sessionId: this.sessionId,
      userId: this.userId,
      performance: this.performanceEntries,
      interactions: this.userInteractions,
      errors: this.errorReports,
      metrics: this.getPerformanceMetrics(),
    };
  }

  private setupErrorHandling(): void {
    // Global error handler
    window.addEventListener("error", (event) => {
      this.trackError(new Error(event.message), {
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
      });
    });

    // Unhandled promise rejections
    window.addEventListener("unhandledrejection", (event) => {
      this.trackError(
        new Error(`Unhandled promise rejection: ${event.reason}`),
        {
          type: "unhandled_promise_rejection",
        },
      );
    });

    // React error boundary integration
    window.addEventListener("react-error", (event: any) => {
      this.trackError(event.detail.error, {
        componentStack: event.detail.componentStack,
        type: "react_error",
      });
    });
  }

  private setupPerformanceObserver(): void {
    if ("PerformanceObserver" in window) {
      const observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();

        entries.forEach((entry) => {
          this.trackPerformance(entry.name, entry.duration, {
            entryType: entry.entryType,
            startTime: entry.startTime,
          });
        });
      });

      observer.observe({
        entryTypes: ["measure", "navigation", "resource", "paint"],
      });
    }
  }

  private startSessionTracking(): void {
    // Track session start
    this.trackEvent("session_start", {
      url: window.location.href,
      referrer: document.referrer,
      userAgent: navigator.userAgent,
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight,
      },
    });

    // Track session end
    window.addEventListener("beforeunload", () => {
      this.trackEvent("session_end", {
        duration: Date.now() - parseInt(this.sessionId),
        interactions: this.userInteractions.length,
        errors: this.errorReports.length,
      });
    });

    // Track page visibility changes
    document.addEventListener("visibilitychange", () => {
      this.trackEvent(document.hidden ? "page_hidden" : "page_visible", {
        timestamp: Date.now(),
      });
    });
  }

  private generateSessionId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  private async sendToAnalytics(eventType: string, data: any): Promise<void> {
    // Only send analytics in production or when explicitly enabled
    if (
      process.env.NODE_ENV !== "production" &&
      !window.localStorage.getItem("kidpix-analytics-debug")
    ) {
      console.log("Analytics (debug):", eventType, data);
      return;
    }

    try {
      // Send to your analytics service
      await fetch("/api/analytics", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          eventType,
          data,
          sessionId: this.sessionId,
          userId: this.userId,
          timestamp: Date.now(),
        }),
      });
    } catch (error) {
      console.error("Failed to send analytics:", error);
    }
  }
}

Step 6.6: Testing and Quality Assurance

Goal: Comprehensive testing strategy for production readiness.

Create src/testing/TestUtils.tsx:

import React from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { KidPixProvider } from '../contexts/KidPixContext';
import { AccessibilityManager } from '../accessibility/AccessibilityManager';

// Mock canvas context for testing
export const mockCanvasContext = () => {
  const mockCtx = {
    fillRect: jest.fn(),
    clearRect: jest.fn(),
    getImageData: jest.fn(() => ({
      data: new Uint8ClampedArray(4),
      width: 1,
      height: 1
    })),
    putImageData: jest.fn(),
    createImageData: jest.fn(() => ({
      data: new Uint8ClampedArray(4),
      width: 1,
      height: 1
    })),
    drawImage: jest.fn(),
    beginPath: jest.fn(),
    moveTo: jest.fn(),
    lineTo: jest.fn(),
    stroke: jest.fn(),
    fill: jest.fn(),
    arc: jest.fn(),
    save: jest.fn(),
    restore: jest.fn(),
    translate: jest.fn(),
    rotate: jest.fn(),
    scale: jest.fn(),
    setTransform: jest.fn(),
    createPattern: jest.fn(() => ({})),
    createLinearGradient: jest.fn(() => ({
      addColorStop: jest.fn()
    })),
    createRadialGradient: jest.fn(() => ({
      addColorStop: jest.fn()
    }))
  };

  HTMLCanvasElement.prototype.getContext = jest.fn(() => mockCtx);
  return mockCtx;
};

// Mock Web Audio API
export const mockWebAudio = () => {
  const mockAudioContext = {
    createBufferSource: jest.fn(() => ({
      buffer: null,
      connect: jest.fn(),
      start: jest.fn(),
      stop: jest.fn(),
      onended: null
    })),
    createGain: jest.fn(() => ({
      gain: {
        setValueAtTime: jest.fn(),
        linearRampToValueAtTime: jest.fn(),
        value: 1
      },
      connect: jest.fn()
    })),
    decodeAudioData: jest.fn(() => Promise.resolve({})),
    destination: {},
    currentTime: 0,
    state: 'running',
    resume: jest.fn(() => Promise.resolve())
  };

  (global as any).AudioContext = jest.fn(() => mockAudioContext);
  (global as any).webkitAudioContext = jest.fn(() => mockAudioContext);

  return mockAudioContext;
};

// Mock IntersectionObserver
export const mockIntersectionObserver = () => {
  const mockIntersectionObserver = jest.fn();
  mockIntersectionObserver.mockReturnValue({
    observe: () => null,
    unobserve: () => null,
    disconnect: () => null
  });

  (global as any).IntersectionObserver = mockIntersectionObserver;
  return mockIntersectionObserver;
};

// Test wrapper with providers
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
  initialState?: any;
}

export const renderWithProviders = (
  ui: React.ReactElement,
  options: CustomRenderOptions = {}
) => {
  const { initialState, ...renderOptions } = options;

  const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
    return (
      <KidPixProvider>
        {children}
      </KidPixProvider>
    );
  };

  return render(ui, { wrapper: Wrapper, ...renderOptions });
};

// Performance testing utilities
export const measurePerformance = async (
  fn: () => Promise<void> | void,
  iterations = 100
): Promise<{
  average: number;
  min: number;
  max: number;
  total: number;
}> => {
  const times: number[] = [];

  for (let i = 0; i < iterations; i++) {
    const start = performance.now();
    await fn();
    const end = performance.now();
    times.push(end - start);
  }

  const total = times.reduce((sum, time) => sum + time, 0);
  const average = total / times.length;
  const min = Math.min(...times);
  const max = Math.max(...times);

  return { average, min, max, total };
};

// Accessibility testing helpers
export const checkAccessibility = async (container: HTMLElement): Promise<{
  violations: any[];
  passes: any[];
}> => {
  // Mock axe-core for testing
  const axe = await import('axe-core');
  const results = await axe.run(container);

  return {
    violations: results.violations,
    passes: results.passes
  };
};

// Visual regression testing helpers
export const captureScreenshot = async (element: HTMLElement): Promise<string> => {
  // Mock implementation - in real scenario you'd use a proper screenshot library
  return Promise.resolve('mock-screenshot-data');
};

export const compareScreenshots = (
  baseline: string,
  current: string,
  threshold = 0.1
): { match: boolean; difference: number } => {
  // Mock implementation
  return { match: true, difference: 0 };
};

// Test data generators
export const generateMockDrawingData = (pointCount = 10) => {
  const points = [];
  for (let i = 0; i < pointCount; i++) {
    points.push({
      x: Math.random() * 640,
      y: Math.random() * 480,
      pressure: Math.random(),
      timestamp: Date.now() + i
    });
  }
  return points;
};

export const generateMockToolConfig = (overrides = {}) => ({
  name: 'Test Tool',
  icon: '๐Ÿงช',
  category: 'basic' as const,
  cursor: 'crosshair',
  sounds: {
    start: 'test-sound.wav'
  },
  ...overrides
});

// Setup and teardown helpers
export const setupTestEnvironment = () => {
  // Mock all necessary APIs
  mockCanvasContext();
  mockWebAudio();
  mockIntersectionObserver();

  // Initialize accessibility manager
  AccessibilityManager.getInstance();

  // Suppress console warnings in tests
  const originalWarn = console.warn;
  console.warn = jest.fn();

  return () => {
    console.warn = originalWarn;
    jest.clearAllMocks();
  };
};

Background & Context

Progressive Web Apps (PWAs)

What is a PWA?: A web application that provides native app-like experiences using modern web capabilities.

Key PWA Features:

  • Service Workers: Background scripts for offline functionality
  • Web App Manifest: Metadata for installation and app behavior
  • HTTPS: Required for security and PWA features
  • Responsive Design: Works on all devices and screen sizes

Service Worker Strategies:

// Cache First (good for static assets)
async function cacheFirst(request) {
  const cached = await caches.match(request);
  return cached || fetch(request);
}

// Network First (good for API calls)
async function networkFirst(request) {
  try {
    const response = await fetch(request);
    // Cache successful responses
    return response;
  } catch (error) {
    return caches.match(request);
  }
}

// Stale While Revalidate (best of both worlds)
async function staleWhileRevalidate(request) {
  const cached = await caches.match(request);
  const fetchPromise = fetch(request).then((response) => {
    cache.put(request, response.clone());
    return response;
  });

  return cached || fetchPromise;
}

Web App Manifest Benefits:

  • App-like installation experience
  • Splash screen customization
  • Orientation control
  • Display mode options (fullscreen, standalone, minimal-ui)

Web Accessibility (A11Y)

WCAG 2.1 Guidelines:

  • Perceivable: Information must be presentable in ways users can perceive
  • Operable: Interface components must be operable
  • Understandable: Information and UI operation must be understandable
  • Robust: Content must be robust enough for various assistive technologies

Key Accessibility Features:

<!-- Semantic HTML -->
<button aria-label="Save drawing" aria-describedby="save-help">๐Ÿ’พ Save</button>
<div id="save-help">Saves your current drawing to local storage</div>

<!-- Keyboard navigation -->
<div role="toolbar" aria-label="Drawing tools">
  <button role="button" tabindex="0" aria-pressed="false">Pencil</button>
  <button role="button" tabindex="-1" aria-pressed="false">Brush</button>
</div>

<!-- Live regions for screen readers -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
  <!-- Dynamic announcements -->
</div>

<!-- Canvas accessibility -->
<canvas role="img" aria-label="Drawing canvas" tabindex="0">
  <!-- Fallback content -->
  <p>Interactive drawing canvas. Use keyboard shortcuts to draw.</p>
</canvas>

Screen Reader Support:

// Announce tool changes
const announcer = document.querySelector("[aria-live]");
announcer.textContent = `Selected ${toolName}`;

// Describe canvas content
canvas.setAttribute(
  "aria-label",
  `Drawing with ${brushCount} brush strokes using ${toolName}`,
);

// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
  if (e.key === "Enter" && e.target === canvas) {
    // Activate drawing at canvas center
    simulateClick(canvas.width / 2, canvas.height / 2);
  }
});

Performance Optimization

Bundle Analysis and Optimization:

// Code splitting by route
const LazyHome = lazy(() => import("./components/Home"));
const LazyGallery = lazy(() => import("./components/Gallery"));

// Code splitting by feature
const LazyAdvancedTools = lazy(() =>
  import("./components/AdvancedTools").then((module) => ({
    default: module.AdvancedTools,
  })),
);

// Dynamic imports for conditional features
const loadImageEditor = async () => {
  if (userHasPremium) {
    const { ImageEditor } = await import("./components/ImageEditor");
    return ImageEditor;
  }
  return null;
};

Canvas Performance:

// Dirty rectangle optimization
const dirtyRects: DOMRect[] = [];

function markDirty(x: number, y: number, width: number, height: number) {
  dirtyRects.push(new DOMRect(x, y, width, height));
}

function redraw(ctx: CanvasRenderingContext2D) {
  dirtyRects.forEach((rect) => {
    ctx.clearRect(rect.x, rect.y, rect.width, rect.height);
    // Redraw only affected area
    drawInRegion(ctx, rect);
  });
  dirtyRects.length = 0;
}

// OffscreenCanvas for background processing
const offscreen = new OffscreenCanvas(800, 600);
const offscreenCtx = offscreen.getContext("2d");

// Process expensive operations off main thread
const worker = new Worker("canvas-worker.js");
worker.postMessage({ canvas: offscreen }, [offscreen]);

Memory Management:

// Canvas cleanup
function cleanupCanvas(canvas: HTMLCanvasElement) {
  const ctx = canvas.getContext("2d");
  ctx?.clearRect(0, 0, canvas.width, canvas.height);

  // Reset canvas size to free memory
  canvas.width = 1;
  canvas.height = 1;
  canvas.width = originalWidth;
  canvas.height = originalHeight;
}

// Audio cleanup
function cleanupAudio(audioBuffer: AudioBuffer) {
  // AudioBuffers can't be explicitly freed, but remove references
  audioBuffer = null;

  // Force garbage collection if available
  if ("gc" in window) {
    (window as any).gc();
  }
}

// Event listener cleanup
class ComponentManager {
  private cleanupFunctions: (() => void)[] = [];

  addCleanup(cleanup: () => void) {
    this.cleanupFunctions.push(cleanup);
  }

  cleanup() {
    this.cleanupFunctions.forEach((fn) => fn());
    this.cleanupFunctions = [];
  }
}

Monitoring and Analytics

Performance Metrics:

// Core Web Vitals
function measureWebVitals() {
  // Largest Contentful Paint
  new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];
    console.log("LCP:", lastEntry.startTime);
  }).observe({ type: "largest-contentful-paint", buffered: true });

  // First Input Delay
  new PerformanceObserver((list) => {
    const entries = list.getEntries();
    entries.forEach((entry) => {
      console.log("FID:", entry.processingStart - entry.startTime);
    });
  }).observe({ type: "first-input", buffered: true });

  // Cumulative Layout Shift
  new PerformanceObserver((list) => {
    let clsValue = 0;
    list.getEntries().forEach((entry) => {
      if (!entry.hadRecentInput) {
        clsValue += entry.value;
      }
    });
    console.log("CLS:", clsValue);
  }).observe({ type: "layout-shift", buffered: true });
}

User Behavior Analytics:

// Tool usage analytics
function trackToolUsage(toolName: string, duration: number) {
  analytics.track("tool_used", {
    tool: toolName,
    duration,
    timestamp: Date.now(),
    sessionId: getSessionId(),
  });
}

// Canvas interaction heatmap
function trackCanvasInteraction(x: number, y: number) {
  // Normalize coordinates to percentage
  const normalizedX = (x / canvas.width) * 100;
  const normalizedY = (y / canvas.height) * 100;

  analytics.track("canvas_interaction", {
    x: normalizedX,
    y: normalizedY,
    tool: currentTool,
    timestamp: Date.now(),
  });
}

// Error tracking with context
function trackErrorWithContext(error: Error) {
  analytics.track("error", {
    message: error.message,
    stack: error.stack,
    url: window.location.href,
    userAgent: navigator.userAgent,
    timestamp: Date.now(),
    context: {
      currentTool,
      canvasSize: { width: canvas.width, height: canvas.height },
      memoryUsage: getMemoryUsage(),
    },
  });
}

Practical Examples

Example 1: Complete PWA Integration

// PWA App Component
const PWAApp: React.FC = () => {
  const { isInstallable, isOnline, updateAvailable, installApp, reloadApp } = usePWA();
  const [showInstallPrompt, setShowInstallPrompt] = useState(false);
  const [showUpdatePrompt, setShowUpdatePrompt] = useState(false);

  useEffect(() => {
    if (isInstallable && !localStorage.getItem('install-prompt-dismissed')) {
      setShowInstallPrompt(true);
    }
  }, [isInstallable]);

  useEffect(() => {
    if (updateAvailable) {
      setShowUpdatePrompt(true);
    }
  }, [updateAvailable]);

  const handleInstall = async () => {
    const installed = await installApp();
    if (installed) {
      setShowInstallPrompt(false);
      localStorage.setItem('install-prompt-dismissed', 'true');
    }
  };

  const handleUpdate = () => {
    reloadApp();
  };

  return (
    <div className="pwa-app">
      {/* Offline indicator */}
      {!isOnline && (
        <div className="offline-banner">
          โš ๏ธ You're offline. Some features may not be available.
        </div>
      )}

      {/* Install prompt */}
      {showInstallPrompt && (
        <div className="install-prompt">
          <p>Install KidPix for a better experience!</p>
          <button onClick={handleInstall}>Install</button>
          <button onClick={() => setShowInstallPrompt(false)}>Dismiss</button>
        </div>
      )}

      {/* Update prompt */}
      {showUpdatePrompt && (
        <div className="update-prompt">
          <p>A new version of KidPix is available!</p>
          <button onClick={handleUpdate}>Update Now</button>
          <button onClick={() => setShowUpdatePrompt(false)}>Later</button>
        </div>
      )}

      <KidPixApp />
    </div>
  );
};

Example 2: Accessibility-First Component

// Accessible Tool Button
const AccessibleToolButton: React.FC<{
  tool: ToolConfig;
  isActive: boolean;
  onSelect: () => void;
  index: number;
}> = ({ tool, isActive, onSelect, index }) => {
  const accessibilityManager = AccessibilityManager.getInstance();
  const buttonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (buttonRef.current) {
      accessibilityManager.makeToolAccessible(
        buttonRef.current,
        tool.name,
        `${tool.name} drawing tool. Press Enter or Space to select.`
      );
    }
  }, [tool, accessibilityManager]);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    // Number key shortcuts
    if (e.key === (index + 1).toString()) {
      onSelect();
      accessibilityManager.announce(`${tool.name} selected`);
    }

    // Arrow key navigation
    const toolButtons = document.querySelectorAll('.tool-button');
    const currentIndex = Array.from(toolButtons).indexOf(e.currentTarget);

    let nextIndex = currentIndex;
    if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
      nextIndex = (currentIndex + 1) % toolButtons.length;
    } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
      nextIndex = (currentIndex - 1 + toolButtons.length) % toolButtons.length;
    }

    if (nextIndex !== currentIndex) {
      e.preventDefault();
      (toolButtons[nextIndex] as HTMLElement).focus();
    }
  };

  return (
    <button
      ref={buttonRef}
      className={`tool-button ${isActive ? 'active' : ''}`}
      onClick={onSelect}
      onKeyDown={handleKeyDown}
      aria-pressed={isActive}
      aria-describedby={`tool-${tool.name.toLowerCase()}-desc`}
      data-testid={`tool-${tool.name.toLowerCase()}`}
    >
      <span className="tool-icon" aria-hidden="true">{tool.icon}</span>
      <span className="tool-name">{tool.name}</span>
      <span className="keyboard-shortcut" aria-hidden="true">
        {index + 1}
      </span>

      <div
        id={`tool-${tool.name.toLowerCase()}-desc`}
        className="sr-only"
      >
        Press {index + 1} to select {tool.name}.
        {tool.sounds?.start && 'Includes sound effects.'}
      </div>
    </button>
  );
};

Example 3: Performance-Optimized Drawing

// High-performance drawing component
const OptimizedCanvas: React.FC = () => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const { optimizeCanvas, optimizeDrawing } = usePerformance();
  const animationFrameRef = useRef<number>();
  const dirtyRegions = useRef<DOMRect[]>([]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (canvas) {
      optimizeCanvas(canvas);
      startRenderLoop();
    }

    return () => {
      if (animationFrameRef.current) {
        cancelAnimationFrame(animationFrameRef.current);
      }
    };
  }, []);

  const markRegionDirty = useCallback((x: number, y: number, width: number, height: number) => {
    dirtyRegions.current.push(new DOMRect(x, y, width, height));
  }, []);

  const optimizedDraw = useCallback((ctx: CanvasRenderingContext2D) => {
    // Only redraw dirty regions
    dirtyRegions.current.forEach(region => {
      optimizeDrawing(canvasRef.current!, (ctx, dirtyRect) => {
        if (dirtyRect) {
          // Draw only in dirty region
          drawInRegion(ctx, dirtyRect);
        }
      }, region);
    });

    // Clear dirty regions
    dirtyRegions.current = [];
  }, [optimizeDrawing]);

  const startRenderLoop = useCallback(() => {
    const render = () => {
      const canvas = canvasRef.current;
      const ctx = canvas?.getContext('2d');

      if (ctx && dirtyRegions.current.length > 0) {
        optimizedDraw(ctx);
      }

      animationFrameRef.current = requestAnimationFrame(render);
    };

    render();
  }, [optimizedDraw]);

  const handleMouseMove = useCallback((e: React.MouseEvent) => {
    const rect = canvasRef.current!.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    // Mark brush area as dirty
    const brushSize = 20; // Get from tool state
    markRegionDirty(
      x - brushSize / 2,
      y - brushSize / 2,
      brushSize,
      brushSize
    );
  }, [markRegionDirty]);

  return (
    <canvas
      ref={canvasRef}
      width={800}
      height={600}
      onMouseMove={handleMouseMove}
      style={{ imageRendering: 'pixelated' }}
    />
  );
};

function drawInRegion(ctx: CanvasRenderingContext2D, region: DOMRect) {
  // Efficient drawing within specified region
  ctx.save();
  ctx.beginPath();
  ctx.rect(region.x, region.y, region.width, region.height);
  ctx.clip();

  // Draw your content here
  // Only pixels within the clipped region will be affected

  ctx.restore();
}

Verification & Testing

Comprehensive Test Suite

# Run all tests
yarn test

# Run tests with coverage
yarn test:coverage

# Run E2E tests
yarn test:e2e

# Run accessibility tests
yarn test:a11y

# Run performance tests
yarn test:performance

# Visual regression tests
yarn test:visual

PWA Testing

# Test PWA features with Lighthouse
npx lighthouse http://localhost:5173 --chrome-flags="--headless" --output=html --output-path=./lighthouse-report.html

# Test offline functionality
npx playwright test --grep "offline"

# Test installation flow
npx playwright test --grep "install"

Accessibility Testing

# Create accessibility test
cat > src/__tests__/accessibility.test.tsx << 'EOF'
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { KidPixApp } from '../App';

expect.extend(toHaveNoViolations);

test('app has no accessibility violations', async () => {
  const { container } = render(<KidPixApp />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

test('keyboard navigation works', async () => {
  const { getByTestId } = render(<KidPixApp />);

  const pencilTool = getByTestId('tool-pencil');
  pencilTool.focus();

  fireEvent.keyDown(pencilTool, { key: 'ArrowRight' });

  const brushTool = getByTestId('tool-brush');
  expect(brushTool).toHaveFocus();
});
EOF

Performance Testing

# Create performance test
cat > src/__tests__/performance.test.ts << 'EOF'
import { measurePerformance } from '../testing/TestUtils';
import { PencilTool } from '../tools/PencilTool';

test('pencil tool performs within limits', async () => {
  const tool = new PencilTool(mockState, mockDispatch);
  const canvas = document.createElement('canvas');

  const results = await measurePerformance(async () => {
    tool.onMouseDown(mockEvent, canvas);
    tool.onMouseMove(mockEvent, canvas);
    tool.onMouseUp(mockEvent, canvas);
  }, 100);

  expect(results.average).toBeLessThan(16); // 60fps
  expect(results.max).toBeLessThan(33); // 30fps worst case
});
EOF

Troubleshooting

PWA Issues

Problem: Service worker not updating

// Force service worker update
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.getRegistrations().then((registrations) => {
    registrations.forEach((registration) => {
      registration.update();
    });
  });
}

Problem: Manifest not being recognized

// Ensure proper manifest structure
{
  "name": "KidPix",
  "short_name": "KidPix",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ]
}

Performance Issues

Problem: Canvas drawing is slow

// Use multiple canvas layers
const backgroundCanvas = document.createElement("canvas");
const drawingCanvas = document.createElement("canvas");

// Only redraw what changed
function optimizedRedraw() {
  if (backgroundChanged) {
    redrawBackground(backgroundCanvas);
    backgroundChanged = false;
  }

  if (drawingChanged) {
    redrawDrawing(drawingCanvas);
    drawingChanged = false;
  }
}

Problem: Memory leaks

// Proper cleanup
useEffect(() => {
  const cleanup = [];

  // Add event listeners
  const handleResize = () => resizeCanvas();
  window.addEventListener("resize", handleResize);
  cleanup.push(() => window.removeEventListener("resize", handleResize));

  return () => cleanup.forEach((fn) => fn());
}, []);

Accessibility Issues

Problem: Screen reader not announcing changes

// Ensure proper ARIA live regions
const announcer = document.querySelector('[aria-live="polite"]');
if (announcer) {
  announcer.textContent = "Tool changed to pencil";

  // Clear after announcement
  setTimeout(() => {
    announcer.textContent = "";
  }, 1000);
}

Problem: Keyboard navigation not working

// Ensure proper focus management
const focusableElements = container.querySelectorAll(
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);

const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[
  focusableElements.length - 1
] as HTMLElement;

// Trap focus within container
container.addEventListener("keydown", (e) => {
  if (e.key === "Tab") {
    if (e.shiftKey) {
      if (document.activeElement === firstElement) {
        e.preventDefault();
        lastElement.focus();
      }
    } else {
      if (document.activeElement === lastElement) {
        e.preventDefault();
        firstElement.focus();
      }
    }
  }
});

Deployment & Monitoring

Production Deployment

# Build for production
yarn build

# Analyze bundle
yarn build:analyze

# Deploy to CDN
aws s3 sync dist/ s3://kidpix-app --delete
aws cloudfront create-invalidation --distribution-id XXXXX --paths "/*"

Monitoring Setup

# Create monitoring dashboard
cat > monitoring-dashboard.json << 'EOF'
{
  "widgets": [
    {
      "type": "metric",
      "properties": {
        "metrics": [
          ["KidPix", "PageLoadTime"],
          ["KidPix", "FrameRate"],
          ["KidPix", "ErrorRate"]
        ],
        "period": 300,
        "stat": "Average",
        "region": "us-east-1",
        "title": "Performance Metrics"
      }
    }
  ]
}
EOF

Success Metrics

After completing Phase 6, your KidPix application should achieve:

  • Performance: 60+ FPS drawing, <3s load time
  • Accessibility: WCAG 2.1 AA compliance
  • PWA Score: 90+ Lighthouse PWA score
  • Bundle Size: <2MB initial load
  • Test Coverage: >90% code coverage
  • Error Rate: <0.1% user sessions

Final Steps:

  1. Production deployment:
git add .
git commit -m "feat(production): complete PWA implementation with accessibility and monitoring"
git push origin main
  1. Launch monitoring:
# Set up error tracking
# Configure performance monitoring
# Enable user analytics
  1. Celebrate! ๐ŸŽ‰ You've successfully migrated KidPix to a modern React + TypeScript PWA!

Related Documentation:

  • Overview - Complete migration plan
  • Phase 5 - Advanced features
  • You're done! KidPix is now a modern, accessible, performant web application!