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效果:
解决方案
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
相关产品推荐
相关产品推荐

