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

如何让计时器中的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 14:21:01