正确做法是用 requestAnimationFrame 替代 setInterval,配合索引推进与时间戳校验兜底;需控制边界、处理换行空格、避免频繁重排,并在完成时清理引用。

直接用 setInterval 模拟打字机容易卡顿或跳字
常见错误是每 100ms 就 element.textContent += str[i++],但没控制边界、没处理换行/空格、也没清理定时器。结果就是:文字突然多出一截、光标乱跳、或者页面滚动卡住。核心问题在于 DOM 频繁重排 + 没做节流。
正确做法是用 requestAnimationFrame 替代 setInterval,配合字符串索引推进,并在完成时清除引用:
function typeText(el, text, delay = 50) {
let i = 0;
const timer = () => {
if (i < text.length) {
el.textContent = text.slice(0, ++i);
requestAnimationFrame(timer);
}
};
requestAnimationFrame(timer);
}
// 调用
typeText(document.getElementById('typewriter'), 'Hello, world!');
- 避免用
innerHTML拼接,防止 XSS 和标签解析干扰 - 若文本含 HTML 标签,需先用
textContent清空再用innerHTML插入,但必须确保内容可信 - 延迟值
delay实际不生效——requestAnimationFrame自动按帧率(约 60fps)执行,视觉节奏更稳
cursor 光标闪烁必须用 border-right 或伪元素,不能靠 opacity 动画硬切
很多人写 .cursor { animation: blink 1s step-end infinite; } 配合 @keyframes blink { 0%, 100% { opacity: 0; } 50% { opacity: 1; } },结果光标在渐显中途“抽搐”或消失。这是因为 opacity 动画和文字渲染不同步,且部分浏览器会合并重绘。
推荐方案:用 border-right 模拟光标,通过 animation 控制宽度或颜色变化:
立即学习“前端免费学习笔记(深入)”;
.typewriter::after {
content: '';
display: inline-block;
width: 2px;
background-color: currentColor;
animation: cursor-blink 1s steps(1) infinite;
}
@keyframes cursor-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
- 必须加
steps(1),否则光标会淡入淡出,失去“开关式”闪烁感 - 不要给伪元素设
height,让它继承行高;否则在多行文本中光标位置错乱 - 如果文字用了
line-height: 1.5,伪元素会自动对齐基线,无需额外vertical-align
逐字渐显 + 打字机效果混用时,transition 会失效
有人想让每个字进来时带淡入,于是给 span 加 opacity: 0; transition: opacity 0.3s;,再 JS 动态添加 class="show"。结果所有字一起闪出来,或完全不动画。根本原因是:DOM 批量插入时,浏览器把多个样式变更合并成一次重绘,transition 没捕获到“从 0 到 1”的起始瞬间。
真正可行的解法只有两个:
- 用
@keyframes+animation-delay:每个span的动画延迟 =i * 0.05s,由 JS 动态写入style属性 - 放弃 CSS 过渡,改用
requestAnimationFrame控制每个字的opacity值,从 0 线性增至 1(耗时约 10 帧)
前者更轻量,后者更可控。示例(延迟动画):
text.split('').forEach((char, i) => {
const span = document.createElement('span');
span.textContent = char;
span.style.animation = `fadeIn 0.3s ease-out ${i * 0.05}s 1 forwards`;
container.appendChild(span);
});
移动端 Safari 下 requestAnimationFrame 可能被节流,得加兜底
iOS 15+ 的 Safari 在后台标签页或低功耗模式下,会把 requestAnimationFrame 降频到 10fps 甚至暂停。用户切回页面时,打字效果就“突突突”一下全出来。这不是 bug,是系统策略。
必须加时间戳校验兜底:
function typeText(el, text, speed = 50) {
const startTime = performance.now();
let i = 0;
const tick = (now) => {
const elapsed = now - startTime;
const targetIndex = Math.min(Math.floor(elapsed / speed), text.length);
if (i < targetIndex) {
el.textContent = text.slice(0, ++i);
requestAnimationFrame(tick);
}
};
requestAnimationFrame(tick);
}
- 不用
setTimeout回退——它在 iOS 后台可能被延后数秒 -
performance.now()比Date.now()更精准,且不受系统时间修改影响 - 这个逻辑在 Chrome/Firefox/Safari 桌面端和移动端都一致,无兼容性分支
最易被忽略的是:打字机效果本质是「节奏控制」,不是「动画实现」。CSS 动画负责视觉反馈,JS 负责时间轴调度,两者不能混用同一套触发逻辑。一旦开始用 requestAnimationFrame 推进文字,就别再指望 transition 自动补帧。



















