跳到主要内容

Performance Optimization Guide

This document outlines current performance optimizations in TinyEmulator and provides actionable optimization strategies for improving loading speed, runtime performance, and memory usage.

Current Optimizations

TinyEmulator already implements several performance optimizations:

Caching Layer

LayerTechnologyWhat's Cached
ROM CacheIndexedDBGame ROM files, BIOS, additional files
Core CacheIndexedDBEmulator core WASM files (versioned)
Cover CacheIndexedDB + Blob URLGame cover images
Cheat CacheIndexedDBGame cheat codes
Favorites CachelocalStorage + IndexedDBUser favorites list

Key Files:

Multi-threading (Worker Mode)

TinyEmulator supports running the emulator core in a Web Worker via OffscreenCanvas:

  • Auto-detection: Uses worker mode if OffscreenCanvas is supported
  • Toggle: Users can switch between normal/worker mode in settings
  • Benefits: Main thread remains responsive, UI doesn't block during emulation

Key Files:

Build Optimizations

OptimizationToolDescription
Fast compilationSWC (@vitejs/plugin-react-swc)Rust-based JS/TS compiler
Tree shakingVite/RollupDead code elimination
Code minificationVite/esbuildJavaScript/CSS minification
Code obfuscationjavascript-obfuscatorCode protection (build only)
Single file buildvite-plugin-singlefileHarmonyOS single-file output
Static asset copyingvite-plugin-static-copyCore/BIOS/shader files

React Optimizations

  • useMemo for expensive calculations (dropzone)
  • useCallback for event handlers (dropzone, cheats menu)
  • loading="lazy" for images in game lists
  • react-query for server state caching

Optimization Roadmap

P0: Critical - Loading Performance

1. Route-level Code Splitting

Current state: All code bundled in a single chunk.

Optimization: Implement lazy loading for route components.

// src/App.jsx
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./modules/home'));
const Rooms = lazy(() => import('./modules/rooms'));
const Player = lazy(() => import('./modules/Player'));
const Manage = lazy(() => import('./modules/manage'));

// Wrap with Suspense
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/rooms" element={<Rooms />} />
<Route path="/app/:type" element={<Player />} />
</Routes>
</Suspense>

Expected improvement: 40-60% reduction in initial bundle size.

2. Core Files Lazy Loading

Current state: All cores loaded upfront.

Optimization: Load cores on-demand when a game is selected.

// src/utils/core.js
export const loadCore = async (coreName) => {
if (coreCache.has(coreName)) {
return coreCache.get(coreName);
}

const response = await fetch(`/cores/${coreName}_libretro.js`);
const code = await response.text();
const core = eval(code);
coreCache.set(coreName, core);
return core;
};

Expected improvement: 200-500KB initial load reduction.

3. Font Optimization

Optimization: Preload critical fonts, use font-display: swap.

/* Add to main CSS */
@font-face {
font-family: 'Public Sans';
font-display: swap;
src: url('/fonts/public-sans.woff2') format('woff2');
}
<!-- Add to index.html <head> -->
<link rel="preload" href="/fonts/public-sans.woff2" as="font" type="font/woff2" crossorigin>

Expected improvement: Eliminate FOIT (Flash of Invisible Text).

P1: High - Runtime Performance

4. Virtual Scrolling for Game Lists

Current state: All games rendered in DOM at once.

Optimization: Use react-window for long lists.

npm install react-window
import { FixedSizeList as List } from 'react-window';

const GameList = ({ games, onSelect }) => (
<List
height={600}
itemCount={games.length}
itemSize={120}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<GameCard game={games[index]} onClick={() => onSelect(games[index])} />
</div>
)}
</List>
);

Expected improvement: O(1) DOM nodes regardless of list size.

5. Memoize Expensive Components

Optimization: Wrap frequently re-rendered components with React.memo.

// GameCard.jsx - before
export default function GameCard({ game, onClick }) {
return <div onClick={onClick}>{game.title}</div>;
}

// GameCard.jsx - after
import { memo } from 'react';

const GameCard = memo(function GameCard({ game, onClick }) {
return <div onClick={onClick}>{game.title}</div>;
}, (prev, next) => {
return prev.game.id === next.game.id;
});

export default GameCard;

Key components to memoize:

  • GameCard - in game lists
  • RoomCard - in room lists
  • ControlButton - virtual gamepad

6. Image Optimization

Optimization: Use WebP format, implement responsive images, add blur-up placeholder.

// Use progressive loading with blur-up effect
const GameCover = ({ src, alt }) => {
const [loaded, setLoaded] = useState(false);

return (
<div style={{ position: 'relative' }}>
{!loaded && (
<div
style={{
filter: 'blur(10px)',
transform: 'scale(1.05)',
}}
>
<img src={`${src}?w=20`} alt="" aria-hidden="true" />
</div>
)}
<img
src={src}
alt={alt}
loading="lazy"
onLoad={() => setLoaded(true)}
style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
</div>
);
};

P2: Medium - Memory Optimization

7. WASM Memory Management

Optimization: Clean up WASM instances when switching games.

// src/emulator/module/index.js
class EmulatorModule {
destroy() {
// 1. Stop the emulation loop
this.stop();

// 2. Free WASM memory
if (this.Module && this.Module._free) {
// Free allocated buffers
this.romBuffer && this.Module._free(this.romBuffer);
this.saveBuffer && this.Module._free(this.saveBuffer);
}

// 3. Terminate worker if in worker mode
if (this.worker) {
this.worker.terminate();
}

// 4. Clear references for GC
this.Module = null;
this.romBuffer = null;
this.saveBuffer = null;
}
}

8. ROM Data Streaming

Optimization: Stream ROM files instead of loading entirely into memory.

// For large ROMs (e.g., PSX, N64)
async function streamRom(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const chunks = [];

while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}

return new Uint8Array(await new Blob(chunks).arrayBuffer());
}

P3: Low - Network & CDN

9. Service Worker / PWA Caching

Optimization: Cache core files and assets with Service Worker for offline support.

// sw.js - Service Worker
const CORE_CACHE = 'cores-v1';
const ASSET_CACHE = 'assets-v1';

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CORE_CACHE).then((cache) => {
return cache.addAll([
'/cores/nes_libretro.js',
'/cores/snes_libretro.js',
'/bios/...',
]);
})
);
});

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});

10. HTTP/2 & CDN Optimization

Recommended CDN setup:

ResourceCDN StrategyCache TTL
Core filesCDN + versioned30 days
BIOS filesCDN30 days
Game coversCDN + image resize7 days
HTML/JS/CSSCDN + ETag1 hour
API responsesNo cache0s

Performance Budget

Bundle Size

MetricTargetCurrent
Initial JS< 500KB gzipTBD
Initial CSS< 50KB gzipTBD
First core load< 200KBTBD
Total (all cores)< 5MBTBD

Load Time (4G, mid-tier device)

MetricTarget
First Contentful Paint< 2s
Largest Contentful Paint< 2.5s
Time to Interactive< 3s
First game load< 5s

Runtime Performance

MetricTarget
FPS (60fps games)55-60 fps
Input latency< 50ms
Memory usage< 500MB
Long tasks< 50ms

Measuring Performance

Lighthouse

# Run Lighthouse audit
npx lighthouse http://localhost:3307 --view

Key metrics to track:

  • LCP (Largest Contentful Paint)
  • FID (First Input Delay)
  • CLS (Cumulative Layout Shift)
  • TTI (Time to Interactive)

Browser DevTools

Performance tab:

  1. Record page load
  2. Check for long tasks (>50ms)
  3. Analyze flame chart for bottlenecks

Memory tab:

  1. Take heap snapshot
  2. Compare before/after game load
  3. Check for memory leaks

Network tab:

  1. Throttle to Fast 3G
  2. Check resource loading order
  3. Verify caching headers

Web Vitals

// Add to index.html for real-user monitoring
import { onLCP, onFID, onCLS } from 'web-vitals';

onLCP(metric => console.log('LCP:', metric.value));
onFID(metric => console.log('FID:', metric.value));
onCLS(metric => console.log('CLS:', metric.value));

Quick Wins

These optimizations can be implemented with minimal effort:

1. Add React.memo to GameCard

  • Effort: 10 minutes
  • Impact: Medium (faster list scrolling)

2. Implement route code splitting

  • Effort: 30 minutes
  • Impact: High (smaller initial bundle)

3. Add font-display: swap

  • Effort: 5 minutes
  • Impact: Low (better perceived performance)

4. Enable HTTP caching for static assets

  • Effort: 15 minutes (Nginx config)
  • Impact: High (repeat visits)

5. Image lazy loading (already done, verify)

  • Effort: Verify existing implementation
  • Impact: Medium (faster initial load)

Server-side Optimizations

Nginx Configuration

# Enable gzip/brotli compression
gzip on;
gzip_types text/css application/javascript application/wasm;
gzip_min_length 1024;

# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|wasm)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}

# Enable HTTP/2
listen 443 ssl http2;

CDN Recommendations

  • China: Use Alibaba Cloud CDN or Tencent Cloud CDN
  • Global: Use Cloudflare or Fastly
  • Key features: HTTP/2, Brotli, image optimization, edge caching