Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions example/src/demos/default/CanvasDebounceFixed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import * as React from 'react'
import { Canvas, useFrame } from '@react-three/fiber'

//* Timing Test Component ==============================
function TimingBox() {
const startTime = React.useRef(performance.now())
const hasLogged = React.useRef(false)

useFrame(() => {
if (!hasLogged.current) {
hasLogged.current = true
const elapsed = performance.now() - startTime.current
console.log(`%c✓ First frame rendered after: ${elapsed.toFixed(2)}ms`, 'color: #00ff00; font-weight: bold')
}
})

return (
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}

//* Main Test ==============================
export default function CanvasDebounceFixed() {
const [testCase, setTestCase] = React.useState<'default' | 'custom' | 'scroll-off'>('default')

React.useEffect(() => {
console.clear()
console.log('%c========================================', 'color: #ffffff')
console.log('%cCanvas Debounce Fix Test', 'color: #ffffff; font-size: 16px; font-weight: bold')
console.log('%c========================================', 'color: #ffffff')
console.log(`%cTesting: ${testCase}`, 'color: #00ffff; font-weight: bold')
}, [testCase])

return (
<div style={{ padding: '20px', fontFamily: 'monospace' }}>
<h1>Canvas Debounce Fix - Verification</h1>
<p>
This test verifies that the Canvas now renders immediately without the debounce delay.
<br />
All cases should now render in ~5-15ms regardless of debounce settings.
</p>

{/* Test Selector */}
<div style={{ marginBottom: '20px', display: 'flex', gap: '10px' }}>
<button
onClick={() => setTestCase('default')}
style={{
padding: '10px',
fontWeight: testCase === 'default' ? 'bold' : 'normal',
backgroundColor: testCase === 'default' ? '#4488ff' : '#ddd',
}}>
Default Settings
</button>
<button
onClick={() => setTestCase('custom')}
style={{
padding: '10px',
fontWeight: testCase === 'custom' ? 'bold' : 'normal',
backgroundColor: testCase === 'custom' ? '#ff8844' : '#ddd',
}}>
Custom Debounce (2s)
</button>
<button
onClick={() => setTestCase('scroll-off')}
style={{
padding: '10px',
fontWeight: testCase === 'scroll-off' ? 'bold' : 'normal',
backgroundColor: testCase === 'scroll-off' ? '#44ff88' : '#ddd',
}}>
Scroll Disabled
</button>
</div>

{/* Description */}
<div
style={{
marginBottom: '20px',
padding: '15px',
backgroundColor: '#f0f0f0',
borderRadius: '5px',
}}>
{testCase === 'default' && (
<>
<strong>Default Settings:</strong> No resize config provided.
<br />
Expected: Fast initial render (~5-15ms), then 50ms scroll debounce applies.
</>
)}
{testCase === 'custom' && (
<>
<strong>Custom Debounce:</strong> 2 second debounce configured.
<br />
Expected: Fast initial render (~5-15ms), then 2s debounce applies to resizes.
<br />
<em>Previously this would have delayed initial render by 2 seconds!</em>
</>
)}
{testCase === 'scroll-off' && (
<>
<strong>Scroll Disabled:</strong> Scroll tracking disabled.
<br />
Expected: Fast initial render (~5-15ms), no scroll tracking.
</>
)}
</div>

{/* Canvas Test Cases */}
<div style={{ width: '100%', height: '400px', border: '2px solid #4488ff' }}>
{testCase === 'default' && (
<Canvas>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} />
<TimingBox />
</Canvas>
)}

{testCase === 'custom' && (
<Canvas resize={{ debounce: 2000 }}>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} />
<TimingBox />
</Canvas>
)}

{testCase === 'scroll-off' && (
<Canvas resize={{ scroll: false }}>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} />
<TimingBox />
</Canvas>
)}
</div>

{/* Instructions */}
<div style={{ marginTop: '20px', fontSize: '14px', color: '#666' }}>
<strong>Verification Steps:</strong>
<ol>
<li>Open browser console (F12)</li>
<li>Switch between test cases and observe timing</li>
<li>All cases should show ~5-15ms initial render time</li>
<li>Try resizing the window - debounce should work normally after initial render</li>
</ol>
<p>
<strong>The Fix:</strong> Canvas now starts with 0ms debounce for immediate initial size measurement, then
switches to user-configured debounce settings for subsequent resize/scroll events.
</p>
</div>
</div>
)
}
2 changes: 2 additions & 0 deletions example/src/demos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { lazy } from 'react'
//* Default Examples ==============================
// Examples that work with both WebGL and WebGPU renderers
const AutoDispose = { Component: lazy(() => import('./default/AutoDispose')) }
const CanvasDebounceFixed = { Component: lazy(() => import('./default/CanvasDebounceFixed')) }
const ChangeTexture = { Component: lazy(() => import('./default/ChangeTexture')) }
const ClickAndHover = { Component: lazy(() => import('./default/ClickAndHover')) }
const ContextMenuOverride = { Component: lazy(() => import('./default/ContextMenuOverride')) }
Expand Down Expand Up @@ -50,6 +51,7 @@ const UseFrameNextControls = { Component: lazy(() => import('./webgpu/UseFrameNe
export {
// Default
AutoDispose,
CanvasDebounceFixed,
ChangeTexture,
ClickAndHover,
ContextMenuOverride,
Expand Down
30 changes: 29 additions & 1 deletion packages/fiber/src/core/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,35 @@ function CanvasImpl({

const Bridge = useBridge()

const [containerRef, containerRect] = useMeasure({ scroll: true, debounce: { scroll: 50, resize: 0 }, ...resize })
//* Dynamic Debounce for Fast Initial Render ==============================
// Track if we've gotten initial size measurement
const hasInitialSizeRef = React.useRef(false)

// Create measure config with immediate initial measurement (0ms debounce)
// After first size, we'll use user-provided debounce for subsequent updates
const measureConfig = React.useMemo(() => {
if (!hasInitialSizeRef.current) {
// First measurement: use 0ms debounce for immediate rendering
return {
...resize,
scroll: resize?.scroll ?? true,
debounce: 0,
}
}
// Subsequent measurements: use user-provided debounce
return {
scroll: true,
debounce: { scroll: 50, resize: 0 },
...resize,
}
}, [resize, hasInitialSizeRef.current]) // eslint-disable-line react-hooks/exhaustive-deps

const [containerRef, containerRect] = useMeasure(measureConfig)

// Mark that we have initial size (for next render cycle)
if (!hasInitialSizeRef.current && containerRect.width > 0 && containerRect.height > 0) {
hasInitialSizeRef.current = true
}
const canvasRef = React.useRef<HTMLCanvasElement>(null!)
const divRef = React.useRef<HTMLDivElement>(null!)
React.useImperativeHandle(ref, () => canvasRef.current)
Expand Down