FastClick在移动端点击延迟问题中的实战应用与优化经验
先看效果,再看代码
我上周上线一个活动页,iOS上点按钮总要等300ms才响应——用户点完第一下没反应,下意识又点一次,结果触发了两次提交。后台直接报警,运营妹子在群里艾特我:“你那个抽奖按钮是不是卡了?”
我打开 Safari Web Inspector 连真机调试,一瞅 Event Listeners,好家伙,click 事件确实被延迟了。不用猜,就是移动端经典的 300ms 点击延迟。这时候 FastClick 就是那种“不用想,直接装”的救命稻草。
亲测有效:加完 FastClick,iOS 上点击秒响应,安卓也顺滑很多(虽然安卓部分机型本来就没这问题)。下面是我现在用的最稳的一套写法,不是官网抄来的 demo,是我在三个项目里反复改、压测、上线后盯着 Sentry 错误日志验证过的版本。
核心代码就这几行
别整那些 npm install fastclick + import + FastClick.attach() 的花活儿,我现在全站都用 <script> 直接引入 CDN,原因很简单:快、稳、不和 webpack 打架,而且 FastClick 本身才 1.5KB(gzip 后),没必要折腾打包流程。
我用的是官方推荐的 CDN 地址(注意:不是 GitHub raw 链接,那个不稳定):
<script src="https://cdnjs.cloudflare.com/ajax/libs/fastclick/1.0.6/fastclick.min.js"></script>
然后在 DOM ready 后初始化,**但注意:必须等 <body> 渲染完成,且不能在 DOMContentLoaded 之前执行**。我以前图省事写在 <head> 里,结果报 document.body is null,折腾了半天才发现顺序错了。
这是我现在固定写的初始化方式(兼容旧版 iOS 和现代 SPA):
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
// 只在移动端启用,PC 端不干扰原生 click 行为
if (typeof FastClick !== 'undefined' && /iPad|iPhone|iPod|Android/.test(navigator.userAgent)) {
FastClick.attach(document.body);
// 关键:禁用 iOS 上的默认缩放,避免双击放大干扰
document.body.style.webkitTouchCallout = 'none';
document.body.style.webkitUserSelect = 'none';
}
}, false);
}
这个场景最好用:Vue 单页应用里混用原生组件
我们有个老项目是 Vue 2 + jQuery 插件混搭的,里面用了 bootstrap-datepicker 和 select2,这两个库内部全是绑定 click 的。不用 FastClick?iOS 上点日期弹层要卡半拍,用户手速快一点就直接点穿了。
但这里有个坑:FastClick 默认会把 click 事件代理到 body,而某些 jQuery 插件(比如早期版本的 select2)会在 document 上监听 click 来关闭下拉框。结果 FastClick 触发了两次 click(一次原生,一次 FastClick 模拟),下拉框闪一下就关了。
解决办法很简单:给需要 FastClick 的容器单独 attach,而不是整个 document.body:
// 只对 .app-content 区域启用 FastClick
const appContent = document.querySelector('.app-content');
if (appContent && typeof FastClick !== 'undefined') {
FastClick.attach(appContent);
}
HTML 结构大概是这样:
<body>
<header class="navbar">...</header>
<main class="app-content">
<div id="app"><!-- Vue mount point --></div>
</main>
<footer>...</footer>
</body>
这样既保住了 FastClick 的加速效果,又不会干扰 header 或 footer 里的原生交互逻辑(比如某些统计埋点用的 onclick 属性)。
踩坑提醒:这三点一定注意
- 别在 input[type=”file”] 上用 FastClick:iOS 下会导致文件选择框打不开,或者点了没反应。FastClick 官方文档其实写了,但我第一次还是踩了。解决方案是:给 file input 加个 class,初始化时排除掉 ——
FastClick.attach(element, { tapDelay: 200, exclude: ['input[type="file"]'] });不过更简单粗暴的做法是:全局禁用 FastClick 对 input 的处理:FastClick.prototype.focus = function(targetElement) {}(重写 focus 方法为空函数,防止它偷偷调 focus 导致 file input 失效) - 不要和 touch-action: manipulation 共存:有些同学图省事,在 CSS 里写了
* { touch-action: manipulation; },以为能替代 FastClick。结果发现 FastClick 初始化后反而失效了——因为 FastClick 内部依赖touchstart事件,而manipulation会阻止touchstart在非滚动区域触发。建议二选一:要么纯 CSS 方案(只用于简单按钮),要么纯 FastClick(兼容性更广) - SPA 路由切换后记得重新 attach(如果用了局部 attach):比如用 Vue Router 切换页面,
.app-content被替换了,原来的 FastClick 实例就丢了。我的做法是在路由守卫里手动销毁再重建:FastClick.destroy(document.querySelector('.app-content')),再 attach 新节点。当然,如果你 attach 的是document.body,这步就不用管。
高级技巧:配合 fetch 做点击防抖
FastClick 解决的是“延迟”,但没解决“误点”。我们有个支付按钮,用户狂点三次,后端收到三笔请求。这时候我就把 FastClick 和业务逻辑绑一起了:
let isProcessing = false;
document.querySelector('#pay-btn').addEventListener('click', function(e) {
if (isProcessing) {
e.preventDefault();
return;
}
isProcessing = true;
this.disabled = true;
this.textContent = '支付中...';
fetch('https://jztheme.com/api/pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId: 'xxx' })
})
.then(res => res.json())
.then(data => {
alert('支付成功');
})
.catch(err => {
alert('支付失败');
})
.finally(() => {
isProcessing = false;
this.disabled = false;
this.textContent = '立即支付';
});
});
注意:这段代码必须放在 FastClick 初始化之后执行,否则 click 事件可能还没被接管,防抖逻辑就白写了。
最后说两句
FastClick 不是银弹。它解决的是历史遗留问题,现代 Chrome 和 Safari 已经通过 <meta name="viewport" content="width=device-width, user-scalable=no"> + touch-action 基本消除了 300ms 延迟。但如果你还在维护老项目、要兼容 iOS 9~12、或者团队里有人坚持不用 modern CSS,那 FastClick 就是目前最省心的方案。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如配合 IntersectionObserver 做懒加载按钮优化、或者和 Pointer Events API 混合使用,后续会继续分享这类博客。
有更优的实现方式欢迎评论区交流——特别是你怎么处理 iOS 上 input[type="date"] 和 FastClick 冲突的问题,我还在找完美解法。

暂无评论