rive-performance
**Scope**: Canvas sizing, WebGL context limits, frame budget, runtime lazy loading for 60fps mobile. **Version range**: `@rive-app/react-canvas` 4.x, React 18/19 **Generated**: 2026-04-14
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
**Scope**: Canvas sizing, WebGL context limits, frame budget, runtime lazy loading for 60fps mobile. **Version range**: `@rive-app/react-canvas` 4.x, React 18/19 **Generated**: 2026-04-14
Agent definition
rive-performance.mdRive Performance Reference
> **Scope**: Canvas sizing, WebGL context limits, frame budget, runtime lazy loading for 60fps mobile. > **Version range**: `@rive-app/react-canvas` 4.x, React 18/19 > **Generated**: 2026-04-14
---
Rive renders into WebGL canvas. Primary performance killers: canvas resolution (900px at 2x DPR = 1800x1800px) and exceeding browser WebGL context limit (~16). Lazy load WASM runtime (~150KB). All three required for 60fps on mid-range mobile.
Pattern Table
| Pattern | Use When | Avoid When | |---------|----------|------------| | `layout={new Layout({ fit: Fit.Contain })}` | character fits fixed canvas | pixel-perfect sprite replacement | | `React.lazy` + `Suspense` for Rive component | combat screen not on initial load | animation needed at page load | | `rive.cleanup()` on unmount | component unmounts/remounts | canvas stays mounted | | `useRive({ autoplay: false })` + manual play | deferring until user action | should start on load |
---
Correct Patterns
Lazy Load the Rive Component
const CombatScene = React.lazy(() => import('./CombatScene'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
{showCombat && <CombatScene />}
</Suspense>
);
}WASM bundle ~150KB gzipped loads synchronously on first import. Lazy loading amortizes to first combat encounter.
---
Explicit Canvas Size via Container Div
// Correct — container controls size
<div style={{ width: 400, height: 400, position: 'relative' }}>
<RiveComponent />
</div>
// Wrong — RiveComponent does not accept width/height reliably
<RiveComponent width={400} height={400} />`RiveComponent` mounts `<canvas>` and observes container via `ResizeObserver`. Direct canvas dimensions bypass the observer.
---
Downscale Large Characters on Mobile
function EnemyCharacter() {
const isMobile = useMediaQuery('(max-width: 768px)');
const size = isMobile ? 450 : 900;
return (
<div style={{ width: size, height: size }}>
<RiveComponent />
</div>
);
}900px at 2x DPR = 3.24M pixels/frame. 450px = 810K — 4x cheaper.
---
Cleanup on Unmount
useEffect(() => {
return () => { rive?.cleanup(); };
}, [rive]);Browsers cap WebGL contexts at ~16. Each `useRive` without cleanup holds a slot. After 16, new canvases silently fail.
---
Pattern Catalog
Use CSS Transitions Instead of Framer Motion for Rive
**Detection**:
grep -rn 'motion\.' --include="*.tsx" | grep -i 'rive'
**Signal**: `<motion.div animate={{ opacity: 1 }}><RiveComponent /></motion.div>`
Both Framer Motion and Rive own animation timing. Framer driving opacity/transform on the container forces composite layer repaints, doubling GPU work.
**Fix**:
<div style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s ease' }}>
<RiveComponent />
</div>---
Create Rive Instances via useRive Hook
**Detection**:
grep -rn 'new Rive(' --include="*.ts" --include="*.tsx"**Signal**: `const riveInstance = new Rive({ src: '...', canvas: canvasEl })` outside React.
Manual instances lose cleanup path. `useRive` manages lifecycle (loading, resize, cleanup) tied to component tree. Manual instances leak WebGL contexts.
**Fix**: Use `useRive` inside component. Pass fire-input callback to Zustand, not the instance.
---
Share WebGL Context Across Multiple Canvases
**Detection**:
grep -rn 'useRive' --include="*.tsx" | grep -v 'test\|spec\|story'
Count active `useRive` calls. If >12, context exhaustion is likely.
**Signal**: Character select with 8 previews + combat with 2 = 10+ contexts.
Exceeding limit silently produces blank canvases with no console error.
**Fix**: Use Rive's `SharedRenderer` to share one WebGL context.
---
Error-Fix Mappings
| Error | Root Cause | Fix | |-------|------------|-----| | Blank canvas, no error | WebGL context limit (>16) | `rive.cleanup()` on unmount; `SharedRenderer` | | `ResizeObserver loop limit exceeded` | Container div has no explicit dimensions | Set `width`/`height` on wrapper | | FPS drops 60→30 on mobile | Canvas too large at 2x DPR | Halve dimensions at 768px breakpoint | | Animation plays once, freezes | `autoplay: false` without `rive.play()` | Set `autoplay: true` or call `rive.play()` | | WASM load failure in Vite | Vite doesn't serve `.wasm` by default | Add `assetsInclude: ['**/*.wasm']` |
Version Notes
| Version | Change | Impact | |---------|--------|--------| | 4.0 | `useRive` hook (replaced `useRiveFile`) | Old constructor pattern deprecated | | 4.7 | `SharedRenderer` exported from main package | No longer need `canvas-advanced` | | React 19 | StrictMode double-invokes effects | `rive.cleanup()` essential — double-mount creates then destroys one instance |
---
Detection Commands
# Framer Motion wrappers around Rive
grep -rn 'motion\.' --include="*.tsx" | grep -i 'rive'
# Manual Rive constructor (context leak)
grep -rn 'new Rive(' --include="*.ts" --include="*.tsx"
# Active useRive count (context exhaustion)
grep -rn 'useRive' --include="*.tsx" | grep -v 'test\|spec\|story'
# RiveComponent without container sizing
grep -rn 'RiveComponent' --include="*.tsx" -A2 | grep -v 'width\|height\|style'
# Missing cleanup
grep -rn 'useRive' --include="*.tsx" -l | xargs grep -L 'cleanup'See Also
- `rive-react-setup.md` — useRive parameters, Vite WASM config, lazy loading
- `rive-animation-library.md` — State machine timing, frame sync with CombatEngine
Read more
Rive Performance Reference
> **Scope**: Canvas sizing, WebGL context limits, frame budget, runtime lazy loading for 60fps mobile. > **Version range**: `@rive-app/react-canvas` 4.x, React 18/19 > **Generated**: 2026-04-14
---
Rive renders into WebGL canvas. Primary performance killers: canvas resolution (900px at 2x DPR = 1800x1800px) and exceeding browser WebGL context limit (~16). Lazy load WASM runtime (~150KB). All three required for 60fps on mid-range mobile.
Pattern Table
| Pattern | Use When | Avoid When | |---------|----------|------------| | `layout={new Layout({ fit: Fit.Contain })}` | character fits fixed canvas | pixel-perfect sprite replacement | | `React.lazy` + `Suspense` for Rive component | combat screen not on initial load | animation needed at page load | | `rive.cleanup()` on unmount | component unmounts/remounts | canvas stays mounted | | `useRive({ autoplay: false })` + manual play | deferring until user action | should start on load |
---
Correct Patterns
Lazy Load the Rive Component
const CombatScene = React.lazy(() => import('./CombatScene'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
{showCombat && <CombatScene />}
</Suspense>
);
}WASM bundle ~150KB gzipped loads synchronously on first import. Lazy loading amortizes to first combat encounter.
---
Explicit Canvas Size via Container Div
// Correct — container controls size
<div style={{ width: 400, height: 400, position: 'relative' }}>
<RiveComponent />
</div>
// Wrong — RiveComponent does not accept width/height reliably
<RiveComponent width={400} height={400} />`RiveComponent` mounts `<canvas>` and observes container via `ResizeObserver`. Direct canvas dimensions bypass the observer.
---
Downscale Large Characters on Mobile
function EnemyCharacter() {
const isMobile = useMediaQuery('(max-width: 768px)');
const size = isMobile ? 450 : 900;
return (
<div style={{ width: size, height: size }}>
<RiveComponent />
</div>
);
}900px at 2x DPR = 3.24M pixels/frame. 450px = 810K — 4x cheaper.
---
Cleanup on Unmount
useEffect(() => {
return () => { rive?.cleanup(); };
}, [rive]);Browsers cap WebGL contexts at ~16. Each `useRive` without cleanup holds a slot. After 16, new canvases silently fail.
---
Pattern Catalog
Use CSS Transitions Instead of Framer Motion for Rive
**Detection**:
grep -rn 'motion\.' --include="*.tsx" | grep -i 'rive'
**Signal**: `<motion.div animate={{ opacity: 1 }}><RiveComponent /></motion.div>`
Both Framer Motion and Rive own animation timing. Framer driving opacity/transform on the container forces composite layer repaints, doubling GPU work.
**Fix**:
<div style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s ease' }}>
<RiveComponent />
</div>---
Create Rive Instances via useRive Hook
**Detection**:
grep -rn 'new Rive(' --include="*.ts" --include="*.tsx"**Signal**: `const riveInstance = new Rive({ src: '...', canvas: canvasEl })` outside React.
Manual instances lose cleanup path. `useRive` manages lifecycle (loading, resize, cleanup) tied to component tree. Manual instances leak WebGL contexts.
**Fix**: Use `useRive` inside component. Pass fire-input callback to Zustand, not the instance.
---
Share WebGL Context Across Multiple Canvases
**Detection**:
grep -rn 'useRive' --include="*.tsx" | grep -v 'test\|spec\|story'
Count active `useRive` calls. If >12, context exhaustion is likely.
**Signal**: Character select with 8 previews + combat with 2 = 10+ contexts.
Exceeding limit silently produces blank canvases with no console error.
**Fix**: Use Rive's `SharedRenderer` to share one WebGL context.
---
Error-Fix Mappings
| Error | Root Cause | Fix | |-------|------------|-----| | Blank canvas, no error | WebGL context limit (>16) | `rive.cleanup()` on unmount; `SharedRenderer` | | `ResizeObserver loop limit exceeded` | Container div has no explicit dimensions | Set `width`/`height` on wrapper | | FPS drops 60→30 on mobile | Canvas too large at 2x DPR | Halve dimensions at 768px breakpoint | | Animation plays once, freezes | `autoplay: false` without `rive.play()` | Set `autoplay: true` or call `rive.play()` | | WASM load failure in Vite | Vite doesn't serve `.wasm` by default | Add `assetsInclude: ['**/*.wasm']` |
Version Notes
| Version | Change | Impact | |---------|--------|--------| | 4.0 | `useRive` hook (replaced `useRiveFile`) | Old constructor pattern deprecated | | 4.7 | `SharedRenderer` exported from main package | No longer need `canvas-advanced` | | React 19 | StrictMode double-invokes effects | `rive.cleanup()` essential — double-mount creates then destroys one instance |
---
Detection Commands
# Framer Motion wrappers around Rive
grep -rn 'motion\.' --include="*.tsx" | grep -i 'rive'
# Manual Rive constructor (context leak)
grep -rn 'new Rive(' --include="*.ts" --include="*.tsx"
# Active useRive count (context exhaustion)
grep -rn 'useRive' --include="*.tsx" | grep -v 'test\|spec\|story'
# RiveComponent without container sizing
grep -rn 'RiveComponent' --include="*.tsx" -A2 | grep -v 'width\|height\|style'
# Missing cleanup
grep -rn 'useRive' --include="*.tsx" -l | xargs grep -L 'cleanup'See Also
- `rive-react-setup.md` — useRive parameters, Vite WASM config, lazy loading
- `rive-animation-library.md` — State machine timing, frame sync with CombatEngine
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

