React Scan 接入 Remix 全指南:Script Tag、模块导入与生产环境检测
【免费下载链接】react-scanScan and fix React performance issues项目地址: https://gitcode.com/GitHub_Trending/re/react-scan
导读
本文基于 React Scan 官方安装文档,完整讲解如何在 Remix 应用中接入 React Scan 性能扫描工具。你将掌握两种官方接入方式(<script>标签与模块导入)、如何在app/root与app/entry.client中选择正确的挂载位置、如何通过react-scan/all-environments让扫描在生产环境生效,以及两个必须遵守的硬性前提(React 19 与"先于 React 加载"的导入顺序)。文章结合仓库源码,解释这些配置背后的实现原理,让接入过程不再只是"复制粘贴"。
为什么需要专门的 Remix 接入指南
Remix 采用服务端渲染(SSR)与客户端水合(hydration)架构,应用入口被拆分为app/root(根布局)、app/entry.client(客户端入口)等多个文件。React Scan 的接入方式因此与普通 Vite/Next.js 项目不同:它必须被注入到正确的生命周期节点,且必须保证在 React 及其渲染器(如 React DOM)加载之前完成对 React DevTools 通道的接管。
从源码看,React Scan 的核心逻辑在 packages/scan/src/core/index.ts,而自动注入入口在 packages/scan/src/auto.ts——后者在客户端环境下直接调用scan()并把window.reactScan暴露到全局:
if (IS_CLIENT) { scan(); window.reactScan = scan; }因此,无论采用哪种接入方式,核心目标只有一个:让 React Scan 的扫描逻辑先于 React 执行。
前提条件:React 19
官方文档在两种接入方式下都标注了同一条警告:
[!CAUTION] 此方式仅支持 React 19(This only works for React 19)。
React Scan 通过劫持 React 内部的 DevTools 钩子来获取渲染信息(依赖bippy库的副作用安装钩子,见 packages/scan/src/index.ts 中的import 'bippy')。因此在 Remix 项目中使用前,请确认你的react/react-dom版本为 19.x。仓库中react-scan包的 peerDependencies 声明为^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0(见 packages/scan/package.json),但 Remix 接入方式本身对 React 版本有硬性要求。
方式一:Script 标签(CDN)
如果不想改动构建流程,可以在根布局的<head>中直接注入 CDN 脚本。
操作步骤
在app/root的Layout组件中添加<script>标签,且必须放在其他任何脚本之前:
// app/root.jsx import { Links, Meta, Scripts, ScrollRestoration, } from "@remix-run/react"; export function Layout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> {/* Must run before any of your scripts */} <script src="https://unpkg.com/react-scan/dist/auto.global.js" /> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> {children} <ScrollRestoration /> <Scripts /> </body> </html> ); } // ...关键点:注释明确强调Must run before any of your scripts(必须运行在你的任何脚本之前)。由于<script>位于<head>顶部且先于<Meta />、<Links />输出,可以保证在 Remix 的客户端脚本执行前,React Scan 的全局钩子已安装完毕。
可用的 CDN 地址
官方文档中该方式引用了 CDN 指南,其中列出了两个等效的 CDN 地址:
# JSDelivr https://cdn.jsdelivr.net/npm/react-scan/dist/auto.global.js # UNPKG https://unpkg.com/react-scan/dist/auto.global.jsauto.global.js是构建产物中面向浏览器的自动执行版本。从源码 packages/scan/src/auto.ts 可以看出,它内部会:
- 引入 polyfills;
- 通过
import 'bippy'安装 React DevTools 钩子(副作用导入); - 在客户端环境下自动执行
scan(),并把scan暴露到window.reactScan。
也就是说,CDN 方案完全不需要写任何初始化代码,加载即扫描。
方式二:模块导入(npm 包)
如果项目已安装react-scan依赖,推荐使用模块导入方式,可以更精细地控制扫描启停。
在app/root中启用
官方推荐做法是在app/root中导入scan并在水合完成后调用:
// app/root.jsx import { scan } from "react-scan"; // Must be imported before Remix import { Links, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; export function Layout({ children }) { useEffect(() => { // Make sure to run React Scan after hydration scan({ enabled: true, }); }, []); return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> {children} <ScrollRestoration /> <Scripts /> </body> </html> ); } export default function App() { return <Outlet />; }要点分析:
- 导入顺序:
import { scan } from "react-scan"必须出现在@remix-run/react的导入之前。官方警告如下:
[!CAUTION] React Scan 必须在整个项目中先于 React(以及 React DOM 等其他 React 渲染器)和 Remix 被导入,因为它需要在 React 访问 React DevTools 之前将其劫持。
- 调用时机:
scan()放在useEffect中,确保水合完成后才开始扫描,避免与 Remix 的首屏渲染竞争。
参数说明
scan(options)是命令式 API(声明见 packages/scan/README.md 的 API Reference 部分)。文档示例中使用的enabled: true只是其中一个选项,完整Options接口定义在 packages/scan/README.md 中,常用项包括:
| 选项 | 默认值 | 说明 |
|---|---|---|
enabled | true | 是否启用扫描,官方推荐写法为process.env.NODE_ENV === 'development' |
dangerouslyForceRunInProduction | false | 强制在生产环境运行(不推荐) |
log | false | 在控制台打印渲染日志(高频渲染时开销较大) |
showToolbar | true | 是否显示工具栏 |
animationSpeed | "fast" | 高亮动画速度:"slow" \| "fast" \| "off" |
trackUnnecessaryRenders | false | 跟踪不必要的渲染并用灰色轮廓标记(有额外开销) |
onCommitStart/onRender/onCommitFinish | — | 提交生命周期回调 |
onPaintStart/onPaintFinish | — | 轮廓绘制回调 |
在开发环境中,更推荐写成enabled: process.env.NODE_ENV === 'development',这样生产构建会自动关闭扫描。
生产环境:切换到react-scan/all-environments
默认情况下,react-scan主入口在生产环境会静默退出。若希望 React Scan 在生产环境同样运行,需要改用react-scan/all-environments导入路径:
- import { scan } from "react-scan"; + import { scan } from "react-scan/all-environments";从源码 packages/scan/src/core/all-environments.ts 可以看到该入口的实现:
export const scan = /*#__PURE__*/ (...params: Parameters<typeof innerScan>) => { if (typeof window !== 'undefined') { ReactScanInternals.runInAllEnvironments = true; innerScan(...params); } };它所做的就是设置ReactScanInternals.runInAllEnvironments = true,绕过生产环境拦截。与之对应的拦截逻辑在 packages/scan/src/core/index.ts 的start()函数中:
if ( !ReactScanInternals.runInAllEnvironments && getIsProduction() && !ReactScanInternals.options.value.dangerouslyForceRunInProduction ) { return; }也就是说,默认入口在生产构建中会因getIsProduction()返回true而直接返回;使用all-environments入口后该判断被短路,扫描照常执行。该导出路径在 packages/scan/package.json 的exports字段中有明确声明("./all-environments")。需要说明的是,让扫描工具跑在生产环境通常仅用于排查线上性能问题,生产环境会带来性能开销,建议排查完毕后移除。
方式三:在app/entry.client中手动接管水合
如果你的项目自定义了app/entry.client,官方还提供了第三种方案——在 Remix 水合之前调用scan():
// app/entry.client.jsx import { RemixBrowser } from "@remix-run/react"; import { StrictMode, startTransition } from "react"; import { hydrateRoot } from "react-dom/client"; import { scan } from "react-scan"; scan({ enabled: true, }); // Hydration must happen in sync! // startTransition(() => { hydrateRoot( document, <StrictMode> <RemixBrowser /> </StrictMode> ); // });这是最直接的"抢占时机"方案:scan()在hydrateRoot调用之前同步执行,确保 React DOM 挂载前钩子已就位。
代码中的注释揭示了两个关键约束:
- 水合必须同步进行:官方把
startTransition包住hydrateRoot的调用注释掉了(// startTransition(() => {),因为异步过渡会推迟水合执行,破坏"先扫描后水合"的顺序保证。如果你的 Remix 模板默认使用startTransition包裹水合(这是某些 Remix 版本的默认entry.client写法),需要参照此示例调整。 - 入口文件职责单一:
app/entry.client是 Remix 的客户端引导入口,将扫描初始化放在这里、将 UI 根布局保留在app/root,职责划分更清晰。
三种方式的选型建议
| 方式 | 挂载位置 | 适用场景 | 是否支持生产运行 |
|---|---|---|---|
| Script 标签 | app/root的<Layout><head> | 不想改动构建、快速体验 | 是(脚本本身无环境判断) |
模块导入(app/root) | app/root的Layout+useEffect | 常规开发环境排查 | 需切换到all-environments |
模块导入(entry.client) | app/entry.client顶层 | 自定义了客户端入口、需要精确控制水合时序 | 需切换到all-environments |
选择建议:
- 临时排查:用 Script 标签方式,加载即用,改一行 HTML 即可。
- 长期集成:用模块导入方式,结合
process.env.NODE_ENV控制enabled,只在开发环境扫描。 - 线上问题复现:切换到
react-scan/all-environments,定位后立即移除。
常见问题与排错
Q1:控制台报错[React Scan] Failed to load. Must import React Scan before React runs.
这说明 React Scan 的钩子没有在 React 之前安装。在 Remix 中通常由以下原因导致:
<script>标签没有放在<head>最顶部;- 模块导入顺序中
react-scan排在了@remix-run/react之后; - 使用了
startTransition异步水合导致时序错乱。
该错误信息来自 packages/scan/src/core/index.ts 的start()函数,它会在初始化后 5 秒内检测到 instrumentation 未激活时输出。
Q2:生产环境看不到任何高亮?
默认入口在生产环境会主动退出(见上文start()中的getIsProduction()判断)。如确需生产扫描,改用react-scan/all-environments,或传入dangerouslyForceRunInProduction: true(更不推荐)。
Q3:React 18 项目能接入吗?
官方文档明确标注此指南仅支持 React 19。React 18 项目建议先升级 React 版本,或关注仓库中其他安装指南(如 Vite 指南、Next.js App Router 指南)确认对应版本的接入要求。
Q4:高亮轮廓与 Remix 的流式渲染冲突?
React Scan 基于 DevTools 钩子监听 commit 事件,与 Remix 的流式渲染(如defer/Await导致的后续提交)机制兼容——每个提交都会被独立捕捉并高亮。若发现轮廓位置漂移,优先检查 CSS 中是否有影响getBoundingClientRect的动画或 transform,这是所有基于 overlay 的性能工具共有的已知限制。
小结
在 Remix 中接入 React Scan 的核心是"抢先"二字:无论是通过<head>中的<script>、app/root中的模块导入,还是app/entry.client中的手动水合,都必须保证 React Scan 在 React 与 Remix 之前完成钩子安装。记住两个硬性前提——React 19、导入顺序——再按需选择是否通过react-scan/all-environments开启生产扫描,即可在 Remix 应用中获得即时的组件渲染高亮,快速定位需要优化的性能瓶颈。
延伸阅读
- CDN 指南:两种 CDN 地址的完整清单
- Vite 安装指南 / Next.js App Router 安装指南:其他框架的接入方式对照
- React Scan 主文档:
Options完整 API、CLI 用法与 FAQ - 核心实现:
scan()、start()、getIsProduction()的源码逻辑 - 自动注入入口:
auto.global.js的加载即扫描行为 - 包导出配置:
all-environments、auto、lite等导入路径声明
【免费下载链接】react-scanScan and fix React performance issues项目地址: https://gitcode.com/GitHub_Trending/re/react-scan
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考