移动端与含Three.js的Iframe交互后页面崩溃问题求助
移动端Safari关闭含Three.js的Iframe模态框时页面崩溃/重载问题
我有一个嵌套Iframe的模态框,Iframe内通过Three.js加载3D门模型。桌面端运行正常,但移动端交互后关闭模态框时页面会崩溃或重载;Safari浏览器中首次触发页面重载,二次操作直接导致页面崩溃。已排查内存泄漏,未发现异常。
加载Iframe的页面代码
<iframe loading="lazy" id="door-viewer" src="<?= "{$site_url}/3d-spinner.php?sku={$unique_skus[0]}"; ?>" sandbox></iframe>
生成Iframe的文件代码
<?php $product_sku = isset($_GET['sku']) ? strtoupper($_GET['sku']) : null; $proto = $_SERVER['HTTP_X_FORWARDED_PROTO']; $name = $_SERVER['SERVER_NAME']; $site_url = "{$proto}://{$name}"; ?> <?php header("Content-Security-Policy: frame-ancestors 'self' ".file_get_contents(getcwd()."/3d-spinner-assets/allowed-domains.txt").";"; ?> <html> <head> <!-- Three JS CDN Scripts --> <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.155.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.155.0/examples/jsm/" } } </script> <!-- IFRAME STYLES --> <style> body { margin: 0; box-sizing: border-box; } canvas { object-fit: cover; width: 100% !important; height: 100% !important; aspect-ratio: 1; background-image: radial-gradient(at 50% 50%, rgb(245, 245, 245) 20%, rgb(206, 206, 206) 90%); } .lpd-360-image-icon { position: absolute; top: 2rem; left: 2rem; } #door-viewer { position: relative; } noscript { display: flex; align-items: center; justify-self: center; font-size: 2rem; height: 100vw; width: 100vw; } </style> </head> <body> <noscript> Please enable JavaScript and reload your browser </noscript> <?php if($product_sku) : ?> <div id="door-viewer"> <!-- Canvas is injected via Three.js --> </div> <?php else : ?> <div> <p>Product not found, check your sku</p> </div> <?php endif; ?> <script type="module"> const sku = "<?= preg_replace('/[^-a-zA-Z0-9_]/', '', $product_sku ); ?>" const site_url = "<?= $site_url; ?>" import initSpinner from "./3d-spinner-assets/assets/scripts/main.js" initSpinner(sku, site_url); </script> </body> </html>
Main.js文件代码
import * as THREE from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; function initSpinner(sku, site_url) { const doorAssetPath = `${site_url}/3d-spinner-assets/door/${sku}/model.gltf"`; const doorViewerCanvas = document.getElementById("door-viewer"); // Scene const scene = new THREE.Scene(); // Camera const camera = new THREE.PerspectiveCamera(75, 500 / 500, 0.1, 800); camera.position.z = 10; camera.position.y = 0; camera.position.x = 0; // Renderer const renderer = new THREE.WebGLRenderer({ antialias: true, devicePixelRatio: 1, alpha: true, }); renderer.setSize(1200, 1200); // Set Scene Size renderer.shadowMap.enabled = true; renderer.setClearColor(0xffffff, 0); // Make scene transparent renderer.setPixelRatio(window.devicePixelRatio); // Set Pixel Ratio in line with device // Scene Lighting const ambientLight = new THREE.AmbientLight(0x404040, 90); scene.add(ambientLight); // Controls const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; // Add damping for smoother motion controls.dampingFactor = 0.05; // Adjust the damping factor controls.rotateSpeed = 0.5; // Adjust rotation speed controls.zoomSpeed = 1.2; // Adjust zoom speed // Load Door const loader = new GLTFLoader(); loader.load(doorAssetPath, (gltfScene) => { const loadedDoor = gltfScene.scene; // Traverse the loadedDoor to set transparency for the windows const windowMaterials = []; // Add the window materials to this array // Inside the load callback function loadedDoor.traverse((child) => { if ( (child.isMesh && child.name === "glass") || child.name === "Glass" ) { const windowMaterial = child.material; windowMaterial.transparent = true; windowMaterial.opacity = 0; // Adjust opacity value windowMaterials.push(windowMaterial); } }); const scaleFactor = 0.003; // Adjust this value as needed loadedDoor.scale.set(scaleFactor, scaleFactor, scaleFactor); // Adjust door's scale loadedDoor.position.setY(-3); // Offset door's position scene.add(loadedDoor); // Add the door to the scene }); doorViewerCanvas.appendChild(renderer.domElement); // Mount canvas to the DOM function animate() { requestAnimationFrame(animate); controls.update(); // Update the controls renderer.render(scene, camera); } animate(); } export default initSpinner;
解决方案
1. 完善Three.js实例的销毁逻辑
移动端Safari对WebGL资源回收的处理严格,直接移除iframe易触发资源泄漏导致崩溃。需在iframe卸载前主动销毁Three.js相关实例:
修改Main.js,添加销毁函数并监听卸载信号:
import * as THREE from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; function initSpinner(sku, site_url) { const doorAssetPath = `${site_url}/3d-spinner-assets/door/${sku}/model.gltf"`; const doorViewerCanvas = document.getElementById("door-viewer"); // Scene const scene = new THREE.Scene(); // Camera const camera = new THREE.PerspectiveCamera(75, 500 / 500, 0.1, 800); camera.position.z = 10; camera.position.y = 0; camera.position.x = 0; // Renderer const renderer = new THREE.WebGLRenderer({ antialias: true, devicePixelRatio: 1, alpha: true, }); renderer.setSize(1200, 1200); // Set Scene Size renderer.shadowMap.enabled = true; renderer.setClearColor(0xffffff, 0); // Make scene transparent renderer.setPixelRatio(window.devicePixelRatio); // Set Pixel Ratio in line with device // Scene Lighting const ambientLight = new THREE.AmbientLight(0x404040, 90); scene.add(ambientLight); // Controls const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; // Add damping for smoother motion controls.dampingFactor = 0.05; // Adjust the damping factor controls.rotateSpeed = 0.5; // Adjust rotation speed controls.zoomSpeed = 1.2; // Adjust zoom speed // Load Door const loader = new GLTFLoader(); let loadedDoor = null; loader.load(doorAssetPath, (gltfScene) => { loadedDoor = gltfScene.scene; // Traverse the loadedDoor to set transparency for the windows const windowMaterials = []; // Add the window materials to this array // Inside the load callback function loadedDoor.traverse((child) => { if ( (child.isMesh && child.name === "glass") || child.name === "Glass" ) { const windowMaterial = child.material; windowMaterial.transparent = true; windowMaterial.opacity = 0; // Adjust opacity value windowMaterials.push(windowMaterial); } }); const scaleFactor = 0.003; // Adjust this value as needed loadedDoor.scale.set(scaleFactor, scaleFactor, scaleFactor); // Adjust door's scale loadedDoor.position.setY(-3); // Offset door's position scene.add(loadedDoor); // Add the door to the scene }); doorViewerCanvas.appendChild(renderer.domElement); // Mount canvas to the DOM let animationId; function animate() { animationId = requestAnimationFrame(animate); controls.update(); // Update the controls renderer.render(scene, camera); } animate(); // 新增销毁函数 function destroy() { // 取消动画帧 if (animationId) cancelAnimationFrame(animationId); // 销毁控制器 if (controls) controls.dispose(); // 移除场景中的模型 if (loadedDoor) scene.remove(loadedDoor); // 遍历场景销毁所有材质和几何体 scene.traverse((child) => { if (child.isMesh) { child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(m => m.dispose()); } else { child.material.dispose(); } } } }); // 销毁渲染器 if (renderer) renderer.dispose(); // 移除canvas元素 if (renderer.domElement) doorViewerCanvas.removeChild(renderer.domElement); } // 监听父页面发送的销毁信号 window.addEventListener('message', (event) => { if (event.data === 'destroy-3d-viewer') { destroy(); } }); // 监听iframe自身的卸载事件 window.addEventListener('unload', destroy); } export default initSpinner;
2. 优化iframe的sandbox属性
空白sandbox属性可能导致Safari异常,添加必要权限并限制不必要的:
<iframe loading="lazy" id="door-viewer" src="<?= "{$site_url}/3d-spinner.php?sku={$unique_skus[0]}"; ?>" sandbox="allow-scripts allow-same-origin"></iframe>
仅保留allow-scripts(允许执行脚本)和allow-same-origin(允许同源请求),平衡功能与安全性。
3. 关闭模态框时主动通知iframe销毁
在父页面关闭模态框逻辑中,先发送销毁信号再移除iframe:
// 父页面中关闭模态框的函数 function closeModal() { const iframe = document.getElementById('door-viewer'); if (iframe) { // 通知iframe销毁Three.js实例 iframe.contentWindow.postMessage('destroy-3d-viewer', '*'); // 延迟移除iframe,给销毁逻辑留执行时间 setTimeout(() => { iframe.remove(); }, 100); } // 其他关闭模态框的逻辑 }
4. 降低移动端渲染负载
减少WebGL资源占用,避免触发浏览器崩溃保护:
- 关闭
shadowMap:注释renderer.shadowMap.enabled = true;,移动端阴影渲染开销大 - 限制像素比:改为
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));,避免过高显存占用 - 动态设置分辨率:将
renderer.setSize(1200, 1200);改为根据容器尺寸动态设置,适配移动端屏幕
内容的提问来源于stack exchange,提问作者Jordan Wilson
相关产品推荐
相关产品推荐

