Babylon heavy performance lose after exiting XR

The mandatory sample: and in this case the issue is not as easy to be described so I’ve preferred to record a video:

By the way, it has the same problem in several platforms, not only Windows or Chrome, including other browsers and devices (Quest 2).
Thank you in advance.

adding @RaananW


The issue here is the mouse picking of this complex scene. Babylon runs a picking function on each frame, which uses your current mouse position over the canvas. when dealing with complex meshes it might take quite some time, because it runs picking against the actual triangle of each mesh it collides with (or better - with its bounding box/sphere). To eliminate this you can set each mesh you loaded to be not-pickable (or at least the more complex meshes). isPickable is set to true per default so you will need to actively set it to false.
Now, saince you are using the emulator, the mouse events are still executed on the canvas. this is why the FPS drops even when you are in XR. when outside of the emulator you won’t have the mouse issue, BUT you will still experience the same behavior when moving your xr controllers. that it because the xr controllers are emulating the pointer system! And they technically work the same as a mouse, but in the 3D space (as opposed to the mouse that works on the 2D canvas and always points “forward”). To disable this behavior you can detach the pointer system (which is part of the default xr experience), or, just like in the case of the mouse, make all meshes not-pickable (line 9, a very simple approach that you might want to adjust in your scenes):

Hill Valley in WebXR using Babylon.js | Babylon.js Playground (

Sorry, I think that my video was not clear enough, I don’t care about the initial performance with the same geometry where Babylon is making great work.

The problem is that AFTER you enter and exit from WebXR the performance is much worse than in the initial case, it seems that entering into WebXR mode makes something dirty in the pick system since if I disable picking in all the objects this behavior is no longer happening.

I am probably not detaching the control from the canvas (which can be seen by the fact that the mouse is still influencing the canvas), and then re-attaching. I’ll look into that.


Updating here - this was solved in this PR - [XR] detach and attach non-xr camera by RaananW · Pull Request #10950 · BabylonJS/Babylon.js (

1 Like

This example is very cool, but I have two big questions, valid for any other XR example I found:

  • once I enable the XR dualview mode… how do I exit? Using a cardboard-like viewer I need something to look at to enable it by tapping screen or just by looking at it for long enough, but I can’t see anything. Without exiting, I cannot debug on PC, because I cannot access code anymore!
  • How can use this and other examples locally? I can’t get the browser loading a local file due to CORS restriction, and I don’t want any server on my PC.

I tried with the documentation, but it’s quite confusing due to overlapping of WebVR with WebXR explanations, obsolete examples no more working, and so on.

@RaananW will be back soon from the winter break :slight_smile: please bear with us in the meantime

Feels like a totally different topic :slight_smile:

How to exit? Depends on the system you are using. Every platform has its own way to quit an XR experience. The quest, for example, is using the left controller’s menu button.
The exit button can also be added by you in the scene itself if you believe it is needed - just call exitXR on the experience helper.

Regarding not serving the files - that could be a bit complicated when it comes to XR, as usually you would not host the files on the XT headset. So a server is needed. a dev server will be helpful here, made available only on your local network

I found the solution to first isse in “exit immersive” button in WebXR emulator for chrome.

About the second one, why do yuo think files should not be hosted locally? This would make things much faster, and even independent of network, which is a great plus.
I think modern HTML5 and “file” tag could allow loading a local file selected by an user, I use this method for several of my scripts, but I don’t know how to use it in babylon:

ReadWriteJS/readwrite.html at main · jumpjack/ReadWriteJS (

Many different reasons. The main one is - if you have a headset and you want to access files you are currently editing on your laptop, you will need to serve them somehow. Second, I am not entirely sure WebXR will accept the file protocol as an accepted URL to launch a webxr experience, BUT i haven’t tested it TBH.
I won’t try to convince you to use a local server :slight_smile: . Just my 2 cents.

No need for this, the result of file loading can be put into an ArrayBuffer:

        fileInput.addEventListener("change", handleFileSelect);

        function handleFileSelect(event) {
            const file =[0];

            if (file) {
                const reader = new FileReader();

                reader.onload = function (e) {
                    const sceneData =;


I am trying to figure out how to pass this arraybuffer to ScenLoader.Append()

are you talking about serving models, or about serving your XR experience?

You could, for example, convert anything to base64 URLs and use them as regular URLs. it will work. But how are you going to serve the HTML and js to the headset?

EDIT - again, not trying to convince you to use a local server. I am just not sure why you wouldn’t use it, and how you are planning on using the implementation on a headset :-). If you have a solution for that - awesome!

it’s not for use in headset, is for debugging on my PC: if I am using a big model, I don’t want to wait 1 minute for each change I make to my code.

But anyway, also in headset I could allow user tapping a button to load a specific model before entering XR. (I don’t know how “actual headsets” work, I only have my smartphone as “headset”.)

ok, last message from me in that regard :slight_smile:

That’s not how a local server work. Caching does work, just like from a remote server. But again, you are talking about models, not about the HTML or the javascript files.

And again - I am not talking about models. I am talking about entering XR. Anyhow - if it works, it works! You are good to go. If you have a way to achieve that without a server, please use it :slight_smile:

I found a method to load a model from local filesystem:

<!DOCTYPE html>
				<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

				<title>Babylon.js sample code</title>

				<!-- Babylon.js -->
				<script src="babylon.js"></script>
				<script src="loaders.js"></script>

						html, body {
								overflow: hidden;
								width: 100%;
								height: 100%;
								margin: 0;
								padding: 0;

						#renderCanvas {
								width: 100%;
								height: 100%;
								touch-action: none;
						#canvasZone {
								width: 100%;
								height: 100%;
 <input type="file" id="fileInput" accept=".babylon">
		<div id="canvasZone"><canvas id="renderCanvas"></canvas></div>
				var canvas = document.getElementById("renderCanvas");
				const fileInput = document.getElementById("fileInput");
				fileInput.addEventListener("change", handleFileSelect);

				function handleFileSelect(event) {
						const file =[0];
						console.log("Loaded:", file);

					if (file) {
								const reader = new FileReader();
								reader.onload = function (e) {
										const arrayBuffer =;
										const assetBlob = new Blob([arrayBuffer]);
										const assetUrl = URL.createObjectURL(assetBlob);
						 					createScene = function() { return Playground.CreateScene(engine, engine.getRenderingCanvas()); }
											window.initFunction = async function() {
												var asyncEngineCreation = async function() {
														try {
														return createDefaultEngine();
														} catch(e) {
														console.log("the available createEngine function failed. Creating the default engine instead");
														return createDefaultEngine();

												window.engine = await asyncEngineCreation();
												if (!engine) throw 'engine should not be null.';
												startRenderLoop(engine, canvas);
												window.scene = createScene();
											initFunction().then(() => {
												sceneToRender = scene
												try {
													BABYLON.SceneLoader.AppendAsync( assetUrl, undefined, scene, undefined, ".babylon" );
												} catch (e) {
													console.log("Who cares textures?");

				var startRenderLoop = function (engine, canvas) {
						engine.runRenderLoop(function () {
								if (sceneToRender && sceneToRender.activeCamera) {

				var engine = null;
				var scene = null;
				var sceneToRender = null;
				var createDefaultEngine = function() { return new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true,	disableWebGL2Support: false}); };
				class Playground {
						static CreateScene(engine, canvas) {
								// This creates a basic Babylon Scene object (non-mesh)
								var scene = new BABYLON.Scene(engine);
								// This creates and positions a free camera (non-mesh)
								var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);
								// This targets the camera to scene origin
								// This attaches the camera to the canvas
								camera.attachControl(canvas, true);
								// This creates a light, aiming 0,1,0 - to the sky (non-mesh)
								var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
								// Default intensity is 1. Let's dim the light a small amount
								light.intensity = 0.7;
						 return scene;

				// Resize
				window.addEventListener("resize", function () {

It partially works:

  • model shall not have any texture (I can’t catch the “missing texture” exception)
  • although the model is properly loaded, scene freezes and I cannot do anything with mouse.