React 19 前沿:从 Transition 到 Server Components
useTransition、Lanes 模型、RSC 全链路——Dan Abramov 2025 年最新输出精读
来源:react.dev/reference/react/useTransition
核心:非阻塞更新 + 优先级控制
const [isPending, startTransition] = useTransition()
function handleClick() {
startTransition(() => {
setTab(nextTab) // 这个更新被标记为 transition(低优先级)
})
}
1. Transition vs 普通 setState
普通 setState(SyncLane):
- 同步执行,阻塞渲染
- 用户交互立即响应
- 适合:点击按钮、输入框
Transition(Transition Lane):
- 可以被打断
- 不阻塞用户交互
- 适合:Tab 切换、搜索结果列表更新、大列表渲染
2. isPending 的作用
const [isPending, startTransition] = useTransition()
return (
<div>
<Tabs isPending={isPending} />
{/* isPending=true 时显示加载状态 */}
</div>
)
isPending 在 transition 开始时变为 true,所有 action 完成后变回 false。** 可以用来显示 loading 状态。
3. useTransition vs useDeferredValue
useTransition:
- 控制 setState 的优先级
- 需要 startTransition 包裹
- 适合:你知道要更新什么状态
useDeferredValue:
- 创建一个"延迟"版本的值
- 不需要 startTransition
- 适合:你只控制渲染结果的优先级
// useDeferredValue:输入框快速响应,搜索结果延迟更新
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
useEffect(() => {
fetchResults(deferredQuery) // query 停止输入后才执行
}, [deferredQuery])
4. Transition 的关键特性
1. 可被打断:高优先级任务(用户输入)可以中断低优先级 transition
2. 不会卡顿:transition 中的 setState 不会阻塞输入
3. 自动去重:多个 startTransition 调用会被合并
4. async 支持:React 19 中 startTransition 可以接受 async 函数
5. 内部机制(Lanes 模型)
普通 setState → 分配到 SyncLane(最高优先级)
startTransition → 分配到 TransitionLane(低优先级)
Scheduler 通过 MessageChannel 插入宏任务
当前宏任务执行完 → 清空微任务 → 检查 React 任务
如果有高优先级任务(用户输入)→ 打断当前 transition
如果没有 → 继续执行 transition
核心一句话
useTransition 把 setState 标记为低优先级(Transition Lane),让 React 可以在用户交互时打断它。isPending 告诉你 transition 是否在进行中。本质是 lanes 优先级模型的上层 API。
Separating Events from Effects(事件与 Effect 分离)
来源:react.dev/learn/separating-events-from-effects
核心观点:事件处理器处理用户操作,Effect 处理同步。两者响应变化的方式不同
事件处理器:非响应式(只在用户交互时执行)
Effect:响应式(依赖值变化时重新执行)
1. 什么时候用事件处理器,什么时候用 Effect
✅ 事件处理器:用户点击按钮发送消息
→ 只在用户点击时执行,不会因为 state 变化自动执行
✅ Effect:组件连接到聊天室
→ 不管用户怎么操作,只要 roomId 变了就需要重新连接
判断标准:这段代码是“响应用户操作”还是“保持同步”?
2. 响应式 vs 非响应式
事件处理器里的逻辑 = 非响应式
sendMessage(message) 只在用户点击时执行
即使 message 变了,也不会自动重新发送
Effect 里的逻辑 = 响应式
createConnection(roomId) 在 roomId 变化时重新执行
必须在 deps 里声明 roomId
3. 问题:Effect 里混合了响应式和非响应式逻辑
// 🔴 theme 变了会导致重新连接(不需要)
useEffect(() => {
const connection = createConnection(serverUrl, roomId)
connection.on('connected', () => {
showNotification('Connected!', theme) // theme 是响应式值
})
connection.connect()
return () => connection.disconnect()
}, [roomId, theme]) // theme 变了会重新执行整个 effect
问题: theme 变了不应该重新连接,只应该改变通知的颜色。但因为 theme 在 effect 里被读取了,React 要求你把它放进 deps。
4. 解决方案:Effect Events(useEffectEvent)
// ✅ 用 useEffectEvent 把非响应式逻辑提取出来
const onThemeChange = useEffectEvent((theme) => {
showNotification('Connected!', theme)
})
useEffect(() => {
const connection = createConnection(serverUrl, roomId)
connection.on('connected', () => {
onThemeChange(theme) // theme 不在 deps 里
})
connection.connect()
return () => connection.disconnect()
}, [roomId]) // 只依赖 roomId
useEffectEvent 是一个实验性 API(React 19+):**
-
把回调包装成“Effect Event”
-
Effect Event 可以读取最新的 props/state,但不参与依赖追踪
-
Effect 只在
roomId变化时重新执行 -
Effect Event 内部读到的
theme永远是最新的
5. 总结:什么时候用什么
事件处理器:
- 用户点击按钮
- 用户提交表单
- 用户输入
→ 只在交互时执行,非响应式
Effect:
- 连接/断开 WebSocket
- 订阅事件
- 操作 DOM(如修改 title)
→ 依赖值变化时重新执行,响应式
Effect Events(useEffectEvent):
- Effect 里需要调用非响应式逻辑
- 不希望某个值的变化触发 effect 重新执行
→ 提取成 Effect Event,脱离依赖追踪
核心一句话
事件处理器处理“用户做了什么”(非响应式),Effect 处理“系统需要保持什么状态”(响应式)。当 Effect 里混入了不需要响应的逻辑时,用
useEffectEvent提取出来,避免不必要的重新执行。
React Server Components 核心系列
这是 Dan 近两年最重要的输出,理解 RSC 的底层设计哲学。
1. The Two Reacts(2024.01)
来源:overreacted.io/the-two-reacts/
核心观点:React 的两个范式——客户端的 UI = f(state) vs 服务端的 UI = f(data)
客户端范式:********UI = f(state)
Counter 组件:count 在用户电脑上,setState 立即响应
→ 需要零延迟交互(拖拽、输入、hover)
→ 组件必须跑在客户端
→ 状态是"快照",闭包捕获当时的值
服务端范式:********UI = f(data)
PostPreview 组件:读文件系统、计算字数
→ 数据在服务端,组件跑在数据旁边
→ 构建时就执行完了,客户端只拿到渲染结果
→ 没有额外 API 请求,数据"已经在那了"
矛盾: 两个范式看似互斥——Counter 必须跑在客户端,PostPreview 必须跑在服务端。
真正的公式:********UI = f(data, state)
理想情况:一个编程范式同时处理 data 和 state
问题:如何把 f 拆分到两个不同的运行环境?
→ 这就是 RSC 要解决的问题
核心一句话
React 面临两难:交互需要客户端即时响应,数据处理需要服务端访问资源。RSC 的目标是让一个组件树能跨越两台计算机,同时保持 React 的组合模型。
2. React for Two Computers(2025.04)
来源:overreacted.io/react-for-two-computers/
核心观点:从标签 vs 函数调用的直觉出发,推导出 RSC 的设计
标签 vs 函数调用:
标签(JSX):名词,描述结构,像蓝图(blueprint)
函数调用:动词,描述过程,像菜谱(recipe)
标签天然适合深层嵌套(可读性好)
函数调用天然适合顺序执行
→ React 结合了两者:JSX 是声明式蓝图,事件处理器是命令式菜谱
蓝图 = 潜在的菜谱:
蓝图是把"时间"抽离后的菜谱
标签是把"执行"抽离后的函数调用
→ 标签是潜在的函数调用(tag is a potential function call)
RPC 的演进:
问题:函数在另一台电脑上
原始方案:回调函数 → 代码缠绕(callback hell)
解法1:async/await → 解决代码缠绕
解法2:import rpc → 像本地 import 一样调用远程函数
→ import rpc 语法:import rpc { prompt, alert } from './stuff'
单向通信(Server → Client):
如果另一台电脑不能"回话"怎么办?
→ 不能 await,不能拿返回值
→ 只能传递"潜在的调用":alert⧼'Hello'⧽
→ 这些潜在调用可以编码为 JSON:
{ fn: 'alert', args: [{ fn: 'prompt', args: ['Who?'] }] }
→ 这就是 RSC 协议的雏形!
核心一句话
标签是潜在的函数调用,蓝图是潜在的菜谱。RSC 把这个直觉推向极致:服务端组件是“潜在的客户端组件”,通过 JSON 协议跨越网络边界。
3. What Does “use client” Do?(2025.04)
来源:overreacted.io/what-does-use-client-do/
核心观点:'use client' 和 'use server' 是模块系统级别的创新
传统方式(stringly-typed):
后端:app.post('/api/like', handler)
前端:fetch('/api/like', { body: JSON.stringify(...) })
问题:
- 字符串拼接,没有类型检查
- API 路由和前端调用之间没有直接引用关系
- 找不到引用、重构困难
'use server'********:typed fetch()
// 后端
'use server';
export async function likePost(postId) { ... }
// 前端
import { likePost } from './backend';
await likePost(postId); // 自动变成 HTTP 调用
本质:把 RPC 做进了模块系统
- import 不再只是"导入代码",而是"跨网络调用"
- 类型安全、可静态分析、可用 Find All References
**'use client'****:typed **<script>
// 客户端组件
'use client';
export function LikeButton({ postId, likeCount }) { ... }
// 服务端
import { LikeButton } from './frontend';
<LikeButton postId={42} likeCount={8} />
本质:把 <script> 标签做进了模块系统
- import 得到的不是函数本身,而是"客户端引用"(地址)
- 服务端可以渲染它(预渲染),也可以只发引用(lazy load)
两个世界的两扇门:
'use client' → 从服务端到客户端的门(发送 <script>)
'use server' → 从客户端到服务端的门(发送 fetch)
两者都不执行代码,只是建立"引用"
import 从一侧拿到的是另一侧的"地址"
→ 模块系统跨越了网络边界
核心一句话
'use client'是有类型的<script>,'use server'是有类型的fetch()。它们把客户端/服务端的边界表达在模块系统里,让 import 成为跨越网络的桥梁。
4. Impossible Components(2025.04)
来源:overreacted.io/impossible-components/
核心观点:RSC 让你能写出“不可能”的组件——同时包含服务端数据和客户端交互
核心模式:Backend 渲染 Frontend
// 后端:读数据
async function GreetingBackend() {
const myColor = await readFile('./color.txt', 'utf8')
return <GreetingFrontend color={myColor} />
}
// 前端:处理交互
'use client'
function GreetingFrontend({ color }) {
const [yourName, setYourName] = useState('Alice')
return <p style={{ color }}>Hello, {yourName}!</p>
}
关键特性:
1. 后端先执行 → 数据"已经在那了",没有 loading 闪烁
2. 前端接管交互 → 状态隔离,每个实例独立
3. 组合性 → <Welcome /> 内部包含多个 GreetingBackend,无需手动协调
4. 自包含 → 一个标签搞定一切,局部数据 + 局部状态
进阶:SortableFileList
后端:readdir(directory) → 获取文件列表
前端:useState → 排序/过滤
一个组件同时做到:
- 读文件系统(只有服务端能做)
- 实时过滤排序(只有客户端能做)
- 单次往返,零额外请求
核心一句话
Impossible Component = Backend 数据 + Frontend 交互,封装在一个自包含的抽象里。局部数据,局部状态,一次往返。
5. JSX Over The Wire(2025.04)
来源:overreacted.io/jsx-over-the-wire/
核心观点:用 BFF + JSX 替代 REST API,让 API 返回组件而不是 JSON
REST 的问题:
REST Resource 没有牢固的现实基础:
- 靠近 Model → UI 要多次请求
- 靠近 ViewModel → 维护困难,Resource 形状膨胀
- 本质上是在猜"资源"应该长什么样
ViewModel API(BFF 模式):
每个屏幕有自己的 ViewModel 端点:
GET /screens/post-details/123
服务端返回的数据 = 该屏幕所有组件需要的数据
→ 一个屏幕一个请求,不多不少
→ 数据形状由 UI 决定,不是由"资源"决定
JSX Over The Wire:
API 路由直接返回 JSX 而不是 JSON:
app.get('/api/likes/:postId', (req, res) => {
res.json(<LikeButton totalLikeCount={...} isLikedByUser={...} />)
})
→ API 不再是"返回数据的端点"
→ API 变成"返回组件的端点"
→ 组件本身就是数据的 ViewModel
BFF 的核心价值:
1. 服务端靠近数据源 → 延迟低
2. 可以并行调用多个 REST API → 消除瀑布
3. 每个屏幕独立优化 → 不影响其他屏幕
4. 前端团队掌控数据获取 → 不依赖后端改 API
核心一句话
REST Resource 既不反映数据存储方式,也不反映 UI 展示方式。用 BFF 模式让每个屏幕拿到恰好需要的数据,用 JSX 替代 JSON 让组件直接跨越网络边界。
6. Functional HTML(2025.05)
来源:overreacted.io/functional-html/
核心观点:从零重新发明 HTML,一步步推导出 RSC 的设计
演进路线:
1. Server Tags → 自定义标签 = 函数,服务端调用并替换
2. Attributes → 传参,支持对象
3. JSON 输出 → 序列化为 JSON 而非 HTML,保留对象
4. Async Server Tags → 标签可以 async,读文件系统
5. Client References → 'use client' = 有类型的 <script>
6. Server References → 'use server' = 有类型的 fetch()
7. Client Tags → 客户端组件作为标签延迟执行
8. Full-Stack Tags → 同时组合服务端和客户端标签
9. Streaming → 异步标签不阻塞,Suspense 控制揭示时机
核心洞察:
- <script> 标签在我们的体系里是不必要的
- 'use client' 是 <script> 的模块系统表达
- 'use server' 是 fetch() 的模块系统表达
- HTML 只是 JSON 的一种表现形式
- 主要输出格式是 JSON,HTML 是可选的"渲染"
核心一句话
从 HTML 出发,加上函数调用、JSON 序列化、异步标签、客户端引用、服务端引用、streaming——最终自然推导出 RSC。RSC 不是凭空发明,而是 HTML 演化的必然结果。
7. Static as a Server(2025.05)
来源:overreacted.io/static-as-a-server/
核心观点:“静态”只是“提前运行的服务器”
任何"服务器"框架都能输出"静态"站点:
→ 在构建时运行服务器,对每个页面发一次请求,把响应存到磁盘
"静态"和"服务器"不是两种工具,而是同一套代码的两种运行模式
Hybrid 框架的优势:
- 减少工具碎片化(一个生态系统搞定)
- 按路由选择 server 或 static
- 可以渐进式迁移(先静态,后加动态页面)
- 代码和插件在两种模式间共享
Dan 的博客就是例子:
用 Next.js + RSC 写的 → 但输出是纯静态站点
部署在 Cloudflare 免费静态托管 → 成本 $0
React Server Component 在构建时运行 → 不需要真正的服务器
核心一句话
“静态”是“服务器”的特例——提前运行、结果存盘。RSC 代码写一次,server 和 static 两种模式都能跑。
8. One Roundtrip Per Navigation(2025.05)
来源:overreacted.io/one-roundtrip-per-navigation/
核心观点:导航应该一次往返拿到所有数据,RSC 天然做到这一点
演进:从一次请求到 N 次请求再到一次请求
HTML 时代:一次请求,数据嵌在 HTML 里 ✓
REST + 客户端渲染:N 个 API 请求 + 客户端瀑布 ✗
Server Loader:服务端并行请求,一次返回 ✓
Server Functions(colocated):回到了 N 次请求 ✗
数据获取策略对比:
组件内 fetch (useEffect):
✗ 请求散布在组件树中,瀑布不可见
✗ 性能问题难以审计
React Query (useQuery):
✗ 仍然是客户端发起,瀑布仍在
✓ 缓存有帮助,但不解决根本问题
Server Loader (路由级):
✓ 消除瀑布,一次往返
✗ 丧失 colocation(数据逻辑和组件分离)
Server Functions (组件级):
✓ 恢复 colocation
✗ 但回到了客户端瀑布问题
GraphQL Fragments 的思路:
组件声明自己需要什么数据(fragment)
→ 服务端收集所有 fragment → 合并为一次查询
→ 既保持 colocation 又消除瀑布
RSC 的做法:
Server Components 天然在服务端执行
→ 数据获取在服务端,靠近数据源
→ 一次渲染 = 一次往返
→ 不存在客户端瀑布问题
→ Suspense 处理渐进式揭示
核心一句话
客户端数据获取永远面临“colocation vs 性能”的矛盾。RSC 通过让组件在服务端执行,从根本上消除了这个矛盾——数据获取天然在服务端,天然一次往返。
9. Why Does RSC Integrate with a Bundler?(2025.05)
来源:overreacted.io/why-does-rsc-integrate-with-a-bundler/
核心观点:RSC 需要打包器来高效地“序列化模块”
问题:如何序列化一个模块?
不能把代码内嵌为字符串(eval 不安全,重复传输)
→ 应该引用打包后的文件:'/chunk123.js#Counter'
→ 客户端按需加载对应 chunk
打包器在 RSC 中的三个职责:
1. 构建时:找到 'use client' 文件,创建 chunk 入口
2. 服务端:教 React 如何序列化模块引用(如 'chunk123.js#Counter')
3. 客户端:教 React 如何按引用加载模块
serialize / deserialize API:
服务端:
import { serialize } from 'react-server-dom-webpack'
const output = serialize(<Counter initialCount={10} />)
客户端:
import { deserialize } from 'react-server-dom-webpack/client'
const tree = deserialize(output) // → <Counter initialCount={10} />
root.render(tree)
核心一句话
RSC 不仅发送数据,还发送代码。打包器负责把
'use client'组件变成可引用的 chunk,让服务端能序列化模块引用、客户端能按需加载。
10. How Imports Work in RSC(2025.06)
来源:overreacted.io/how-imports-work-in-rsc/
核心观点:RSC 扩展了 import/export 的语义,让两个环境共享模块系统
JavaScript 模块系统基础:
import 本质上像"复制粘贴",但有关键区别:
- 模块是单例(每个模块最多执行一次)
- 不同文件 import 同一模块 → 共享同一份代码
- 最终效果等价于把所有模块拼成一个文件
两个环境,两个模块系统:
后端 import a.js → a.js 的代码进入后端
前端 import a.js → a.js 的代码进入前端
两边各有"自己的版本",不共享执行上下文
→ 同一个模块可以在两边都存在,但各自独立
server-only 包(毒丸机制):
import 'server-only' // 标记为服务端专用
如果前端打包器检测到这个 import → 构建失败
→ 防止服务端密钥/代码泄漏到客户端
RSC 的 import 语义扩展:
从 'use client' 文件 import:
→ 不执行代码,只拿到客户端引用(地址)
→ 运行时按地址加载模块
从 'use server' 文件 import(从客户端):
→ 不执行代码,只拿到异步函数
→ 调用时自动发 HTTP 请求
同一个文件从不同侧 import → 不同行为
→ import 的语义取决于"谁在 import"
核心一句话
RSC 不发明新概念,而是扩展 import 的语义:同一份 import,在服务端是“直接引用”,在客户端是“跨网络调用”。两个环境共享模块系统,但各自独立执行。
11. Progressive JSON(2025.05)
来源:overreacted.io/progressive-json/
核心观点:RSC 用“渐进式 JSON”传输组件树,一个慢部分不阻塞其他部分
传统 JSON 的问题:
必须等最后一个字节到齐才能 parse
→ 一个慢的部分(如数据库查询)阻塞整棵树
→ 客户端什么都做不了,直到服务端完成所有工作
Progressive JSON(广度优先传输):
第一层(外壳):
{ header: "$1", post: "$2", footer: "$3" }
→ 客户端立刻拿到 header 和 footer
第二层(填充):
/* $2 */ { content: "$4", comments: "$5" }
→ post 的结构出来了,但 comments 还是 Promise
第三层(完成):
/* $5 */ ["$6", "$7", "$8"]
→ comments 填充完毕
关键设计:
1. 占位符($1, $2...):未到达的部分用 Promise 表示
2. 乱序到达:不需要按顺序,客户端按 ID 填充
3. 内联优化:小对象直接内嵌,大对象 outline 为独立行
4. 去重:相同对象只传一次,其他地方引用
5. 循环引用:支持(通过行号引用)
Streaming Data vs Streaming UI:
数据:渐进式到达(RSC 协议自动处理)
UI:由 <Suspense> 控制揭示时机
Promise = throw(数据还没到)
<Suspense> = catch(展示 fallback)
数据传输和 UI 揭示是解耦的
→ 数据尽快到达,UI 按设计意图揭示
核心一句话
Progressive JSON = 广度优先传输 + 占位符 Promise。数据尽快到达,一个慢部分不阻塞其他部分,
<Suspense>控制用户看到什么。
12. Introducing RSC Explorer(2025.12)
来源:overreacted.io/introducing-rsc-explorer/
核心观点:RSC Explorer 是一个可视化工具,展示 RSC 协议的实际工作方式
工具特点:
- 纯客户端运行(Server 部分在 Worker 里)
- 使用 React 官方的 serialize/deserialize 包
- 逐步执行,可视化 RSC 协议流
- 源码完全开源
可以探索的场景:
- Hello World:JSX 如何跨越网络
- Async Component:Streaming + Suspense 的实际效果
- Counter:客户端引用如何传输
- Form Action:Server Action 如何工作
- Router Refresh:无框架的 RSC 路由实现
- Pagination / Error Handling / Binary Data
- CVE-2025-55182(安全漏洞复现)
访问地址: https://rscexplorer\.dev/
核心一句话
RSC Explorer 是理解 RSC 协议的最佳可视化工具——逐步执行、实时查看序列化/反序列化过程,纯客户端运行。
13. How to Fix Any Bug(2025.10)
来源:overreacted.io/how-to-fix-any-bug/
核心观点:系统化的 bug 修复流程——找复现、缩复现、删代码、找根因
四步流程:
Step 0: 不要让 AI 直接修(它没有复现,会反复说"修好了"但没有)
Step 1: 找复现(Repro)
- 明确:做什么 → 期望什么 → 实际什么
- 可靠复现 = 每次都能触发
Step 2: 缩窄复现
- 把视觉问题转化为可测量的问题
- "滚动抖动" → "scroll position 没变"
- 关键:确认正向结果也能获得(注释掉代码能修好)
- 否则可能在修一个无关的问题
Step 3: 逐步删除
1. 跑复现,确认 bug 存在
2. 删一段代码
3. 再跑复现
4. bug 还在 → 提交
5. bug 消失 → 记录理论,回滚,删更小的块
关键:始终保持一个"bug 仍在发生"的检查点
→ 像 well-founded recursion,每步都必须"更小"
Step 4: 找根因
- 删到最小可复现 → 分析代码或依赖
- 本例:React Router 的 ScrollRestoration bug
常见错误:
✗ 删着删着开始"测试理论",创建新的复现案例
→ 新案例可能跟原 bug 无关
→ 正确做法:回到原复现,继续删
✗ 让 AI 直接修
→ AI 没有可靠的复现验证手段
→ 先把复现转化为 AI 可测量的形式
核心一句话
修 bug 的秘诀:找到可靠复现 → 转化为可测量的形式 → 逐步删除代码同时确认 bug 仍在 → 直到删无可删,根因自然浮现。
14. Suppressions of Suppressions(2025.06)
来源:overreacted.io/suppressions-of-suppressions/
核心观点:用 lint 规则禁止 suppress 其他 lint 规则
问题:
Lint 规则有时是错的 → 需要 suppress(注释禁用)
但有些规则绝对不能 suppress(会导致线上事故)
→ 需要一种机制防止误 suppress
解法:suppress 规则的规则
引入一条新 lint 规则:禁止 suppress 某些特定规则
→ 如果你想 suppress 一个"不可 suppress"的规则
→ 会触发新的 lint 错误
进一步:禁止 suppress 这条规则本身(双重 suppress)
→ 只能通过 code review + 人工审批
实际应用(Facebook):
- 某些规则的 suppress 需要链接到 ticket
- 双重 suppress 需要 infra 团队审批
- 自动化 grep 检测新提交的双重 suppress
- 关键规则的 suppress 可以阻止合并
更广的意义:
Lint 规则背后是"社会契约":
- 如何版本管理软件
- 文件结构如何映射组织结构
- 如何分配共担责任
- 如何在演进模式的同时避免事故
核心一句话
有些 lint 规则绝对不能被 suppress——用另一条 lint 规则来禁止 suppress,形成“suppressions of suppressions”。本质上是用工具编码社会契约。
核心一句话总结(全部 14 篇)
RSC 系列(12篇):
React 正在把客户端/服务端的边界做进模块系统。
'use client' = typed <script>,'use server' = typed fetch()。
组件可以跨越两台计算机,数据渐进式传输,一次往返搞定。
"静态"只是"提前运行的服务器"。
How to Fix Any Bug:
找复现 → 缩复现 → 逐步删除 → 根因浮现。
始终保持一个"bug 仍在"的检查点。
Suppressions of Suppressions:
用 lint 规则禁止 suppress 关键 lint 规则。
工具编码社会契约。