移动开发生命周期¶
连接运行时的 HMR 边界 保持稳定的 atom 运行时,并通过可写 atom 替换其 Effect 层。只有在安装新层之后才接受更新。否则,即使 Metro 报告刷新成功,导入方仍会保留旧行为。
不要通过重置共享 atom 注册表来刷新连接运行时的编辑。重置会移除已挂载消费者上的监听器,而 Metro 没有理由重新渲染这些消费者。当注册表或托管运行时模块本身发生变化时,正常的 Metro 传播会处理掉其旧资源。此边界并不会让任意的模块级 atom 族可以安全地热替换。生产环境使用普通的 atom 运行时。
环境监督作用域 是注册表作用域的子级。按环境划分的映射支持定向关闭,但在其清理运行之后创建的监督器会逃逸出该作用域。已关闭的父作用域也会关闭迟到的对象,从而防止中断的启动或运行时替换让 WebSocket 在新注册表之外继续存活。
Uniwind 在 Metro 更新时编译 CSS,从而发现新使用的类。只有当生成的样式表和主题列表未变化时,它才跳过全局样式失效。跳过编译会丢失新类;而为未变化的输出让每个消费者失效,则会使一次普通组件编辑刷新整个应用。指纹仅在初始化成功后记录。
位于 apps/mobile/modules/ 下的原生模块是 file: 依赖项,pnpm 会将这些模块复制到其虚拟存储中,而不是链接它们。Metro 打包的是副本,因此对某个模块 TypeScript 的编辑对于正在运行的开发客户端是不可见的,直到 vp i 重新同步;而 Gradle 和 CocoaPods 则直接编译工作树目录。设备上“没有效果”的 JavaScript 更改通常就是这个原因。
测量渲染¶
render-counter.ts 统计 React 在每次 commit 中渲染的组件及其原因:状态槽变化、props 变化或 context 变化。它包装了来自 index.ts 首次导入的 DevTools hook,并且仅在 __DEV__ 和 EXPO_PUBLIC_T3_RENDER_COUNTER=1 时加载,因此发布包永远不会包含它。遍历会跳过 React 已跳出的子树。一个在每个 fiber 上检查 flags & 1 的探针会报告从未发生的渲染,因为已跳出的 fiber 保留了旧 commit 的 flags。
measure-renders.mts 从应用外部驱动一次测量。它通过 Metro 的 Hermes inspector 代理来评估计数器的全局变量,这种方式对 iOS 模拟器和 Android 模拟器同样有效,并且不需要点击、simctl 或 adb。它通过服务器启动一次真实的 provider 轮次,用应用链接打开会话,在流中记录一段窗口,然后打印渲染次数、commit 次数、主要组件和主要原因的中位数。Expo 会丢弃其 Origin 不是 Metro 所通告 URL 的 inspector socket,因此请将该 URL(使用 --lan 时的局域网地址)作为 --metro 传入。
EXPO_PUBLIC_T3_RENDER_COUNTER=1 vp run dev:client # in apps/mobile, then pair the app
node apps/server/src/bin.ts auth session issue --base-dir <base-dir> --token-only
node apps/mobile/scripts/measure-renders.mts --server <server-origin> --token <token> \
--metro http://<metro-lan-host>:8081 --project-root <scratch-dir>
使用 --route /settings/usage 时,则会重新加载应用,从 Home 打开该路径,并在挂载和数据稳定期间记录一个 6 秒窗口,不需要 provider 轮次,也不需要 --server、--token 或 --project-root。如果同时传入 --server,应用会重新加载并打开该路径(/ 保持在 Home),而回复会流入一个应用从不显示的新会话,这可以衡量其他地方的流式输出对当前屏幕造成的开销。
在同一基础(base)、同一设备上比较前后结果,并取三次运行的中位数。Provider 的输出每次运行都会不同,因此单次运行或跨基础的比较都是噪声。
React Compiler¶
Metro 之所以运行编译器,只是因为 app.config.ts 中设置了 experiments.reactCompiler。Expo 随后将 supportsReactCompiler 传给 babel-preset-expo,后者拥有插件选项;没有 babel.config.js 条目可供查找。发布包使用 panicThreshold: "NONE",因此编译器无法证明安全的函数会在没有构建错误的情况下以未 memo 化的形式发布。
reactCompilerCoverage.test.ts 会对每个源文件运行同一套 preset。它会在出现新的 bailout 时失败,也会在 KNOWN_BAILOUTS 中的某个文件现在可以编译时失败,因此这个列表只会缩小。当你修复某个列出的文件时,请在同一个变更中删除它的条目及其手动添加的 useMemo/useCallback/memo。大多数移动端 bailout 是 try 没有 catch、或有 finally,以及在渲染期间读取 refs;常见改写方式请参阅 Web UI。移动端以 allowMemoHooks 运行 t3code/no-manual-memo,因此 memo 指令会报错,但在 KNOWN_BAILOUTS 清空之前,这些 hooks 仍然合法。t3code/no-hookless-use-function 会拒绝一个不调用任何 hook 的 use* 函数,因为编译器会使其不被缓存。真实 hook 的平台桩(例如 .android.ts 文件)会保留 hook 名称,并附上原因以抑制该规则。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code