FastClick在移动端点击延迟问题中的实战应用与优化经验

长孙怡博 交互 阅读 686
赞 12 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

我上周上线一个活动页,iOS上点按钮总要等300ms才响应——用户点完第一下没反应,下意识又点一次,结果触发了两次提交。后台直接报警,运营妹子在群里艾特我:“你那个抽奖按钮是不是卡了?”

FastClick在移动端点击延迟问题中的实战应用与优化经验

我打开 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-datepickerselect2,这两个库内部全是绑定 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 冲突的问题,我还在找完美解法。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论