如何让计时器中的SVG环形进度条动画更流畅?
优化环形进度条流畅度的方案
你的问题核心是短计时场景下,setInterval 手动更新 stroke-dashoffset 带来的帧率不足、性能损耗问题,以下是几个针对性优化方案:
1. 改用CSS原生动画控制进度(推荐)
浏览器对CSS动画的调度优先级更高,能稳定维持60fps的流畅度,完全规避JS定时器的精度偏差问题。具体做法是动态设置动画时长,让CSS自动完成进度过渡:
修改后的核心代码
JavaScript 部分
class Timer { constructor(circle, text) { this.circle = circle this.text = text // 预计算圆周长度,避免重复DOM查询触发重排 this.circumference = Math.PI * this.circle.getBoundingClientRect().width this.circle.style.strokeDasharray = this.circumference this.circle.style.strokeDashoffset = this.circumference } async start(hours, minutes, seconds) { await this.countdown() this.circle.classList = "progress" const duration = parseInt(hours) * 3600000 + parseInt(minutes) * 60000 + (parseInt(seconds) + 1) * 1000 const end = Date.now() + duration // 动态设置CSS动画时长 this.circle.style.setProperty('--timer-duration', `${duration / 1000}s`) this.circle.style.setProperty('--circle-circumference', this.circumference) this.circle.classList.add('timer-animation') // 仅用定时器更新文本,进度完全交给CSS处理 const interval = setInterval(() => { const remaining = end - Date.now() if (remaining < 0) { clearInterval(interval) this.circle.style.strokeDashoffset = 0 window.location.href = "./" return } this.text.innerHTML = `${("0" + parseInt(remaining / 3600000)).slice(-2)}:${("0" + parseInt(remaining % 3600000 / 60000)).slice(-2)}:${("0" + parseInt(remaining % 3600000 % 60000 / 1000)).slice(-2)}` }, 100) } countdown() { let duration = 2 this.text.innerHTML = 3 return new Promise(resolve => { const timer = setInterval(() => { if (duration <= 0) { clearInterval(timer) resolve(true) } else { this.text.innerHTML = duration duration -= 1 } }, 1000) }) } } const timer = new Timer(document.getElementById("progress"), document.getElementById("text")) const params = new URLSearchParams(window.location.search) timer.start(0, 0, 10)
CSS 部分
:root { --pi: 3.141592653589793; --timer-duration: 0s; /* 默认值,JS动态修改 */ --circle-circumference: 0; /* 默认值,JS动态修改 */ } circle.progress { display: block; position: absolute; fill: none; stroke: url(#circle.progress.color); stroke-width: 4.5vmin; stroke-linecap: round; transform-origin: center; transform: rotate(-90deg); } /* 新增定时器专属动画 */ circle.progress.timer-animation { animation: timer-progress var(--timer-duration) linear forwards; } @keyframes timer-progress { from { stroke-dashoffset: var(--circle-circumference); } to { stroke-dashoffset: 0; } } /* 原CSS样式保留 */ .progress-container { left: 50vw; top: 50vh; width: 90vmin; height: 90vmin; margin-top: -45vmin; margin-left: -45vmin; position: absolute; padding: none; } .outer { margin: none; width: 100%; height: 100%; border-radius: 50%; box-shadow: 6px 6px 10px -1px rgba(0, 0, 0, 0.15), -6px -6px 10px -1px rgba(255, 255, 255, 0.7); padding: 2.5%; } .inner { margin: 2.5%; width: 95%; height: 95%; border-radius: 50%; box-shadow: inset 4px 4px 6px -1px rgba(0, 0, 0, 0.15), inset -4px -4px 6px -1px rgba(255, 255, 255, 0.7); } svg { display: block; position: absolute; left: 50vw; top: 50vh; width: 90.5vmin; height: 90.5vmin; margin-top: -45.25vmin; margin-left: -45.25vmin; } svg text { font-size: 10vmin; font-family: 'Roboto', sans-serif; } /* 移除原circle动画,改用新的定时器动画 */ @keyframes circle { 0% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: calc(45.5vmin * var(--pi) * 2); } }
HTML 部分(移除原冗余的animation类)
<link href="https://cdn.jsdelivr.net/npm/@materializecss/materialize@1.2.1/dist/css/materialize.min.css" rel="stylesheet"/> <div class="progress-container"> <div class="outer center-align"> <div class="inner"></div> </div> </div> <svg xmlns="http://www.w3.org/2000/svg" class="center" version="1.1"> <defs> <linearGradient id="circle.progress.color"> <stop offset="0%" stop-color="BlueViolet" /> <stop offset="100%" stop-color="MediumVioletRed" /> </linearGradient> </defs> <circle id="progress" class="progress" cy="45.25vmin" cx="45.25vmin" r="42.75vmin" /> <text id="text" text-anchor="middle" x="50%" y="50%">Temps restant</text> </svg> <script src="https://cdn.jsdelivr.net/npm/@materializecss/materialize@1.2.1/dist/js/materialize.min.js"></script>
2. 若坚持用JS控制,改用requestAnimationFrame
如果必须依赖JS逻辑控制进度,替换setInterval为requestAnimationFrame,它会和浏览器刷新周期同步(通常60fps),避免定时器的累积延迟问题:
// 替换原setInterval代码块 const updateProgress = () => { const remaining = end - Date.now() this.circle.style.strokeDashoffset = remaining * this.circumference / duration; if (remaining < 0) { this.circle.style.strokeDashoffset = 0 window.location.href = "./" return } this.text.innerHTML = `${("0" + parseInt(remaining / 3600000)).slice(-2)}:${("0" + parseInt(remaining % 3600000 / 60000)).slice(-2)}:${("0" + parseInt(remaining % 3600000 % 60000 / 1000)).slice(-2)}` requestAnimationFrame(updateProgress) } requestAnimationFrame(updateProgress)
3. 减少DOM重排操作
原代码中每次更新都调用getBoundingClientRect(),这会触发浏览器重排。提前计算圆周长度并缓存,避免重复查询DOM:
// 在constructor中预计算并缓存 this.circumference = Math.PI * this.circle.getBoundingClientRect().width // 后续更新直接使用this.circumference即可
内容的提问来源于stack exchange,提问作者JUNG Jules
相关产品推荐
相关产品推荐

