why-did-you-render — 让 React 告诉你这次渲染到底为什么
已复核why-did-you-render(WDYR)是一个 只应在开发时启用 的 React 诊断库。它 monkey-patch React.createElement / cloneElement 和若干 hook,在被跟踪组件再次渲染时比较 prev/next 的 props、state 和 hook 结果,并把“值等价但引用变了”打到 console。
日常类比:React 默认是不解释的收银员,同一瓶水再扫一次也不会说话;WDYR 是会探头问“这瓶刚才结过了,你确定要再扫吗”的老员工。它不是 React DevTools Profiler 的替代品——Profiler 回答“谁慢”,WDYR 回答“谁可能白渲染了”。
固定 10.0.1 的 peerDependencies 是 react@^19。README 写明:未测试 React Compiler,并相信 完全不兼容;生产环境绝不能用,因为会显著拖慢 React,且 patch 公共 API。
不理解这套 patch 合同,下面这些事会按旧印象写错:
- 为什么
<Child style={{width: 100}} />能让React.memo失效,而 WDYR 会把它标成deepEquals - 为什么 React 19 的 automatic JSX 需要
importSource,而jsx-runtime.js本身并不包一层 WDYR - 为什么默认 notifier 不报“props 真的变了”的渲染,除非打开
logOnDifferentValues - 为什么调用两次
whyDidYouRender(React)不会叠两层 patch
固定 10.0.1 的工作可以拆成三步:
-
挂旁路:
whyDidYouRender(React, options)若看到React.__IS_WDYR__直接返回。否则保存原createElement/createFactory/cloneElement,换成会调用getWDYRType()的包装。__REVERT_WHY_DID_YOU_RENDER__能把方法和 hook 还原。 -
决定跟不跟踪:
shouldTrack()看Component.whyDidYouRender、include/exclude正则,以及trackAllPureComponents(只覆盖PureComponent和React.memo)。默认trackHooks: true,会包装useState/useReducer/useContext/useSyncExternalStore;useMemo/useCallback只把 deps 记进 WeakMap,不当 hook 变更上报。 -
结构化 diff:
getUpdateInfo()产出propsDifferences/stateDifferences/hookDifferences/ownerDifferences。顶层对象按 key 浅看,每个 value 再走calculateDeepEqualDiffs。函数先比name;同名且来自被跟踪 hook 时再深比 deps。默认 notifier 只在 没有diffType === 'different'时打日志,也就是专抓“引用变了、值没变”。
class 组件用 renderNumber % 2 === 1 跳过 StrictMode 的奇数次渲染;functional wrapper 没有这条短路,每次后续 render 都比较。
案例 1:React 19 的开发态接入
Section titled “案例 1:React 19 的开发态接入”README 要求 preset-react 走 automatic runtime,并且 development 才把 importSource 指到本包:
['@babel/preset-react', { runtime: 'automatic', development: process.env.NODE_ENV === 'development', importSource: '@welldone-software/why-did-you-render',}]入口仍要 最先 调用 whyDidYouRender(React),否则 wdyrStore.React 为空,jsxDEV wrapper 会原样落到 React:
import React from 'react';if (process.env.NODE_ENV === 'development') { const whyDidYouRender = require('@welldone-software/why-did-you-render'); whyDidYouRender(React, { trackAllPureComponents: true });}jsx-dev-runtime.js 才替换 jsxDEV。同版本的 jsx-runtime.js 只是 require('react/jsx-runtime'),生产/非 dev transform 不会自动获得跟踪。
案例 2:抓一次“值相等、引用不等”
Section titled “案例 2:抓一次“值相等、引用不等””const Child = React.memo(function Child({ style }) { return <div style={style}>hi</div>;});Child.whyDidYouRender = true;
function App() { const [n, setN] = React.useState(0); return ( <> <button onClick={() => setN(n + 1)}>{n}</button> <Child style={{ width: 100 }} /> </> );}父组件每次 setN 都新建 { width: 100 }。WDYR 的 deep diff 应把它标成 deepEquals(“different objects that are equal by value”)。本文未在浏览器里跑这个例子。
案例 3:修引用后再看 notifier 是否安静
Section titled “案例 3:修引用后再看 notifier 是否安静”const STYLE = { width: 100 };<Child style={STYLE} />顶层 findObjectsDifferences 先做 prev === next。引用不变则整段 props diff 为 false,默认 notifier 不再为这次 Child 开 group。这是诊断 → 改引用 → 再看 console 的闭环,不是 WDYR 替你 memo。
-
把
jsx-runtime.js当成第二套 patch:10.0.1 里它不包jsx()。只配 production runtime、或忘了先调用whyDidYouRender(React),都会静默无日志。 -
多份 React 只 patch 到你传入的那一份:monorepo / microfrontend 若解析出两份
react,另一份的createElement仍是哑巴。__IS_WDYR__也只打在传入对象上。 -
匿名函数永远像“等价但新引用”:函数 diff 比的是
name。inline arrow 的name常是空字符串,两次都会走diffTypes.function。useMemo/useCallback只有在 hook wrapper 把结果放进dependenciesMap后才会比 deps。 -
trackAllPureComponents会给每个被跟踪 function 多两个useRef:patchFunctionalOrStrComponent用它们存 prev props 和 prev owner。大应用全开会改变 hook 数量和耗时;README 也警告不要把它当通用性能优化器。 -
默认 notifier 会吞“真的变了”:
shouldLog()看到任何different就返回 false。想记录合法更新必须设logOnDifferentValues。热更新后hotReloadBufferMs默认 500ms 内也会静音。 -
React Compiler / 生产构建:README 两段 CAUTION 写死。Compiler 自动 memo 之后,这套 createElement patch 没有合同;生产引入会同时付出性能和正确性风险。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 追查某个
memo/PureComponent为什么还在更新 - 用
trackExtraHooks看 React-ReduxuseSelector这类自定义 hook 的结果引用 - 给新人演示 inline object / inline function 怎样打穿浅比较
不适用:
- 找“哪个组件最耗时”——那是 Profiler / flame graph
- 已经全量 React Compiler,并且接受 README 的不兼容声明
- 生产监控或 RUM——这是 dev-only monkey-patch
- 期望工具自动修好引用——它只产出
updateInfo,不改你的组件
固定版本边界
Section titled “固定版本边界”- 本文绑定
welldone-software/why-did-you-render@752cfdb4...,即 GitHub annotated tagv10.0.1剥出的 commit;package.json为10.0.1。 - npm
10.0.1的gitHead是5623596ee8833f8352c6bf7a713619a1bcd57c6c(tag 之后的 badge 提交);master后来还有3ec3512d...,版本号仍写 10.0.1。本文不把后两份当成同一 provenance。 - 运行时依赖
lodash@^4;类型和dist/随 npm 包发布,本 review 读的是src/。 - 本文未安装依赖、未跑 Jest/Cypress、未在浏览器挂载 React 19,状态保持
UNVERIFIED。
- 没有官方钩子时,幂等哨兵和 revert 是最低安全网——
__IS_WDYR__与__REVERT_*决定能不能安全地补丁公共 API。 - 诊断默认要偏向假阳性里的“白渲染”——默认 notifier 故意忽略值真的变了的更新。
- JSX transform 入口和
whyDidYouRender()必须成对——只改 babelimportSource或只 patchReact.createElement,在 React 19 都会漏一侧。 - 函数相等是工程近似——比
name和 hook deps,不比toString()。
- 第二次调用
whyDidYouRender(React)会再包一层createElement吗? - 默认 notifier 会不会为
prev={a:1}→next={a:2}打日志? - 只引入
jsx-runtime.js、不调用whyDidYouRender(React),automatic production runtime 会被跟踪吗?
检查点:
- 不会。已有
React.__IS_WDYR__时函数直接 return。 - 不会。顶层值变化是
different,shouldLog为 false,除非logOnDifferentValues。 - 不会。
jsx-runtime.js原样重导出 React;跟踪发生在jsxDEV包装和已初始化的wdyrStore。
- 固定源码:welldone-software/why-did-you-render —— 本文绑定提交
752cfdb4d5c5eba5a8774fb19a978a7ac0a0d5de - Vitali 的 v1.0 动机文
- react ——
createElement/ memo / hook 的公共合同 - react-compiler —— README 声明与 WDYR 不兼容的自动 memo
- React DevTools Profiler —— 看耗时用它
- react —— 全部 monkey-patch 都建立在 React 公共对象上
- react-compiler —— 编译期 memo,和运行时 patch 互斥
- use-deep-compare-effect —— 用深比回避引用抖动;WDYR 负责把抖动显示出来
- eslint-plugin-react-hooks —— 管 deps 数组写没写全,不管 inline object prop
- react-devtools —— 看 commit 时序和耗时
- turbopack —— 同样把重代价留在 development
- testing-library —— Testing Library — 像用户一样测前端,重构不再挂测试