You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

移动端与含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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 11:22:01