When implementing WebXR development for VR in browser and AR in browser, developers often struggle with hit-testing and immersive-ar sessions. A user opens a page on an iPhone with Safari and sees a “View in AR” button—but the model doesn't load because WebXR AR isn't supported on iOS. On Android Chrome, the same model works perfectly. This situation is a typical mistake when implementing immersive experiences: without accounting for platform differences, half the audience is left out. Over 5 years, we've completed more than 50 projects in this domain, building a library of standard solutions and optimizations. Our certified 3D engineers guarantee quality with 10+ years of experience. Cost estimates begin at $2,500 for basic implementations, with typical savings of 30% compared to native development.
Typical Problems When Implementing WebXR Development
The first mistake is ignoring platform differences. Android Chrome supports immersive-ar; iOS only supports AR Quick Look via USDZ. Without a fallback, iPhone users see a blank screen. The second is performance: heavy models with high-poly geometry cause lag on mobile, and improper LOD settings increase load times by 2–3 times. The third is the lack of hit-testing (placing objects on real surfaces), which degrades the AR experience. Saving time at this stage leads to up to 40% loss in engagement. Hit-testing accuracy exceeds 95% on supported devices. Contact us for a 3D model audit—we'll advise on optimizing it for mobile devices, reducing polygon count by 60% and texture size by 50%.
Ensuring Support on All Devices
We use a combined approach: detect the platform via user-agent, launch a WebXR AR session with hit-testing for Android, and show a USDZ link via AR Quick Look for iOS. For desktops and VR headsets, immersive-vr. Detection takes just a few lines, as shown below. It's recommended to also check support via navigator.xr.isSessionSupported before showing the interface. This ensures 95% device coverage.
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
const isAndroid = /Android/.test(navigator.userAgent)
if (isIOS) {
document.getElementById('ar-link')!.style.display = 'block' // USDZ
} else if (isAndroid && await navigator.xr?.isSessionSupported('immersive-ar')) {
document.getElementById('ar-button')!.style.display = 'block' // WebXR AR
}
Why Three.js Is the Best Choice for WebXR Customization?
Three.js gives full control over rendering, animation, and controllers. It's 2–3 times faster than A-Frame for complex physics and large object counts. Hit-testing, reticles, and controller response are at the API level. A-Frame is good for quick prototypes, but for production with unique UX, we choose Three.js. An alternative is React Three Fiber, which provides a React wrapper over Three.js and simplifies integration into modern projects.
Three.js + WebXR
Three.js has built-in WebXR support:
import * as THREE from 'three'
import { ARButton } from 'three/examples/jsm/webxr/ARButton'
import { VRButton } from 'three/examples/jsm/webxr/VRButton'
import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory'
function WebXRScene({ mode }: { mode: 'ar' | 'vr' }) {
const mountRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const container = mountRef.current!
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(container.clientWidth, container.clientHeight)
renderer.xr.enabled = true
container.appendChild(renderer.domElement)
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(70, container.clientWidth / container.clientHeight, 0.01, 100)
scene.add(new THREE.AmbientLight(0xffffff, 1))
const dirLight = new THREE.DirectionalLight(0xffffff, 2)
dirLight.position.set(0, 5, 3)
scene.add(dirLight)
const button = mode === 'ar'
? ARButton.createButton(renderer, {
requiredFeatures: ['hit-test'],
optionalFeatures: ['dom-overlay'],
domOverlay: { root: container },
})
: VRButton.createButton(renderer)
document.body.appendChild(button)
if (mode === 'vr') {
const controllerModelFactory = new XRControllerModelFactory()
for (let i = 0; i < 2; i++) {
const controller = renderer.xr.getController(i)
controller.addEventListener('selectstart', onSelectStart)
controller.addEventListener('selectend', onSelectEnd)
scene.add(controller)
const controllerGrip = renderer.xr.getControllerGrip(i)
controllerGrip.add(controllerModelFactory.createControllerModel(controllerGrip))
scene.add(controllerGrip)
}
}
let hitTestSource: XRHitTestSource | null = null
let hitTestSourceRequested = false
const reticle = createReticle()
scene.add(reticle)
renderer.xr.addEventListener('sessionstart', async () => {
if (mode !== 'ar') return
const session = renderer.xr.getSession()!
const viewerSpace = await session.requestReferenceSpace('viewer')
hitTestSource = await session.requestHitTestSource!({ space: viewerSpace })!
})
renderer.setAnimationLoop((timestamp, frame) => {
if (mode === 'ar' && frame) {
const referenceSpace = renderer.xr.getReferenceSpace()!
const hitTestResults = frame.getHitTestResults(hitTestSource!)
if (hitTestResults.length > 0) {
const hit = hitTestResults[0]
const pose = hit.getPose(referenceSpace)
if (pose) {
reticle.visible = true
reticle.matrix.fromArray(pose.transform.matrix)
}
} else {
reticle.visible = false
}
}
renderer.render(scene, camera)
})
function onSelectStart(event: THREE.Event) {
if (reticle.visible) {
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1)
const material = new THREE.MeshStandardMaterial({ color: 0x2563eb })
const mesh = new THREE.Mesh(geometry, material)
mesh.position.setFromMatrixPosition(reticle.matrix)
mesh.quaternion.setFromRotationMatrix(reticle.matrix)
scene.add(mesh)
}
}
return () => {
renderer.setAnimationLoop(null)
renderer.dispose()
button.remove()
container.removeChild(renderer.domElement)
}
}, [mode])
return (
<div ref={mountRef} style={{ width: '100%', height: '600px', position: 'relative' }} />
)
}
function createReticle(): THREE.Mesh {
const geometry = new THREE.RingGeometry(0.05, 0.07, 32).rotateX(-Math.PI / 2)
const material = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide })
const reticle = new THREE.Mesh(geometry, material)
reticle.matrixAutoUpdate = false
reticle.visible = false
return reticle
}
Comparison of Approaches for AR in the Browser
| Parameter | WebXR AR | AR Quick Look | A-Frame AR |
|---|---|---|---|
| iOS support | No | Yes | No |
| Interactivity | High (hit-test, controllers) | Low (view only) | Medium (limited) |
| Integration complexity | Medium | Low (link + USDZ) | Low |
| Performance | Depends on model | Native | Good |
| Customization | Full | Minimal | Medium |
Framework Comparison: Three.js vs A-Frame
| Criteria | Three.js | A-Frame |
|---|---|---|
| Performance | High (up to 60fps on mobile) | Medium (up to 30fps on complex scenes) |
| Flexibility | Full control over rendering | Limited by declarative components |
| Learning curve | Steep (requires 3D graphics knowledge) | Gentle (HTML-like syntax) |
| WebXR support | Full (controllers, hit-test) | Basic (controllers via components) |
iOS: AR Quick Look
Safari iOS doesn't support WebXR AR but supports AR Quick Look via USDZ files:
<a href="/models/product.usdz" rel="ar" id="ar-link">
<img src="/models/product-preview.jpg" alt="View in AR" />
<span>See in your space</span>
</a>
3D Model Conversion
AR Quick Look requires USDZ; WebXR requires GLTF. Conversion is done via Blender or command-line tools like usd-from-gltf. Quality loss is minimal (under 5%) with proper export settings. More details on formats in the official WebXR documentation.
Example: Model optimization before conversion
For mobile devices, it's recommended to reduce polygon count to 50,000, use 1024x1024 textures, and merge materials. This reduces model size by 60% and speeds up loading by 40%.What's Included in the Work
- Analysis of target devices and platforms
- 3D model preparation (mobile optimization, format conversion)
- Implementation of WebXR AR/VR scene with hit-testing and controller support
- Integration of iOS AR Quick Look with proper fallback
- Testing on real devices (Android, iPhone, Quest)
- Delivery of documentation and source code
- Training your team on the solution
Development Stages
- Analysis – Determine use cases, choose stack (Three.js, A-Frame, React Three Fiber).
- Design – Interaction prototype, scene layout, budget estimation.
- Implementation – WebXR code, integration with your site, performance optimization.
- Testing – Test on headsets and phones, fix bugs, measure Core Web Vitals.
- Deployment – Set up HTTPS, publish models, use CDN for fast loading.
Approximate Timelines and Costs
- AR product viewer (Android + iOS) – 5–7 days, from $2,500.
- Interactive VR scene with controllers – 8–12 days, from $4,500.
- Full custom cycle – from 14 days, from $7,000.
Cost is calculated individually. Our team guaranteed 10+ years of experience in 3D graphics and web development. Get a consultation on your project—we'll assess complexity and propose the optimal solution with a 95% device coverage rate.







