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

Safari中requestAnimationFrame仅30fps,如何对齐Chrome120fps性能?

解决Three.js场景在Safari下帧率过低的问题

问题描述

我有一个用于移动并渲染平面的基础Three.js场景:

  • Chrome下帧率为120 fps
  • Safari下帧率仅为30 fps

需要实现跨浏览器性能归一化,使体验趋于一致,理想目标是达到120fps水平,Safari的30fps表现对体验影响极大。

已尝试设置powerPreference: "high-performance"但无明显改善,推测核心问题出在requestAnimationFrame的计时逻辑和Safari特定的WebGL优化上。

完整场景代码

JavaScript

const mod = (k, n) => ((k %= n) < 0)? k+n : k;
const lerp = (v0, v1, t) => (1 - t) * v0 + t * v1;
const fpsElem = document.querySelector("#fps");

const ThreeCarousel = {
  clock: new THREE.Clock(),
  sizes: {
    width: window.innerWidth,
    height: window.innerHeight
  },
  slideGap: 2,
  slides: [],
  cols: [
    0xC4E7D4,
    0x998DA0,
    0xC4DACF,
    0xB9C0DA,
    0x63585E,
  ],
  p: 0,
  targetP: 0,
  currentX: 0,
  dragReduce: 0.01,
  wheelReduce: 0.001,
  s: 0.01,
  slidePosition(i){
    const max = this.slides.length * this.slideGap;
    const p = mod(this.p + (i * this.slideGap), max)
    
    return p - max / 2;
    
  },
  addObjects() {
    for(var i = 0; i < 5; i++){
      const geometry = new THREE.PlaneGeometry(1.1, 1.5);
      const material = new THREE.MeshBasicMaterial({ color: this.cols[i] });
      const mesh = new THREE.Mesh(geometry, material);
      mesh.position.x = this.slidePosition(i);
      this.slides.push(mesh);
      this.scene.add(mesh);
    }
  },
  addCamera() {
    this.camera = new THREE.PerspectiveCamera(
      75,
      this.sizes.width / this.sizes.height,
      0.1,
      100
    );
    this.camera.position.z = 3;
    this.scene.add(this.camera);
  },
  addRenderer() {
    this.renderer = new THREE.WebGLRenderer({
      canvas: this.canvas,
      powerPreference: "high-performance",
    });
    this.renderer.setSize(this.sizes.width, this.sizes.height);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  },
  addEvents(){
    window.addEventListener("resize", this.resize.bind(this));
    this.canvas.addEventListener("pointerdown", this.startDrag.bind(this));
    this.canvas.addEventListener("pointerup", this.stopDrag.bind(this));
    this.canvas.addEventListener("pointercancel", this.stopDrag.bind(this));
    this.canvas.addEventListener("pointerout", this.stopDrag.bind(this));
    this.canvas.addEventListener("pointermove", this.drag.bind(this));
    this.canvas.addEventListener("wheel", this.wheelDrag.bind(this));
  },
  startDrag(e){
    this.dragging = true;
    this.currentX = e.screenX;
  },
  stopDrag(e){
    this.dragging = false;
  },
  drag(e){
    if(!this.dragging){
      return;
    }
    this.targetP = this.targetP - (this.currentX - e.screenX) * this.dragReduce;
    this.currentX = e.screenX;
  }, 
  wheelDrag(e){
    if(Math.abs(e.deltaY) > Math.abs(e.deltaX)){
      return;
    }
    e.preventDefault();
    e.stopPropagation();
    this.targetP += e.deltaX * this.dragReduce * -1;
  },
  resize(){
    this.sizes.width = window.innerWidth;
    this.sizes.height = window.innerHeight;

    this.camera.aspect = this.sizes.width / this.sizes.height;
    this.camera.updateProjectionMatrix();

    this.renderer.setSize(this.sizes.width, this.sizes.height);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  },
  init() {
    this.canvas = document.querySelector("canvas#carousel");
    this.scene = new THREE.Scene();
    this.addObjects();
    this.addCamera();
    this.addRenderer();
    this.addEvents();
    this.then = 0;

    const tick = (now) => {
      now *= 0.001;                          // convert to seconds
      const deltaTime = now - this.then;          // compute time since last frame
      this.then = now;                            // remember time for next frame
      const fps = 1 / deltaTime;             // compute frames per second
      fpsElem.textContent = fps.toFixed(1);  // update fps display
      this.p = lerp(this.p, this.targetP, 0.06);
      const elapsedTime = this.clock.getElapsedTime();
      this.slides.forEach((slide, i) => {
        slide.position.x = this.slidePosition(i);
        slide.rotation.y = (Math.PI / 30) * slide.position.x + Math.PI / 12;
      })
      this.renderer.render(this.scene, this.camera);
      window.requestAnimationFrame(tick);
    };

    tick();
  }
};

ThreeCarousel.init();

CSS

html, body, canvas{
  height: 100%;
}

div{
  position: fixed;
  top: 0;
  left: 0;
  color: #fff;
}

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.145.0/three.min.js"></script>

<canvas id="carousel"></canvas>

<div>fps: <span id="fps"></span></div>

已知问题

现有一些帧率节流的旧方案在Safari上无法达到60fps效果:
帧率节流旧方案Demo截图


解决方案

1. 修复requestAnimationFrame计时逻辑

Safari的requestAnimationFrame时间戳精度较低,且在非活跃状态下会被强制节流。改用performance.now()获取更精准的时间,并采用固定时间步长+累积时间的方式更新动画,确保动画速度在不同帧率下一致:

init() {
  // ... 其他初始化代码
  this.lastTime = performance.now();
  this.accumulator = 0;
  const fixedTimeStep = 1/120; // 目标120fps的时间步长

  const tick = () => {
    const currentTime = performance.now();
    const deltaTime = (currentTime - this.lastTime) / 1000;
    this.lastTime = currentTime;
    this.accumulator += deltaTime;

    // 用固定步长更新动画,避免帧率波动影响动画速度
    while (this.accumulator >= fixedTimeStep) {
      this.p = lerp(this.p, this.targetP, 0.06);
      this.slides.forEach((slide, i) => {
        slide.position.x = this.slidePosition(i);
        slide.rotation.y = (Math.PI / 30) * slide.position.x + Math.PI / 12;
      });
      this.accumulator -= fixedTimeStep;
    }

    // 计算当前帧率
    const fps = 1 / deltaTime;
    fpsElem.textContent = fps.toFixed(1);

    this.renderer.render(this.scene, this.camera);
    window.requestAnimationFrame(tick);
  };

  tick();
}

2. Safari专属WebGL优化

调整渲染器配置,减少Safari的性能开销:

addRenderer() {
  this.renderer = new THREE.WebGLRenderer({
    canvas: this.canvas,
    powerPreference: "high-performance",
    antialias: false, // 关闭抗锯齿(视觉可接受的话)
    alpha: false, // 不需要透明背景时关闭
    preserveDrawingBuffer: false // 避免不必要的内存占用
  });
  // 降低像素比,减少渲染压力
  this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
  this.renderer.setSize(this.sizes.width, this.sizes.height);
}

3. 动画逻辑优化

缓存slidePosition的计算结果,避免重复计算:

updateSlides() {
  const max = this.slides.length * this.slideGap;
  this.slides.forEach((slide, i) => {
    const p = mod(this.p + (i * this.slideGap), max);
    slide.position.x = p - max / 2;
    slide.rotation.y = (Math.PI / 30) * slide.position.x + Math.PI / 12;
  });
}

之后在tick的固定步长循环中调用this.updateSlides()即可。

4. 强制Safari启用高性能模式

通过CSS触发硬件加速:

canvas#carousel {
  transform: translateZ(0);
  will-change: transform;
}

内容的提问来源于stack exchange,提问作者Djave

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 20:15:38