Cesium Viewer Lifecycle Management and Crash Prevention
ARCHITECTURE •
Situation
The application crashed during rapid navigation between Cesium utility demo pages with errors like Cannot read properties of undefined (reading 'scene') and Cannot set properties of undefined (setting 'canAnimate').
Investigated and identified the root cause: dangling events from terrain setups and tileset loading, plus animations called by requestAnimationFrame, were trying to access the viewer that was already destroyed.
Task
- Prevent async operations and animations from accessing destroyed viewers
- Eliminate the need for scattered viewer state checks across every demo page
- Maintain native Cesium API usage without complex wrapper patterns
Action
1. First Attempt - Coordinating Viewer Destruction Timing
Initially tried to fix the viewer component by coordinating the timing of viewer destruction with async operations.
Discovered that Cesium's native API requires asynchronous functions for terrain loading and tileset initialization. Making this approach work would require guard checks like if (!viewer || viewer.isDestroyed()) return; scattered across every demo page using the viewer.
This would couple API usage patterns to lifecycle management - unacceptable for maintaining clean, native Cesium usage.
2. Decided to Split Context Scope
Created apps/web/app/cesium-utils/[api]/layout.tsx that wraps each API route with its own ViewerProvider. Removed ViewerProvider from the global cesium-utils layout, keeping only CesiumUtilsProvider for feature selection state. Wrapped the root demo page with its own ViewerProvider.
Each route now gets a fresh viewer instance that lives and dies with that route's lifecycle. Demo pages can continue using the viewer naturally without defensive checks everywhere.
3. Discovered Animation Destruction Race
After per-route isolation, discovered another crash: destroying the viewer during ongoing animations causes requestAnimationFrame callbacks to fire after destruction.
4. Improved Viewer Cleanup Logic
Split viewer component into two separate effects. Initialization effect (empty deps) runs once and handles cleanup by stopping render loop with before calling , wrapped in try-catch to suppress Cesium's internal race condition errors.