perf: pause tavern canvas when off-screen (0.60.0)

- the 60fps canvas render loop ran continuously even when scrolled out of
  view (the home page is long now — wasted CPU/battery while reading below)
- IntersectionObserver pauses the rAF loop at 0% visibility and resumes it
  when the tavern scrolls back in; cleaned up on unmount
- verified live: ~60 rAF/s on-screen → 0 off-screen → 60 on resume, 0 errors
This commit is contained in:
artheru 2026-06-10 08:26:48 +08:00
parent 3ddbc76dfc
commit cf67af7131
4 changed files with 12 additions and 5 deletions

View File

@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0e0a14" /> <meta name="theme-color" content="#0e0a14" />
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." /> <meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
<meta name="version" content="0.59.0-a11y" /> <meta name="version" content="0.60.0-canvasperf" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" /> <link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />

View File

@ -1,6 +1,6 @@
{ {
"name": "amerc-site", "name": "amerc-site",
"version": "0.59.0-a11y", "version": "0.60.0-canvasperf",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -7,7 +7,7 @@ import Account from './Account.jsx';
import TavernGame from './TavernGame.jsx'; import TavernGame from './TavernGame.jsx';
import { api } from './api.js'; import { api } from './api.js';
const RELEASE = '0.59.0-a11y'; const RELEASE = '0.60.0-canvasperf';
const NAV = [ const NAV = [
{ id: 'home', label: 'Tavern' }, { id: 'home', label: 'Tavern' },

View File

@ -57,12 +57,13 @@ export default function TavernGame({ onHotspot, stats }) {
useEffect(() => { statsRef.current = stats; }, [stats]); useEffect(() => { statsRef.current = stats; }, [stats]);
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
useEffect(() => { useEffect(() => {
const keys = Object.keys(SRC); const S = {}; let loaded = 0, raf, f = 0, buf; const keys = Object.keys(SRC); const S = {}; let loaded = 0, raf, f = 0, buf, paused = false, io;
const start = () => { const start = () => {
setReady(true); setReady(true);
buf = document.createElement('canvas'); buf.width = W; buf.height = H; buildStatic(buf, S); buf = document.createElement('canvas'); buf.width = W; buf.height = H; buildStatic(buf, S);
const ctx = canvasRef.current.getContext('2d'); ctx.imageSmoothingEnabled = false; const ctx = canvasRef.current.getContext('2d'); ctx.imageSmoothingEnabled = false;
const render = () => { const render = () => {
if (paused) return;
f++; f++;
ctx.drawImage(buf, 0, 0); ctx.drawImage(buf, 0, 0);
// bartender (a clothed guard tending bar) behind the bar (idle bob) // bartender (a clothed guard tending bar) behind the bar (idle bob)
@ -107,9 +108,15 @@ export default function TavernGame({ onHotspot, stats }) {
raf = requestAnimationFrame(render); raf = requestAnimationFrame(render);
}; };
render(); render();
// pause the 60fps loop while the tavern is scrolled out of view (CPU/battery)
io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { if (paused) { paused = false; render(); } }
else if (!paused) { paused = true; cancelAnimationFrame(raf); }
}, { threshold: 0 });
io.observe(canvasRef.current);
}; };
keys.forEach((k) => { const im = new Image(); im.onload = () => { if (++loaded === keys.length) start(); }; im.src = SRC[k]; S[k] = im; }); keys.forEach((k) => { const im = new Image(); im.onload = () => { if (++loaded === keys.length) start(); }; im.src = SRC[k]; S[k] = im; });
return () => cancelAnimationFrame(raf); return () => { cancelAnimationFrame(raf); if (io) io.disconnect(); };
}, []); }, []);
return ( return (