
Polar 前端性能实践为 React 滚动事件启用 Passive 事件监听器【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar本篇技术指南以 Polar 前端仓库内置的 Vercel React Best Practices 技能规则 为主体系统讲解{ passive: true }对 touch、wheel、scroll 等滚动相关事件的性能影响、适用场景与反模式并结合 Polar 代码库中的真实实现useStickToBottom给出可直接落地的工程实践。读完本文你将理解被动事件监听器消除滚动延迟的底层原理掌握在 React/Next.js 项目中正确声明 passive 并避免preventDefault()失效陷阱的完整方案。背景滚动卡顿的根源与 passive 的由来浏览器通常把滚动交由独立的合成器线程处理以保持 60fps 的流畅度。但当你通过addEventListener()监听touchstart、touchmove、wheel等事件时情况发生了变化浏览器无法预知监听器内部是否会调用preventDefault()来取消默认滚动行为因此必须等待所有监听器在主线程执行完毕才能决定是否继续滚动。这一先执行 JS、再决定滚动的串行等待正是触屏与滚轮操作出现肉眼可见延迟scroll delay的直接原因——监听器越多、执行越慢延迟越明显。Passive被动事件监听器正是针对这一问题的标准化方案当你向浏览器声明passive: true即承诺该监听器不会调用preventDefault()合成器线程便无需等待可以立即开始滚动。这正是规则文件 frontmatter 中impact: MEDIUM、impactDescription: eliminates scroll delay caused by event listeners的含义所在。规则核心给 touch 与 wheel 监听器显式声明 passivePolar 技能包中的规则原文给出了明确的改法。先看错误写法——监听器未声明任何选项浏览器出于安全会保守地等待执行结果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) } }, [])正确写法——为不需要阻止默认行为的监听器显式传入{ 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) } }, [])注意一个容易被忽略的细节removeEventListener无需也不必重复传入{ passive: true }——浏览器匹配监听器是否相同只依赖capture标志passive不参与匹配。上面的清理函数写法是正确的。何时启用 passive何时必须关闭规则文件最后给出了两条清晰的判断准则它们是决定passive取值的关键场景是否启用 passive典型例子埋点统计 / tracking是上报触摸坐标、滚动位置日志记录 / logging是打印deltaY、clientX等诊断信息任何不调用preventDefault()的监听器是被动观察滚动状态、驱动 UI 同步自定义滑动手势swipe否{ passive: false }下拉刷新、横向滑动切换自定义缩放控制zoom否{ passive: false }双指捏合缩放地图/画布任何需要preventDefault()的监听器否{ passive: false }阻止原生滚动、屏蔽默认交互判断口诀很简单只读事件不拦事件就用 passive要拦截默认行为就必须passive: false。仓库实战Polar 中 useStickToBottom 的 passive 用法Polar 代码库中有一个非常契合本规则的真实案例useStickToBottom.ts。该 Hook 用于让滚动容器在流式内容如 AI 对话输出、实时增长的区块、图表高度变化增长时始终贴底同时不干扰用户主动滚动。其核心思路是通过ResizeObserver观察内容变化用requestAnimationFrame把滚动写入合并到每帧至多一次并在scroll事件中只读滚动位置来判定是否处于底部const onScroll () { const distance scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight stickRef.current distance STICK_THRESHOLD_PX } const scrollTarget scroller document.scrollingElement ? window : scroller scrollTarget.addEventListener(scroll, onScroll, { passive: true })关键点在于源码第 60-79 行onScroll仅计算滚动距离并写入stickRef从不调用preventDefault()属于典型只读滚动状态场景因此{ passive: true }完全正确且必要它同时配合ResizeObserver与requestAnimationFrame做写入合并从监听端不阻塞与写入端不频繁两个方向共同保证滚动流畅在组件卸载的清理函数中同步removeEventListener与observer.disconnect()避免泄漏——这与规则示例中的 useEffect 清理模式一脉相承。从这个案例可以提炼出适用 passive 的判别特征事件处理器内部只做状态记录、UI 同步或数据上报不改变浏览器默认行为。深入原理passive 的底层机制与三个易错点1. 默认值并不总是 false规范层面addEventListener的passive默认值为false但浏览器对部分事件类型在部分目标上会覆盖默认值。例如 Chrome 自 56 起对window、document、body上注册的touchstart/touchmove默认启用 passive 语义。依赖这类隐式默认值并不可靠wheel事件与自定义目标上的行为因浏览器而异显式声明{ passive: true }才是跨浏览器一致的稳妥做法。2. passive 监听器中的 preventDefault() 会失效一旦监听器以passive: true注册在其中调用preventDefault()将被静默忽略并触发控制台警告。因此绝不能给需要拦截默认行为的监听器盲目加上 passive——这会让手势/缩放等自定义交互静默失效且难以排查。3. React 合成事件系统的隐含 passivePolar 前端基于 React/Next.js见 clients/apps/web 的 package.json。从 React 17 起的事件委托实现看React 在根容器上对touchstart、touchmove、wheel等事件以 passive 方式注册因此在这些合成事件处理器中调用preventDefault()不会生效。如果需要实现自定义滑动、缩放等必须阻止默认行为的交互应改用原生addEventListener并显式传入{ passive: false }而不能依赖onTouchMove等合成事件。兼容性注意老旧浏览器会把options对象当作useCapture布尔值处理对象转布尔恒为true导致监听器意外变成捕获阶段注册。现代浏览器已无此问题但若需兼容远古环境可先做特性检测再决定是否传 options。如何在 Polar 代码库中自查与落地该规则文件属于技能包 Section 4 Client-Side Data Fetching影响级别 MEDIUM-HIGH与同节的 client-event-listeners.md全局事件监听器去重 共同构成客户端事件处理规范完整编译版见 AGENTS.md规则文件结构与 impact 级别定义见 README.md。在 Polar 仓库中自查时可聚焦clients/apps/web/src下的addEventListener调用如 CompassHistoryMenu.tsx、appealCaseUnread.ts 等逐一确认监听touch、wheel、scroll类事件且不调用preventDefault()的是否都声明了{ passive: true }需要自定义手势/缩放、依赖preventDefault()的是否用原生监听并显式{ passive: false }而非依赖 React 合成事件每个 useEffect 中的addEventListener是否都有对应的removeEventListener清理事件处理器内是否有同步的昂贵计算——passive 只是消除等待监听器本身执行过慢仍会拖慢主线程。小结{ passive: true }是一个性价比极高的滚动性能优化一行声明即可向浏览器交出不会拦截滚动的承诺换取合成器线程的即时滚动。其适用边界非常清晰——纯观察型监听器埋点、日志、状态记录启用 passive需要拦截默认行为的手势与缩放交互必须关闭。Polar 仓库中的 useStickToBottom 正是只读滚动状态 passive 监听 rAF 写入合并的标准范式可作为新代码的参照模板。【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考