跳到主要内容

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