Your preferred behavior is what the current Babylon Lite implementation does.
disposeScene() unregisters the scene from its surface immediately. Conversely, registerScene() does not attach the new scene until its deferred builders, task preloads, renderable sorting, and frame-graph build have completed. Therefore, a partially built scene is never rendered.
During the period when no scene is registered, the engine’s RAF loop continues, but renderFrame() detects that there are zero rendering contexts and returns before creating a command encoder, acquiring a new swapchain texture, clearing, or submitting anything.
So, assuming the canvas is not resized or reconfigured externally, the last rendered image remains on the canvas during the transition:
disposeScene(currentScene);
// The previous frame remains visible while this is running.
await registerScene(nextScene);
currentScene = nextScene;
A canvas resize during that interval is the main caveat, since changing its backing-store size may invalidate the preserved contents.
startEngine() is independent of scene registration. With no scenes, its render loop simply performs no rendering work; scenes registered later are included in subsequent frames. The source explicitly allows registrations after the loop has started.
One small detail: the promise returned by startEngine() resolves after the first RAF callback, even when no scene was registered and no GPU frame was submitted. It should therefore be understood as “the engine loop has started,” not “the initial scene is ready.”
In practice:
Call startEngine() once.
Register and dispose scenes independently afterward.
During a zero-scene gap, Babylon Lite leaves the previous canvas image untouched.