why-did-you-render 追踪规则:从配置到实操

发布时间:2026/11/3 2:20:19
why-did-you-render 追踪规则:从配置到实操 why-did-you-render 追踪规则从配置到实操【免费下载链接】why-did-you-renderwhy-did-you-render by Welldone Software monkey patches React to notify you about potentially avoidable re-renders. (Works with React Native as well.)项目地址: https://gitcode.com/gh_mirrors/wh/why-did-you-render控制台里一句「Re-rendered because of props changes」常常是 React 应用卡顿的唯一线索但要读懂它你得穿过一整条链路追踪开关怎么标在组件上、全局 options 有哪些字段、JSX 转换如何被接进补丁链、为什么值完全相等的 props 变化也会触发日志。这篇文章按 why-did-you-render 的源码把这条链路理成一条可执行的认知链读完后你能在自己的项目里完整配置这套追踪规则、看懂任意一行控制台输出并独立判断某次 re-render 是否本可避免。全景图追踪规则的每个模块都在哪对象职责边界越界后果src/whyDidYouRender.js主入口猴补丁 React 的createElement/createFactory/cloneElement接管 hooks不做 diff、不输出日志jsx-dev-runtime.jsautomatic JSX 转换的拦截入口包装jsxDEV不管生产形态jsx-runtime.js是纯透传src/shouldTrack.js「这个组件该不该被追踪」的唯一守门不关心 diff 结果src/normalizeOptions.js用户 options 补默认值不做任何类型校验src/patches/ 四个文件把类组件/函数组件/memo/forwardRef 四种形态包成可观测组件不改变 render 输出src/calculateDeepEqualDiffs.jsdiff 算法核心产出 8 种diffType不知道往哪输出src/defaultNotifier.js把 updateInfo 翻译成控制台分组不决定「该不该报」过滤在shouldLog依据 reasonsrc/wdyrStore.js单例状态袋原始函数备份、componentsMap/ownerDataMap两个 WeakMap不持有按 render 的临时数据随对象回收src/consts.jsdiffTypes枚举与描述文案、memo/forwardRef 的 Symbol—types.d.tsUpdateInfo/WhyDidYouRenderOptions的 TS 契约不约束运行时tests/、cypress/单测 21 个 E2E 场景的校验体系—流向是单向的jsxDEV被拦截 →getWDYRType查shouldTrack→ 命中则换成交由 src/patches/ 生成的包装组件 → 包装组件 render 时用useRef抓 prev/next →getUpdateInfo→ diff 算法 →notifier。锚点看 src/whyDidYouRender.js 的getWDYRType第 188 行与 src/shouldTrack.js 的返回链即可。why-did-you-render 配置硬规则按违规后果分组强约束直接抛错或无法运行规则include/exclude必须是RegExp[]不是字符串数组。依据src/shouldTrack.js 第 10/18 行直接调regex.test(displayName)。违规后TypeError被 src/whyDidYouRender.js 第 238 行catch吞掉控制台输出whyDidYouRender error in createElement. Please file a bug at ...且该元素退回原始createElement追踪静默失效。规则trackExtraHooks每项必须是[模块对象, hook名]二元组。依据src/whyDidYouRender.jstrackHooksIfNeeded第 162–163 行hookParent[hookName]。违规后拿到的「原 hook」是undefined组件第一次调用该 hook 时抛TypeError——且发生在你的组件 render 里堆栈指向组件而非配置。规则只能在 development 使用。依据README.md 的 CAUTION 区显著拖慢 React 猴补丁副作用库本身没有生产自动关闭逻辑if (process.env.NODE_ENV development)的包裹是你的职责。违规后生产包体积增加且每次 render 都跑 diff无报错。弱约束静默降级无报错规则importSource必须指向welldone-software/why-did-you-render且wdyr模块要最先执行。依据jsx-dev-runtime.js 第 11 行wdyrStore.React wdyrStore.React.__IS_WDYR__未 patch 时所有 JSX 走origJsxDev。违规后零日志、零报错这是最高频的「配置没生效」。规则props 里不能有循环引用。依据src/calculateDeepEqualDiffs.js 第 207–221 行专门 catch 栈溢出。违规后输出Warning: why-did-you-render couldnt handle circular references in props.该 diff 返回false这条线索丢失。规则hotReloadBufferMs依赖 bundler 的module.hot.addStatusHandler。依据src/defaultNotifier.js 第 181–192 行。违规后不支持的打包器里热重载抑制直接失效HMR 后刷屏。风格建议titleColor默认#058、diffNameColorblue、diffPathColorred、textBackgroundColorwhite、onlyLogs、collapseGroups——只改控制台外观见 src/normalizeOptions.js 第 35–40 行的默认值。逐个对象的格式解剖组件上的 whyDidYouRender 追踪开关1. 骨架class BigList extends React.Component { static whyDidYouRender {customName: BigList}; // 类组件 render() { /* ... */ } } const Pure props div/; Pure.whyDidYouRender true; // 函数组件等价可写对象形式2. 字段表whyDidYouRender静态属性的三种取值取值作用实现细节消费方代码路径true追踪该组件src/shouldTrack.js 第 40 行 truthy 判断getWDYRType→ 选择 src/patches/ 中对应包装false强制排除优先级仅次于exclude第 29 行在include之前判定getWDYRType返回null{customName}覆盖展示名src/whyDidYouRender.js 第 200–202 行优先于getDisplayNamedefaultNotifier的 group 标题{logOnDifferentValues}该组件强制记录全部 re-rendersrc/defaultNotifier.jsshouldLog第 20–22 行覆盖全局默认「只报可避免的」{trackHooks: false}该组件关闭 hook 追踪src/shouldTrack.js 第 33–37 行isHookChange分支trackHookChanges第 52 行3. 写作纪律好 vs 坏坏——SimpleComponent.whyDidYouRender true写在connect之前因为connecthoist statics开关会泄漏到 HOC 上控制台被复杂 hook 刷屏README.md Troubleshooting 第 383–396 行给了反例。好——在connect(...)(SimpleComponent)之后再挂开关。坏——依赖 HOC 自动生成的链式名字README 里那条 300 字符的withPropsOnChange(withStateHandlers(...示例。好——EnhancedMenu.whyDidYouRender {customName: Menu}demo 同款写法见 demo/src/bigList/index.js 第 10 行static whyDidYouRender {customName: BigList}。4. 独有校验tests/shouldTrack.test.js 的Dont track components with whyDidYouRenderfalsefalse压过include与Track included not tracked componentsinclude能拉起未标记组件tests/index.test.js 的dont swallow errors保证补丁链不会吞掉 React 自身错误。全局 optionswhyDidYouRender(React, options)1. 骨架// wdyr.js —— 应用入口的第一个 import import React from react; if (process.env.NODE_ENV development) { const whyDidYouRender require(welldone-software/why-did-you-render); whyDidYouRender(React, { trackAllPureComponents: true, include: [/^ConnectFunction/], // 可选正则数组 }); }2. 字段表默认值均出自 src/normalizeOptions.js字段默认值作用关联测试include/excludenull按 displayName 正则以正则批量纳管/排除tests/shouldTrack.test.jstrackAllPureComponentsfalse追踪所有PureComponent与React.memo同上trackAllPureComponents描述块trackHookstrue是否接管useState等内置 hookstests/hooks/ 全目录trackExtraHooks[]接管自定义 hook如useSelectortests/hooks/childrenUsingHookResults.test.jslogOwnerReasonstrue每个元素创建时记录 owner 数据供「谁让我重渲染」归因tests/logOwnerReasons.test.jslogOnDifferentValuesfalse值不同「正当」的 re-render 也报tests/logOnDifferentValues.test.jshotReloadBufferMs500HMR 后静默窗口tests/defaultNotifier.test.jsnotifierdefaultNotifier完全接管 updateInfo → 输出的最后一环types.d.ts 的Notifier类型getAdditionalOwnerDataundefined从 element 收割额外数据写入ownerDataMapsrc/whyDidYouRender.jsstoreOwnerData第 132 行3. 写作纪律坏——在index.tsx里无条件调whyDidYouRender(React)生产环境也背着猴补丁。好——包在NODE_ENV development判断里README Setup 的标准写法。坏——include: [BigList]字符串没有.test走进强约束第一条的吞错分支。好——include: [/^BigList/]。4. 独有校验tests/normalizeOptions.test.js 逐字段核对默认值notifier的契约以 types.d.ts 的UpdateInfo为准——自定义 notifier 拿到的reason含propsDifferences/stateDifferences/hookDifferences三组 diff。JSX 转换的 importSource 配置1. 骨架// babel.config.jsReact 19 只支持 automatic 转换 [babel/preset-react, { runtime: automatic, development: process.env.NODE_ENV development, importSource: welldone-software/why-did-you-render, }]React Native bare workflow 与 Expo 的等价写法在 README.md「React Native」两小节区别只在把该对象塞进babel/plugin-transform-react-jsx的env.development或babel-preset-expo的jsxImportSource。2. 字段表babel 插件参数 → 消费方参数作用实现细节消费方runtime: automaticJSX 编译为jsx-runtime模块调用不再经React.createElement字面量babel 插件development: true输出jsxDEV调用dev 形态是独立函数不经过createElement打包器运行时importSource把 JSX 导入指向本包解析到根目录 jsx-dev-runtime.jswebpack/metro/jest3. 写作纪律为什么 jsx-dev-runtime.js 要包装jsxDEV而 jsx-runtime.js 只有一行module.exports require(react/jsx-runtime)因为生产形态jsx()最终落到React.createElement——那个函数已被whyDidYouRender(React)换掉透传即可而 dev 形态jsxDEV是另一条实现路径必须显式拦截第 10–38 行复制了createElement补丁的全部逻辑换WDYRType、记ownerBeforeElementCreation、logOwnerReasons时storeOwnerData。坏——development写死true生产 bundle 里带着jsxDEV检查。好——development: process.env.NODE_ENV development。本地开发时 demo 用 nollup 的 alias 把包名直接映射到../src/index.jsdemo/nollup.config.js 第 16–20 行单测用 jest.config.js 的moduleNameMapper指向src/whyDidYouRender.js。4. 独有校验cypress/e2e/ 全部 19 个用例都以「真实打包 真实浏览器 控制台断言」验证这条链路等价于对 importSource 配置的端到端回归。hooks 追踪hooksConfig 与 trackExtraHooks1. 骨架src/whyDidYouRender.js 第 116–123 行原文export const hooksConfig { useState: {path: 0}, useReducer: {path: 0}, useContext: undefined, useSyncExternalStore: undefined, useMemo: {dependenciesPath: 1, dontReport: true}, useCallback: {dependenciesPath: 1, dontReport: true}, };2. 字段表配置项作用实现细节path: 0只比较 hook 返回值的第 0 位状态值本身跳过 settertrackHookChanges第 31 行get(rawHookResult, path)dependenciesPath: 1取依赖数组注册进dependenciesMap让「函数型 props 换了引用」能进一步比对 deps 是否真变了src/calculateDeepEqualDiffs.js 第 131–141 行dontReport: true不发独立 hook 变更通知useMemo/useCallback的变化通过「函数 deps 对比」间接体现避免重复报undefineduseContext 等整个返回值参与比较默认配置{}dontReport为假 → 正常追踪3. 写作纪律好——追踪useSelector的官方写法是trackExtraHooks: [[ReactRedux, useSelector]]README「Custom Hooks」坏——在 webpack 环境下对具名导出引用 hooktrackHooksIfNeeded要改写hookParent[hookName]而 webpack 的导出绑定可能是只读 getter报trackExtraHooks cannot set property——README Troubleshooting 记录了该问题的 workaround。4. 独有校验tests/hooks/ 按 hook 分文件useState.test.js、useReducer.test.js、useContext.test.js、useSyncExternalStore.test.jschildrenUsingHookResults.test.js专门覆盖「子组件 props 来自父组件 hook 结果」的归因E2E 侧对应 cypress/e2e/hooks-use-state.js 等 4 个场景。diffTypes「有多不同」的八种判定1. 骨架src/consts.js 第 1–9 行export const diffTypes { different: different, deepEquals: deepEquals, date: date, regex: regex, reactElement: reactElement, function: function, same: same, };2. 判定表条件均来自 src/calculateDeepEqualDiffs.jsdiffType判定条件控制台描述文案deepEquals对象/数组逐子值递归全等第 77、197 行different objects that are equal by valuedate两个 Date 且getTime()相等第 97–101 行different date objects with the same valueregex两个 RegExp 且toString()相等第 103–107 行different regular expressions with the same valuereactElement同 type 的 React 元素且 props 深等第 113–123 行每次都是新元素收到它的组件必然重渲染function同名函数、deps 无差异第 126–144 行different functions with the same namesame命中仅detailed模式记录第 46–51 行same objects by ref ()different以上都不满足的兜底第 203 行different objects3. 写作纪律坏——BigListPureComponent style{{width: 100%}}/每次 render 新建 style 对象memo 形同虚设好——把bigListStyle提为常量再传入。对照见 demo/src/bigList/index.js 第 23 行注释掉的常量与第 45 行的内联对象——同一 demo 里两种写法的差就是different与deepEquals的差也是默认 notifier 只报哪一种的差。4. 独有校验tests/calculateDeepEqualDiffs.test.js 与 tests/findObjectsDifferences.test.js 覆盖八种类型的边界含 Error 的stack键被排除、getter 被跳过cypress/e2e/big_list.js 断言console.group以BigList为标题恰好 1 次、且路径匹配/props.*style\W/。构建与校验流水线checkHealth 四连执行顺序由 package.json 的 scripts 固定buildrollup→linteslint--max-warnings 0→testjest→cypress:teststart-server-and-test start http://localhost:3003 cypress:run。checkHealth把四步串起来且version脚本在发版前先跑checkHealth——构建产物、静态检查、单测、E2E 全绿才允许出包。输入是 src/index.js输出是dist/whyDidYouRender.jsmain字段指向它。rollup 配置的关键片段input: src/index.js, external: [lodash, react], output: [{ name: whyDidYouRender, file: pkg.main, format: umd, sourcemap: true, exports: default, }], plugins: [ babel({exclude: node_modules/**, babelHelpers: bundled}), resolve(), commonjs(), license({sourcemap: true, banner}), ],external保证 lodash 与 react 不打进产物——猴补丁必须作用于用户项目里那份React。分阶段检查单条eslint--max-warnings 0warning 也当错误。单元jest每个测试文件beforeEach里whyDidYouRender(React, {notifier: updateInfo updateInfos.push(updateInfo)})afterEach里调React.__REVERT_WHY_DID_YOU_RENDER__()——这个还原函数定义在 src/whyDidYouRender.js 第 274–295 行恢复三个原函数、逐个还原 hooks凭.originalHook标记、清空componentsMap。跨场景cypress E2Enollup 起 demo带 HMRcypress/support/commands.js 的visitAndSpyConsole挂监听断言走 cypress/support/assertions.js 的calledWithMatches自定义 chai 方法。产物一致性files字段只放行dist、types.d.ts与四个 jsx-runtime 文件多写一个文件不会进包。检查项失败信息修复动作E2E 调用次数断言was expected to be called with X for N times but got M timesassertions.js 第 15–16 行模板手动打开对应 demo核对控制台 group 标题与 diff 路径是否匹配用例预期还原不干净相邻用例互相污染失败信息指向上一用例的 console 输出检查afterEach是否漏掉__REVERT_WHY_DID_YOU_RENDER__打包体积/依赖泄漏rollup 把 react 打进 bundle 的告警核对external: [lodash, react]lint任意 warning 即失败--max-warnings 0下逐条清零不要用eslint-disable掩盖运行时行为从 JSX 拦截到控制台一行匹配优先级链src/shouldTrack.js 从上到下短路exclude命中 → 否whyDidYouRender false→ 否仅 hook 场景trackHooks: false→ 否whyDidYouRendertruthy /trackAllPureComponents且是纯组件 /include命中 → 是。注意include排最末exclude排最前。同一条事实驱动整条日志的因果链——以 demo/src/bigList/index.js 的内联 style 为例父组件setState后子组件重渲染jsxDEV拦截getWDYRType把BigList换成包装组件jsx-dev-runtime.js 第 15–19 行包装组件 render 时useRef拿到上一次的 propssrc/patches/patchFunctionalOrStrComponent.js 第 17–19 行组装 updateInfofindObjectsDifferences浅层逐键比较style子树递归全等 → 记deepEqualsshouldLog检查hasDifferentValues没有different类型 → 判定「本可避免」→ 输出控制台依次出现Re-rendered because of props changes:→props.style分组 →different objects that are equal by value.→ 修复提示「This could have been avoided by making the component pure, or by preventing its father from re-rendering.」src/defaultNotifier.js 第 64–68 行。自动记录与抑制策略对应「什么时候不用你主动查」logOwnerReasons默认true每次createElement/cloneElement调storeOwnerData把 owner 的 props/state/hooks 快照写进ownerDataMapWeakMap随 fiber 回收无手动清理defaultNotifier 发现子组件有 props diff 且 owner 快照可比对时追加一层「Rendered by Owner」嵌套分组连 owner 的 hook 变化都逐条列出第 120–166 行。热重载抑制createDefaultNotifier(hotReloadBufferMs)注册module.hot.addStatusHandler状态变idle后置inHotReload true持续 500ms期间shouldLog直接返回false——HMR 触发的整树重渲染不算你的问题。Strict mode类组件在StrictMode下 render 两次patchClassComponent用renderNumber % 2 1跳过奇数次src/patches/patchClassComponent.js 第 36–37 行owner 归因侧则把 prev 的hooksInfo减半切片对齐src/getUpdateInfo.js 第 17–20 行。tests/strictMode.test.js 全目录验证「双 render 只报一次」。用户可见的最终效果默认 notifier 输出三层信息——组件名分组标题customName或推导名、每条 diff 的「类型描述 prev/next 值」、可避免时的修复提示。完整 owner 归因样例 动手清单给一个组件加追踪并验证9 步场景给项目里的HeavyTable组件接上追踪并验证日志链路完整。装依赖npm install welldone-software/why-did-you-render --save-dev。验证peerDependencies要求react ^19与项目 React 版本对齐package.json 第 63–65 行。指 importSourcebabel 配置加[babel/preset-react, {runtime: automatic, development: process.env.NODE_ENV development, importSource: welldone-software/why-did-you-render}]。验证grep -n importSource babel.config.js恰好一行且development不是写死的true。建 wdyr.js 并作为第一个 import入口第一行import ./wdyr甚至排在react-hot-loader之前README Setup 的示例顺序。验证入口文件里./wdyr早于任何会触发 render 的 import。打开组件开关HeavyTable.whyDidYouRender true;HOC 包裹的组件放在最外层之后。验证grep -rn whyDidYouRender src/命中目标组件。制造一次「本可避免」的重渲染父组件传内联对象 propstyle{{...}}并setState。预期控制台出现Re-rendered because of props changesdifferent objects that are equal by value。确认 owner 归因默认已开启预期出现Rendered by Owner分组样式见上图。没有 → 检查createElement是否被换掉步骤 2。临时打开全量模式options 加logOnDifferentValues: true验证「值不同」的正当重渲染也报验证完删掉恢复默认「只报可避免」语义tests/logOnDifferentValues.test.js 就是这条行为的单测。跑单测回归yarn test。预期全绿。若dont swallow errorstests/index.test.js挂 → 补丁链吞了 React 自身错误回到步骤 3 查 import 顺序与 wdyr 版本。跑 E2E 终验yarness cypress:testdemo 起在 3003 端口后cypress run。预期 19 个用例通过。失败定位calledWithMatches报for 1 times but got 0→ 步骤 2/3 的拦截链没生效报got 2→ Strict mode 或 HMR 时机问题查步骤 4/5 的场景写法。 排错速查从「没输出」到断言失败报错片段原文根因一句话修法一句话控制台无任何 WDYR 输出__IS_WDYR__未置位或importSource未生效jsx-dev-runtime.js 第 11 行按清单步骤 2/3 重配 babel 并确认 wdyr 是第一 importwhyDidYouRender error in createElement. Please file a bug at ...补丁链内异常被createElement的 catch 吞掉src/whyDidYouRender.js 第 238–246 行常见为 options 类型错对照错误载荷里的options字段修类型RegExp[]别写成字符串whyDidYouRender JSX transform error. Please file a bug at ...jsxDEV包装分支抛错jsx-dev-runtime.js 第 25 行检查该次渲染的元素 type 是不是合法组件Warning: why-did-you-render couldnt handle circular references in props.props 存在循环引用diff 中止src/calculateDeepEqualDiffs.js 第 219 行去掉 props 里的自引用结构或换成非循环容器connect 组件刷屏控制台connecthoist statics 把内层开关带到 HOCREADME Troubleshooting开关挪到connect(...)之后挂载trackExtraHooks cannot set propertywebpack 下改写具名导出失败README 记录的已知问题用 README Troubleshooting 给的 workaround 换 hook 引用方式HMR 后日志暴增打包器不支持module.hot.addStatusHandler抑制未启用src/defaultNotifier.js换支持 add-status-handler 的工具链或调大hotReloadBufferMsStrict mode 下日志次数与直觉不符类组件双 render 被 【免费下载链接】why-did-you-renderwhy-did-you-render by Welldone Software monkey patches React to notify you about potentially avoidable re-renders. (Works with React Native as well.)项目地址: https://gitcode.com/gh_mirrors/wh/why-did-you-render创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询