news 2026/9/11 16:04:01

Langfuse 前端性能优化:为触摸与滚轮事件使用 Passive Event Listeners 消除滚动延迟

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Langfuse 前端性能优化:为触摸与滚轮事件使用 Passive Event Listeners 消除滚动延迟

Langfuse 前端性能优化:为触摸与滚轮事件使用 Passive Event Listeners 消除滚动延迟

【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse

导读

本指南基于 langfuse 仓库内 Vercel 工程团队的 React/Next.js 最佳实践规则库(vercel-react-best-practices)中的client-passive-event-listeners规则,深入讲解如何在触摸(touchstart/touchmove)与滚轮(wheel)事件监听器上添加{ passive: true },以消除浏览器为等待preventDefault()判定而产生的滚动延迟。读完本文,你将掌握 passive 监听器的底层原理、正确的代码写法、适用与禁用场景,以及 langfuse 仓库中真实的工程化落地案例。

为什么 passive 监听器能消除滚动延迟

当浏览器遇到一个非 passive(默认模式)的touchwheel事件监听器时,它无法预知该监听器是否会调用preventDefault()来取消滚动。因此浏览器必须同步等待监听器执行完毕,才能决定是否继续滚动页面。这一等待过程会直接造成肉眼可感知的滚动卡顿与延迟。

{ passive: true }的含义是向浏览器明确承诺:这个监听器不会调用preventDefault()。有了这一承诺,浏览器可以放心地立即开始滚动,无需等待监听器返回,从而获得即时的滚动响应。该规则在规则库中被标记为impact: MEDIUM,其 impact 描述即为 "eliminates scroll delay caused by event listeners"(消除事件监听器造成的滚动延迟),归属于客户端数据获取与交互(client-前缀)这一 MEDIUM-HIGH 优先级类别。

错误写法:未声明 passive 造成滚动阻塞

下面是在 ReactuseEffect中注册全局触摸与滚轮监听器的错误示范(与规则文档原文一致):

useEffect(() => { const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) const handleWheel = (e: WheelEvent) => console.log(e.deltaY) document.addEventListener('touchstart', handleTouch) document.addEventListener('wheel', handleWheel) return () => { document.removeEventListener('touchstart', handleTouch) document.removeEventListener('wheel', handleWheel) } }, [])

问题在于:handleTouchhandleWheel都只是读取事件数据(触摸点横坐标clientX、滚轮位移deltaY)用于日志或埋点,并不需要取消默认行为,却以默认的非 passive 方式注册。浏览器每次滚动都必须先等待这两个回调执行完毕并确认没有调用preventDefault(),滚动手感因此变差。

此外,代码在useEffect的清理函数中对称地调用了removeEventListener,这是一个正确的习惯,但只移除而不声明 passive 并不解决延迟问题。同时注意:useEffect的依赖数组为[],回调内部也没有使用组件状态,这与 React Hooks 的约定一致,是真实项目中监听器的标准挂载方式——问题纯粹出在缺少 passive 选项上。

正确写法:显式声明{ passive: true }

只需在addEventListener的第三个参数中传入{ passive: true }即可(与规则文档原文一致):

useEffect(() => { const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) const handleWheel = (e: WheelEvent) => console.log(e.deltaY) document.addEventListener('touchstart', handleTouch, { passive: true }) document.addEventListener('wheel', handleWheel, { passive: true }) return () => { document.removeEventListener('touchstart', handleTouch) document.removeEventListener('wheel', handleWheel) } }, [])

两点补充说明:

  • 移除监听时无需重复声明 passiveremoveEventListener只需要传入事件类型与回调函数引用即可正确解绑,passive 标志只影响注册行为,不影响移除。因此上面的清理函数保持不变。
  • 关于默认行为的浏览器差异:在现代浏览器中,windowdocumentbody上的touchstart/touchmove/wheel监听器在部分场景下默认即为 passive;但注册在其他元素(如可滚动容器<div>)上时默认仍是非 passive,需要显式声明{ passive: true }。因此最稳妥的做法是不依赖浏览器默认值,一律显式书写选项,规则文档也正是这样要求的。

何时使用、何时禁用:一条简单的判断标准

规则文档给出了清晰的边界条件:

应当使用 passive({ passive: true })的场景:

  • 埋点与统计(tracking/analytics);
  • 日志记录(logging);
  • 任何不会调用preventDefault()的监听器。

不得使用 passive 的场景:

  • 实现自定义滑动手势(custom swipe gestures);
  • 实现自定义缩放控制(custom zoom controls);
  • 任何需要调用preventDefault()的监听器。

注意:在 passive 为true的监听器中调用preventDefault()是无效的(浏览器会忽略,并在控制台输出警告)。因此在需要拦截默认行为的场景下,必须保持 passive 为false(或省略该选项),此时可以接受相应的滚动性能代价。

Langfuse 仓库中的真实落地案例

规则并非纸上谈兵,langfuse 前端代码中已有两处典型的正确用法,以及一处刻意关闭 passive 的反例,可以作为对照学习的范本。

案例一:滚动监听器 + passive(追踪类用途)

useVirtualizedScrollSpy.ts 中,虚拟化列表的滚动监听器只负责判断当前选中项是否离开了缓冲区,以便清除 fallback 选中态——纯读取、不拦截滚动:

scrollElement.addEventListener("scroll", clearFallbackOutsideBuffer, { passive: true, }); return () => scrollElement.removeEventListener("scroll", clearFallbackOutsideBuffer);

这里不仅显式声明了{ passive: true },还通过useEffect的清理函数对称解绑,正是规则推荐"组件卸载即释放监听器"的实践。注意本规则的判断同样适用于scroll事件——只要回调不调用preventDefault(),passive 就是正确的选择。

案例二:wheel / touchmove 追踪 + passive 组合选项

usePreserveRelativeScroll.ts 需要在用户手动滚动(wheel、touchmove、方向键)时记录"用户已主动滚动"这一状态,从而决定是否保留相对滚动位置。这些监听器同样只标记状态、不阻止滚动,因此使用了{ passive: true, once: true }的组合选项:

window.addEventListener("wheel", cancel, { passive: true, once: true }); window.addEventListener("touchmove", cancel, { passive: true, once: true });
  • passive: true:保证监听器不会阻塞滚动;
  • once: true:首次触发后自动移除监听器,避免长期占用全局监听资源(与规则库中client-event-listeners提倡的"去重全局监听器"思路一致,详见 client-event-listeners.md)。

由于监听对象可能是window或任意滚动容器target,代码中对两处addEventListener的第三个参数都做了AddEventListenerOptions类型断言,保证 TypeScript 类型安全,这也是在真实项目中落地该模式时值得注意的细节。

案例三:必须关闭 passive 的反例(自定义手势场景)

规则中"需要preventDefault()时不得使用 passive"的约束,在 TimelineDense.tsx 中有直接的对照实现。该组件实现时间线(Trace Timeline)的滚轮缩放功能,代码注释明确指出滚轮与触控板捏合缩放必须运行在非 passive 监听器上,否则无法通过preventDefault()阻止浏览器原生页面滚动,因而显式使用了{ passive: false }

element.addEventListener("wheel", onWheel, { passive: false });

这恰好印证了规则文档的边界条件:当监听器需要接管默认行为(这里是阻止页面滚动以实现时间线缩放)时,必须放弃 passive 带来的性能收益。性能优化不是无条件堆叠 passive,而是根据监听器职责做正确选择。

实践清单与代码审查要点

在编写或审查 langfuse(或任何 React/Next.js 项目)的前端代码时,可以依据以下清单快速判断:

监听器职责是否调用preventDefault()推荐选项
埋点、日志、只读追踪{ passive: true }
自定义滑动/缩放/拖拽手势{ passive: false }(或省略)
仅需触发一次的滚动状态标记{ passive: true, once: true }

关键要点:

  1. 默认即延迟:不给touch/wheel监听器声明 passive,浏览器就会为等待preventDefault()判定而阻塞滚动;
  2. 只读监听一律 passive:不拦截默认行为的监听器应显式声明{ passive: true }
  3. 拦截行为禁开 passive:需要preventDefault()的监听器必须保持非 passive,并接受相应代价;
  4. 配合清理与去重:在useEffect清理函数中对称removeEventListener,并参考 client-event-listeners.md 对全局监听器做去重,避免 N 个组件实例产生 N 个重复监听器。

通过上述实践,langfuse 这类包含大量表格、时间线、虚拟化列表等高滚动频率交互的观测平台(观测相关源码、虚拟化滚动监听),能够在保持交互完整性的同时,把滚动卡顿降到最低,让工程师在浏览海量 trace 数据时获得流畅的体验。

【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 16:02:49

SSM框架软件项目管理系统实战:从三层架构到MySQL优化

简介&#xff1a;本资源是基于SSM框架的Java软件项目管理系统完整毕设源码包&#xff0c;面向计算机专业毕业生、Java学习者及需要期末大作业的同学。系统围绕软件项目全生命周期管理&#xff0c;覆盖用户、项目、任务、文档及系统设置等核心模块&#xff0c;能够帮助读者快速理…

作者头像 李华
网站建设 2026/9/11 16:02:20

Java 21下Lombok兼容性问题解决方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 16:02:12

Ente Ensu 更新日志解读:从 v0.1.3 到 v0.1.19 的本地 LLM 应用演进

Ente Ensu 更新日志解读&#xff1a;从 v0.1.3 到 v0.1.19 的本地 LLM 应用演进 【免费下载链接】ente &#x1f49a; End-to-end encrypted cloud for everything. 项目地址: https://gitcode.com/GitHub_Trending/en/ente Ente Ensu 是 Ente 旗下的本地优先 AI 聊天应…

作者头像 李华
网站建设 2026/9/11 16:02:02

基于SSM框架的社区老人关爱管理系统设计与实现

1. 项目背景与核心价值社区老人关爱管理系统是当前智慧社区建设中的重要组成部分。随着我国老龄化程度不断加深&#xff0c;2023年数据显示65岁以上人口占比已达14.9%&#xff0c;传统人工管理模式已难以满足社区养老服务的精细化需求。这个基于SSM框架的Java毕设项目&#xff…

作者头像 李华
网站建设 2026/9/11 16:01:19

ESP32-S3端云协同AI架构:实现可持续演进的嵌入式智能

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 15:59:35

安卓JSON解析异常全攻略:从原理到实战的排查与防御指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华