# Performance optimization report

This report documents the production performance strategy for Mechanica and the hardening work applied for final deployment.

## Targets

| Scenario | Target |
| --- | ---: |
| Desktop viewer frame rate | 60 FPS during normal orbit and animation |
| Mobile viewer frame rate | 30 FPS minimum |
| Catalogue interaction latency | < 100 ms for search/filter/sort |
| Initial route JS budget | < 350 KB gzip excluding vendor chunks |
| Largest single machine chunk | < 650 KB gzip excluding GLB/texture assets |
| Machine metadata load | < 50 ms parse on typical desktop |
| Viewer input response | < 50 ms from pointer movement to camera response |
| Share URL parse/apply | < 16 ms for typical state payload |

## Implemented production configuration

The Vite configuration includes:

- React plugin for production JSX transform.
- Path alias `@` for stable imports.
- Manual vendor chunking:
  - `vendor-react`
  - `vendor-three`
  - `vendor-animation`
  - `vendor-state`
  - `vendor-ui`
  - `vendor`
- CSS code splitting.
- Deterministic hashed asset paths.
- Asset subfolders for models, environments, fonts, images, and scripts.
- Bundle visualizer mode through `npm run analyze`.
- Production source maps disabled unless `VITE_ENABLE_SOURCE_MAPS=true`.

Vercel configuration includes:

- Immutable cache headers for hashed assets.
- Long-lived decoder/transcoder cache headers.
- SPA rewrites to avoid route-level 404s.
- CSP that permits WebAssembly and blob workers needed by compressed asset loaders.

## Runtime optimization policy

### Catalogue

- Keep machine registry metadata lightweight.
- Store searchable keywords as normalized arrays.
- Memoize search/filter/sort derivations.
- Avoid loading 3D scene modules on the catalogue route.
- Use skeleton cards with fixed dimensions to prevent layout shift.

### Viewer

- Lazy-load each machine scene.
- Use Suspense loading fallbacks with stable panel dimensions.
- Clamp device pixel ratio:
  - desktop: up to `2`
  - tablet: up to `1.5`
  - mobile: up to `1.25`
- Prefer one environment light plus a small number of shadow-casting lights.
- Disable expensive shadows on low-power/mobile profiles when necessary.
- Use instancing for repeated geometry.
- Use shared materials for procedural parts.
- Keep per-frame allocations out of animation updates.
- Do not write to Zustand stores every frame.
- Dispose runtime clones when leaving a scene.

### Animation

- Store animation control state in Zustand, not animated transforms.
- Use render-loop updates for transforms.
- Keep cycle phase derived from elapsed time, RPM, and time scale.
- Clamp RPM and time scale before animation modules receive them.
- Respect reduced-motion by defaulting to paused or lower speed while preserving user control.

### Cross-section and transparency

- Reuse clipping planes instead of creating new planes per render.
- Mark only affected materials as requiring clipping.
- Avoid sorting-heavy transparent stacks where a simple x-ray material is sufficient.
- Provide opacity sliders per part, but debounce expensive material updates.

### Labels and annotations

- Hide labels below small viewport thresholds unless explicitly enabled.
- Use occlusion-aware labels where available.
- Avoid rendering hundreds of DOM labels; group minor parts under expandable annotations.

## Profiling workflow

Run a production build locally:

```bash
npm run build
npm run preview
```

Open Chrome DevTools:

1. Performance panel: record orbiting, play/pause, and exploded view changes.
2. Memory panel: switch between machines and confirm old geometries/materials are released.
3. Rendering panel: inspect FPS, paint flashing, and layout shift.
4. Network panel: confirm JS chunks and GLB assets use expected cache headers in deployed builds.

Generate bundle report:

```bash
npm run analyze
```

Inspect `dist/bundle-report.html` for:

- Three.js leaking into initial catalogue chunk.
- Machine modules bundled into the home route.
- Duplicate animation libraries.
- Unexpected large SVG or image imports.
- GLB files accidentally imported into the primary JS bundle.

## Mobile tuning checklist

- Test widths: 430, 390, 375.
- Test tablet widths: 1024 and 768.
- Use Chrome CPU throttling at 4x for low-end approximation.
- Confirm side panels collapse into drawers.
- Confirm controls have at least 44px touch targets.
- Confirm canvas does not scroll the page during orbit gestures.
- Confirm bottom status bar does not cover primary actions.
- Confirm annotations do not overwhelm the viewport.
- Confirm reduced DPR does not make labels illegible.

## Measurement log template

Use this table in release notes after measuring a candidate build:

| Build | Route/machine | Device | FPS orbit | FPS animation | JS transfer gzip | GLB transfer | Notes |
| --- | --- | --- | ---: | ---: | ---: | ---: | --- |
| preview URL | four-stroke-petrol-engine | Desktop Chrome |  |  |  |  |  |
| preview URL | planetary-gearbox | Desktop Chrome |  |  |  |  |  |
| preview URL | jet-engine-turbojet | Pixel 5 emulation |  |  |  |  |  |
| preview URL | ball-bearing | iPad landscape |  |  |  |  |  |

## Release gates

A production release should not be promoted if any of these are true:

- Initial catalogue route loads all machine scene modules.
- A common desktop interaction drops below 45 FPS for more than one second.
- A mobile interaction drops below 24 FPS for more than one second.
- Switching machines steadily increases memory after garbage collection.
- A single non-machine JS chunk exceeds 900 KB gzip.
- Share links fail to reconstruct the active machine.
- Reduced-motion mode still auto-plays large camera animations without user initiation.
