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
| Layer | Technology | What's Cached |
|---|---|---|
| ROM Cache | IndexedDB | Game ROM files, BIOS, additional files |
| Core Cache | IndexedDB | Emulator core WASM files (versioned) |
| Cover Cache | IndexedDB + Blob URL | Game cover images |
| Cheat Cache | IndexedDB | Game cheat codes |
| Favorites Cache | localStorage + IndexedDB | User favorites list |
Key Files:
- cache.js - Core caching logic
- favorites.js - Favorites caching
Multi-threading (Worker Mode)
TinyEmulator supports running the emulator core in a Web Worker via OffscreenCanvas:
- Auto-detection: Uses worker mode if
OffscreenCanvasis supported - Toggle: Users can switch between normal/worker mode in settings
- Benefits: Main thread remains responsive, UI doesn't block during emulation
Key Files:
- emulator/module/index.js - Worker runtime management
- utils/worker.js - Worker utilities
- store/reducers/mode.jsx - Mode state management
Build Optimizations
| Optimization | Tool | Description |
|---|---|---|
| Fast compilation | SWC (@vitejs/plugin-react-swc) | Rust-based JS/TS compiler |
| Tree shaking | Vite/Rollup | Dead code elimination |
| Code minification | Vite/esbuild | JavaScript/CSS minification |
| Code obfuscation | javascript-obfuscator | Code protection (build only) |
| Single file build | vite-plugin-singlefile | HarmonyOS single-file output |
| Static asset copying | vite-plugin-static-copy | Core/BIOS/shader files |
React Optimizations
useMemofor expensive calculations (dropzone)useCallbackfor event handlers (dropzone, cheats menu)loading="lazy"for images in game listsreact-queryfor 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 listsRoomCard- in room listsControlButton- 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:
| Resource | CDN Strategy | Cache TTL |
|---|---|---|
| Core files | CDN + versioned | 30 days |
| BIOS files | CDN | 30 days |
| Game covers | CDN + image resize | 7 days |
| HTML/JS/CSS | CDN + ETag | 1 hour |
| API responses | No cache | 0s |
Performance Budget
Bundle Size
| Metric | Target | Current |
|---|---|---|
| Initial JS | < 500KB gzip | TBD |
| Initial CSS | < 50KB gzip | TBD |
| First core load | < 200KB | TBD |
| Total (all cores) | < 5MB | TBD |
Load Time (4G, mid-tier device)
| Metric | Target |
|---|---|
| First Contentful Paint | < 2s |
| Largest Contentful Paint | < 2.5s |
| Time to Interactive | < 3s |
| First game load | < 5s |
Runtime Performance
| Metric | Target |
|---|---|
| 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:
- Record page load
- Check for long tasks (>50ms)
- Analyze flame chart for bottlenecks
Memory tab:
- Take heap snapshot
- Compare before/after game load
- Check for memory leaks
Network tab:
- Throttle to Fast 3G
- Check resource loading order
- 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
Related Documents
- Local Testing Guide - Test performance locally
- Self-Hosting Guide - Deploy and configure servers
- PWA Setup - Progressive Web App configuration