mcpField
R
remix-v2-perf-ssr-review

remix-v2-perf-ssr-review

github.com/existential-birds测试与质量8540 次安装11 周前更新
代码仓库

关于 remix-v2-perf-ssr-review

面向审查 Remix v2 TypeScript 路由模块的性能与 SSR 技能。它提供缓存、服务端与客户端边界、水合、预取和资源加载的检查清单,并要求基于代码位置与上下文验证问题。

主要功能

  1. 01检查缓存标头与 Cookie 变化
  2. 02审查服务端和客户端模块边界
  3. 03识别 SSR 水合风险
  4. 04检查预取与流式数据用法
  5. 05审查图片、字体和样式资源

适用场景

  1. 01审查路由缓存策略
  2. 02排查服务端代码泄漏风险
  3. 03定位渲染导致的水合差异
  4. 04检查预取和资源加载配置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/existential-birds/beagle --skill remix-v2-perf-ssr-review

安装后在 Claude Code 中输入/remix-v2-perf-ssr-review确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
assets.md
caching-headers.md
hydration.md
prefetch-streaming.md
server-client-split.md

详细介绍

Remix v2 性能 / SSR 代码审查

目标是导入 @remix-run/* 的 TypeScript 路由模块。规范模式请参阅 remix-v2-perf-ssr。

快速参考

问题类型参考
缺少 headers 导出、不安全的 public 缓存、子路由丢弃父路由的标头、缺少 Vary: Cookie、Set-Cookie 与 public 并用references/caching-headers.md
导入的服务端库没有放在 .server.ts 文件中、泄露 process.env.SECRET_*、用 typeof window 代替 .server.tsreferences/server-client-split.md
渲染时调用 new Date()、在键中使用 Math.random()、未显式指定区域设置的本地化格式化、缺少 useId()、无差别使用 suppressHydrationWarningreferences/hydration.md
每个链接都设置 prefetch="render"、对快速数据使用 defer、<Await> 外缺少 <Suspense>、预取有副作用的路由references/prefetch-streaming.md
dangerouslySetInnerHTML 使用不受信任的数据、缺少 loading="lazy"、缺少 links 预加载、在 body 中注入样式表references/assets.md

审查清单

  • 提供数据的路由会导出 headers(即使响应为 no-store)
  • 提供个性化数据的子路由会导出自己的 headers(否则会悄无声息地继承父路由的策略)
  • 绝不对需要身份验证或带有 cookie 的响应设置 Cache-Control: public
  • 缓存决策取决于会话时,会设置 Vary: Cookie
  • 仅限服务端使用的库(prisma、bcrypt、node:fs、jsonwebtoken)位于 *.server.ts 或 app/.server/ 中
  • 仅在 loaders/actions 或 .server 模块中读取机密环境变量(如 process.env.STRIPE_SECRET_KEY)
  • 暴露给客户端的环境变量会列入白名单并放入 window.ENV,绝不直接暴露 process.env
  • 不会使用 typeof window === "undefined" 代替 .server.ts(摇树优化并不可靠)
  • JSX 渲染路径中不会调用 new Date()、Math.random()、Date.now() 或 crypto.randomUUID()
  • 本地化格式化(toLocaleDateString、Intl.DateTimeFormat)会传入明确的区域设置
  • 生成 ID 的组件会使用 useId(),而非 Math.random() 或计数器
  • suppressHydrationWarning 仅用于单个元素,并附有说明原因的代码注释
  • <Link prefetch="render"> 仅用于首屏关键导航,而非列表
  • <PrefetchPageLinks> 不会指向 loader 有副作用(分析、变更)的路由
  • 每个 <Await> 都由 <Suspense> 包裹,并设置 errorElement
  • 仅对确实较慢的数据(>~50ms)使用 defer();快速数据会等待完成
  • 首屏以下的图片使用 loading="lazy",并设置 width / height
  • 关键字体 / CSS 通过 links 导出预加载,而非在 body 中注入

有效模式(不要标记)

以下是正确的 Remix v2 用法,不得报告为问题:

  • 有意关闭缓存的路由未导出 headers — 经过身份验证的仪表板、账户页面,以及由已返回 no-store 的布局包裹的路由,可以合理地省略 headers。只有当路由提供可缓存的公开内容但没有 headers 时才标记。
  • useEffect 中的 new Date() — 仅在客户端水合后运行;不会导致 SSR 不匹配。效果函数中的 Date.now()、Math.random()、crypto.randomUUID() 也一样。
  • 事件处理器中的 Math.random() / new Date() — 处理器会在水合后运行。只有当该值在渲染期间使用时才标记。
  • 在单个 <time>(或类似)元素上使用 suppressHydrationWarning,并附有清晰注释 — 对于已知会发生差异的值(如在客户端格式化的绝对时间戳),这是可接受的有限例外。只有当它应用于包裹大型子树的父元素,或没有说明时才标记。
  • 客户端专用库的 .client.ts 文件 — Stripe.js、地图小部件、读取 window 的图表库按惯例应放在 *.client.ts 中;不要因文件扩展名而标记。
  • useId() 后追加额外字符 — `${id}-input` 是多元素组件的文档化模式;不要将其标记为“非稳定 ID”。
  • SSR 中渲染原始 ISO 字符串,再在 useEffect 中重新格式化 — 这是规范的水合安全时间模式;只有在渲染期间重新格式化时才标记。
  • 返回 {} 或 no-store 的 headers 导出 — 明确表示“不要缓存”是有意为之,不应标记为误用。
  • 标准导航中的 <Link prefetch="intent"> — 这是推荐的默认设置;只有当 loader 有副作用时才标记。
  • 通过 headers 导出将 loaderHeaders 转发到文档 — 将数据策略与文档策略放在一起是文档化模式,不是重复设置。

依上下文适用的规则

仅在符合特定上下文时应用:

问题仅当以下情况时标记
未导出 headers路由提供可缓存的公开内容(未经过身份验证、非个性化,且并非有意设置为 no-store)
子路由未导出 headers某个祖先导出了 headers,且其策略范围比子路由的可缓存性更广(例如,父路由缓存公开内容并使用 s-maxage,而子路由提供个性化数据)
Cache-Control: publicLoader 实际读取了 session / 用户状态(或响应包含 Set-Cookie)
缺少 Vary: CookieLoader 根据 cookie(主题、区域设置、session)改变响应结构,且缓存设置为 public/s-maxage
new Date() / Math.random() / Date.now()调用位置在渲染路径中——不在 useEffect、事件处理器、<ClientOnly> 或水合后的代码中
未指定区域设置的区域格式化结果被渲染到 JSX 中(而非仅在效果函数 / 处理器中使用)
<Link prefetch="render">链接位于列表 / .map() 迭代器中(而不是首屏关键导航中)
<Link prefetch="intent"> 指向有副作用的 loaderLoader 有可观察的副作用(写入分析数据、递增计数器、输出日志),且没有根据 Purpose: prefetch 标头进行分支处理
未使用 .server.ts 的服务端库导入导入文件可从客户端依赖图访问(路由模块、从组件可访问的非 .server 工具模块)
process.env.SECRET_* 引用引用位于组件主体中,或位于客户端依赖图可访问的非 .server 模块中
图片缺少 loading="lazy"图片在首屏以下渲染(不在 <header>、主视觉区域或任何 <main> 内容之前)
图片缺少 width/height项目未使用会在构建时注入尺寸的图片处理器

硬性门槛(撰写问题报告前)

按顺序执行以下检查。在本批次的所有门槛都通过之前,不要撰写面向用户的问题报告。

  1. 位置证据 — 通过条件: 每个问题都列出仓库路径,以及你实际读取的文件中的行范围或简短原文引用(不能凭记忆或只根据差异猜测)。没有具体文件路径的缓存、hydration 和 .server 相关问题不得报告。

  2. 豁免检查 — 通过条件: 对于每个问题,你都能用一行说明它为何不属于 有效模式(不要标记)所涵盖的情况。尤其要确认:缺少 headers 导出的问题不在有意禁用缓存的路由上;.client.ts 不是合法的仅限客户端库;suppressHydrationWarning 不是在特定范围内使用且附有注释。

  3. Hydration 上下文检查 — 通过条件: 在标记 new Date()、Math.random()、Date.now()、crypto.randomUUID() 或区域格式化之前,确认调用点位于组件的渲染路径中。在 useEffect、useLayoutEffect、事件处理程序、传给 setTimeout/requestAnimationFrame 的回调中,或 <ClientOnly>{() => ...}</ClientOnly> 内的调用都发生在 hydration 后,不得标记。

  4. 父子 headers 链检查 — 通过条件: 在将“子级缺少 headers,导致静默继承缓存策略”标记为问题之前,确认匹配链中的某个祖先路由确实导出了 headers(搜索路由文件树中的 export const headers 或 export function headers),并且继承的策略比子级的可缓存性范围更广。如果没有祖先导出 headers,这个问题只是“未配置缓存”,而不是“子级静默继承父级缓存”。

  5. 服务器/客户端边界检查 — 通过条件: 在将服务器库导入标记为泄漏之前,确认导入文件可从客户端依赖图访问——也就是说,它是路由模块、由路由默认导出传递导入的非 .server 工具,或 .client.ts 文件。loader、action、headers 或其他 .server.ts 模块中的导入不属于泄漏。

  6. 流程 — 通过条件: 已完成本次审查的 review-verification-protocol 中的预报告验证清单。

何时加载参考资料

审查问题

  1. 每个提供可缓存数据的路由是否都声明了 Cache-Control 策略,即使是“no cache”也不例外?
  2. 个性化路由是否都未使用 public 缓存,并在会话会影响响应时设置 Vary: Cookie?
  3. 服务端库(prisma、bcrypt、fs、机密环境变量访问)是否都位于 *.server.ts 模块中,以便构建过程能在其泄漏时拒绝构建?
  4. 公共环境变量是否通过白名单加入 window.ENV,而不是从 process.env 展开?
  5. new Date() / Math.random() / 区域格式化调用是否仅限于副作用、处理程序或 <ClientOnly> 中,而不在渲染期间调用?
  6. 需要 ID 的组件是否使用 useId()?
  7. <Link prefetch> 模式是否与使用场景匹配(仅在首屏上方使用 render,导航使用 intent,列表中使用 viewport/intent)?
  8. defer() 是否仅用于真正耗时的数据,并且 <Await> 是否始终与 <Suspense> 和 errorElement 配合使用?
  9. 图片是否设置了尺寸、在首屏下方延迟加载,关键字体/CSS 是否通过 links 导出预加载?
  10. dangerouslySetInnerHTML 是否仅用于经过清理的 HTML 或安全序列化的 JS?

提交发现前

完成 硬性门槛(尤其是门槛 3——水合上下文检查,以及门槛 5——服务端/客户端边界检查),然后仅报告仍通过 review-verification-protocol 报告前检查的问题。发现项格式:[FILE:LINE] ISSUE_TITLE,附上违规代码的逐字引用,并用一句话说明其违反的具体 Remix v2 契约。

补充文档

评论

支持 Markdown