
Sanity 仓库中的 React 性能实践用 Passive 事件监听器消除滚动延迟【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity本文聚焦 Sanity 仓库内收录的一条 Vercel React 性能规则——为触摸与滚轮事件监听器声明{ passive: true }从原理、正反代码示例到适用边界逐项讲透并结合 CommandList、PreviewTooltip、ToolSVG 三处真实源码展示该规则在大型 React 应用中的落地方式与反例。读完后你能掌握为什么浏览器默认会“等一等”你的监听器再滚动、如何在useEffect中正确声明被动监听器、以及何时必须反过来使用{ passive: false }。规则定位Vercel React 性能规则集的一部分client-passive-event-listeners.md 是 Sanity 仓库中「Vercel React 最佳实践」技能包skill中的一条规则文件与仓库根目录下 skills/vercel-react-best-practices 中同名文件对应。该技能包收录了 57 条按影响级别排序的性能规则覆盖异步瀑布、包体积、服务端、客户端数据获取、重渲染、渲染性能、JS 性能与高级模式八个类别完整索引见 SKILL.md。本规则在其中的定位如下直接来自规则文件的 frontmatter属性值含义titleUse Passive Event Listeners for Scrolling Performance为滚动性能使用被动事件监听器impactMEDIUM属于第 4 优先级「Client-Side Data Fetching」类别中的中等影响规则impactDescriptioneliminates scroll delay caused by event listeners消除由事件监听器引起的滚动延迟tagsclient, event-listeners, scrolling, performance, touch, wheel作用于客户端的 touch / wheel 事件处理也就是说这是一条客户端client- 前缀类别的性能规则目标是消除触摸与滚轮事件监听器造成的滚动延迟。原理浏览器为什么要“等”你的监听器执行完规则原文给出的核心解释是浏览器默认会等待touchstart/wheel等事件的监听器执行完毕以检查其中是否调用了preventDefault()在此期间浏览器无法立即处理滚动从而造成可感知的滚动延迟。其背后的机制可以理解为触摸与滚轮类事件touchstart、touchmove、wheel属于浏览器可以取消cancelable的事件——preventDefault()能阻止默认的页面滚动/缩放行为。由于存在这种“可取消”语义浏览器在派发事件后会阻塞滚动处理直到所有非被动的监听器执行完才能确定是否应该滚动。监听器越重比如做埋点上报、日志、状态计算滚动卡顿就越明显。{ passive: true }正是用来打破这一等待的它向浏览器作出契约——“该监听器绝不会调用preventDefault()”于是浏览器不再等待监听器完成可以立即执行滚动。正反示例从document级监听看正确写法以下两个示例完整继承自规则文件展示典型的useEffect注册/注销模式。反例未声明 passive 的 touch / wheel 监听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浏览器仍会等待它们执行完毕再滚动。正例声明{ 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) } }, [])两个版本唯一的差别就是第三个参数{ passive: true }。注意 cleanup 函数中removeEventListener的匹配逻辑对于passive这类只影响浏览器调度语义的选项移除监听时并不需要再次传入{ passive: true }按监听器函数与事件名匹配即可这一点在 Sanity 源码中也是通行写法见下文。判断标准什么时候该用什么时候不能规则文件给出了简洁明确的判定口径这也是本条规则最有实操价值的部分应当声明 passive 的场景埋点 / 行为分析tracking / analytics日志记录logging一切不会调用preventDefault()的监听器一句话口诀监听器只做“读”不做“阻”就声明 passive。不要声明 passive应保持默认或显式{ passive: false }的场景自定义滑动手势custom swipe gestures自定义缩放手势custom zoom controls一切需要调用preventDefault()的监听器从源码结构看这个判定标准与浏览器行为完全吻合passive: true的监听器中调用preventDefault()会被浏览器忽略并在控制台产生告警。因此“是否会调用preventDefault()”是唯一的分水岭——声明了 passive 却需要阻止默认行为等于同时丢失了性能收益和手势语义。Sanity Studio 源码中的三个真实案例以下案例均来自 Sanity Studio 核心包源码可作为该规则在真实大型 React 应用中的落地参照。案例一CommandList 的滚轮监听——纯状态读取声明 passiveCommandList 是命令列表组件需要在虚拟列表上监听鼠标/滚轮事件来重新启用子容器的指针事件useEffect(() { function handleMouseEvent() { enableChildContainerPointerEvents(true) } virtualListElement?.addEventListener(mousemove, handleMouseEvent) virtualListElement?.addEventListener(wheel, handleMouseEvent, {passive: true}) return () { virtualListElement?.removeEventListener(mousemove, handleMouseEvent) virtualListElement?.removeEventListener(wheel, handleMouseEvent) } }, [enableChildContainerPointerEvents, virtualListElement])该监听器仅调用enableChildContainerPointerEvents(true)切换一个布尔状态不涉及任何preventDefault()因此按规则口径正确声明了{ passive: true }保证滚轮滚动命令列表时不被监听器拖慢。案例二PreviewTooltip 的 capture 阶段滚动监听——passive 与 capture 可同时声明PreviewTooltip 在悬浮预览提示框悬停期间监听滚动事件用于在滚动时暂停suspend提示框const handleScroll () setSuspended(true) // Capture phase, since scroll events dont bubble from nested containers. window.addEventListener(scroll, handleScroll, {capture: true, passive: true})这个案例额外传递了两个信息监听器回调只更新一个布尔状态无preventDefault()需求因此安全声明passive: truepassive与capture是相互正交的选项可以同时声明。源码注释也解释了为何要用捕获阶段滚动事件不会从嵌套容器冒泡因此需在window上以捕获方式监听。案例三ToolSVG 的触摸拖动——必须用{ passive: false }的反例ToolSVG 是图片工具的裁剪/热点 SVG 画布其触摸移动监听刻意声明为非被动const handleTouchMove (e: TouchEvent) { // Prevent iOS scrolling page while dragging the element e.preventDefault() return undefined } svgElement.addEventListener(touchmove, handleTouchMove, {passive: false})注释说明了动机拖动裁切框时若允许 iOS 页面随之滚动交互会被打断因此必须调用e.preventDefault()阻止默认滚动——这正是规则中“Dont use passive when”一栏列出的“自定义手势”场景。对照来看同一个组件/同一个团队在不同监听器上对 passive 的取舍完全由“是否要阻止默认行为”决定两条源码互相印证了规则的判断标准。在 Sanity 仓库中查找这条规则的所有落点如果想系统排查项目中是否存在“未声明 passive 的 touch / wheel 监听”可以按下面的方式检索本文引用的所有案例即由此发现用addEventListener配合事件名定位注册点addEventListener\([]wheel[] addEventListener\([]touchmove[]用passive: true/passive: false反向核对已显式声明的监听器确认每个 touch/wheel 监听都有明确的 passive 决策而不是依赖浏览器默认值。在 Sanity 核心包中显式声明{ passive: true }的滚动类监听还包括 useScrollIndicatorVisibility导航栏滚动指示、scrollContainer滚动容器等组件显式声明{ passive: false }的手势类监听则集中在图片工具与虚拟列表的拖拽逻辑中。可以推断仓库中“滚动/触摸相关监听要么显式 passive、要么显式 non-passive”的写法是有意保持的约定。补充说明passive 与事件默认行为的细节结合本规则的适用前提还有三点值得注意适用前提规则针对的是通过原生addEventListener手动注册的 touch / wheel 类监听。React 合成事件的 passive 行为由 React 运行时统一管理不在这条规则的直接管辖范围内仓库中所有案例均为原生监听。scroll事件规则文本聚焦touchstart/touchmove/wheel但 PreviewTooltip 对scroll同样显式声明了{ passive: true }。从源码结构看这是一种防御性写法——无论浏览器对scroll事件的默认 passive 策略如何显式声明能消除实现差异带来的不确定性。与去重规则配合使用passive 解决的是“单个监听器拖慢滚动”的问题当 N 个组件各自注册同一个全局监听器时还需要配合同技能包中的 client-event-listeners 规则做监听器去重例如用useSWRSubscription将 N 个实例的监听收敛为 1 个。两者分别优化监听器的“执行成本”与“数量”可以叠加收益。实践清单在评审或编写 React 代码中的触摸/滚轮监听时可按以下清单自检该监听器是否会调用preventDefault()会 → 不得声明 passive必要时显式{ passive: false }如自定义滑动手势、缩放控制参照 ToolSVG。不会埋点、日志、状态读取等→ 声明{ passive: true }参照 CommandList、PreviewTooltip。事件类型是否为touchstart/touchmove/wheel含scroll等可被默认行为消费的事件是否在useEffect的 cleanup 中正确removeEventListener避免组件卸载后监听器残留多个组件是否共享同一全局事件如是结合 client-event-listeners 规则做去重避免“N 个实例 N 个监听器”。掌握这条规则后你在 Sanity 或任何 React 项目中处理触摸、滚轮与滚动性能问题时都能做出正确的 passive 决策既不因冗余的浏览器等待损失滚动流畅度也不误伤依赖preventDefault()的自定义手势。【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考