Next.js 16,缓存组件和 React 编译器:真正的变化是什么
· 2 min read · Filippo Spinella · Next.js, React, Frontend, Performance
多年来,Next.js 中最烦人的问题之一一直是:“此页面是静态的还是动态的?”。这似乎是一个简单的问题,直到您添加对 cookies() 的调用、具有不同选项的 fetch、数据库客户端、CMS、购物车或自定义内容。
Next.js 16 很有趣,因为它试图让这个对话变得不那么神秘。它并没有消除复杂性,但它改变了思维模型:默认情况下路由是动态的,缓存在需要时声明自身,并且 Suspense 成为用保持新鲜的部分组成快速 shell 的自然方式。
要了解的功能是缓存组件。 Stable Turbopack、React Compiler、proxy.ts 和新的失效 API 很重要,但它们都围绕着同样的问题:构建快速的应用程序,而不必猜测框架在幕后决定了什么。
因为这件事很重要
在真正的应用程序中,您不仅仅有“静态页面”和“动态页面”。您有不同的需求的不同部分。
产品表每天可能会更改几次。价格可能会更频繁地变化。可用性必须几乎是实时的。用户名是个人的。评论可以流式传输。侧边栏可以稳定。购物车没有。
如果您将所有事物视为一个整体,那么您总是会陷入两个极端之一:
- 积极的缓存和看到旧数据的风险;
- 到处都是动态渲染,性能比必要的要差。
缓存组件正是为了避免这种错误的选择。
实践中的模型
通过 cacheComponents: true,您可以使用 "use cache" 声明可缓存的内容。然后,您可以将持续时间和标签与 cacheLife() 和 cacheTag() 关联起来。动态部分保持动态,可以使用 Suspense 进行隔离。
设置很小:
// next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { cacheComponents: true, }; export default nextConfig;
最大的变化并不在于配置。这取决于您如何开始编写组件。
// app/products/[slug]/page.tsx import { Suspense } from 'react'; import { cacheLife, cacheTag } from 'next/cache'; async function getProduct(slug: string) { 'use cache'; cacheLife('hours'); cacheTag(`product:${slug}`); return db.product.findUnique({ where: { slug } }); } async function ProductDetails({ slug }: { slug: string }) { const product = await getProduct(slug); return ( <section> <h1>{product.name}</h1> <p>{product.description}</p> </section> ); } async function LiveInventory({ slug }: { slug: string }) { const inventory = await db.inventory.findFirst({ where: { slug } }); return <p>{inventory.quantity} pezzi disponibili</p>; } export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; return ( <> <ProductDetails slug={slug} /> <Suspense fallback={<p>Controllo disponibilità...</p>}> <LiveInventory slug={slug} /> </Suspense> </> ); }
该页面不必全部缓存或全部动态。产品表可以快速且可重复使用。库存可以保持新鲜。用户可以立即看到某些内容,而无需等待最慢的部分。
use cache 是可执行文档
我喜欢 "use cache" 的一点是它迫使你明确表达意图。当你读到一个函数时,你立即明白有人已经决定:“这个数据可以重用”。
当您不使用 fetch 时,它特别有用。许多应用程序从 Prisma、Drizzle、内部 SDK、CMS 客户端或服务功能读取数据。在这些情况下,仅基于 fetch 选项的旧推理是不够的。
经验法则:
- 缓存a含量相对稳定;
- 使用细粒度标签;
- 留下动态权限、会话、购物车、通知和事务状态;
- 将慢速部分放入
Suspense中; - 在说“我们提高了绩效”之前先进行衡量。
无效而不扔掉所有东西
仅当您可以准确更新缓存时,缓存才有用。这里 cacheTag、revalidateTag 和 updateTag 变得很重要。
示例:
'use server'; import { updateTag } from 'next/cache'; export async function updateProductName(productId: string, name: string) { await db.product.update({ where: { id: productId }, data: { name }, }); updateTag(`product:${productId}`); }
重要的细节是标签。 product:${productId} 给出了精确的边界。 products 讲述了一个巨大的桶。起初,巨大的水桶很舒服;几个月后,它就成为您因更改标题而使半个应用程序失效的原因。
Stable Turbopack:您每天听到的部分
Next.js 16 将 Turbopack 引入开发和构建中心。这不是最富有诗意的功能,但它是您在工作时感受到的功能:服务器启动更早,刷新更快,构建不再感觉像是被迫喝咖啡休息。
也就是说,我不会闭着眼睛迁移充满自定义插件的代码库。我会检查:
- 本地构建;
- 非标进口;
- MDX、SVG 和 CSS;
- 剩下的 Webpack 插件;
- 关键页面;
- 构建时间的差异。
对于新项目,我会从默认开始。对于成熟的,我会进行谨慎的迁移。
React Compiler:消除噪音,而不是思考
React Compiler 1.0 很稳定,Next.js 16 通过 reactCompiler 支持它。我们的承诺是减少大量的手动记忆:“为了安全”使用更少的 memo、更少的 useMemo、更少的 useCallback。
// next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { reactCompiler: true, }; export default nextConfig;
我不会把它当作魔法灰尘。当代码很好地遵循 React 规则时,编译器会提供帮助。如果组件有奇怪的副作用、隐藏的突变或使用不当的钩子,则需要首先修复。
健康的尝试方法:
- 更新
eslint-plugin-react-hooks; - 纠正实际违规行为;
- 在受控区域启用它;
- 测量构建时间和行为;
- 仅当不再需要时才删除手动记忆。
目标不是擦除每个 useMemo。我们的目标是停止编写预防性记忆,因为我们害怕渲染。
proxy.ts 和网络边界
旧的 middleware.ts 变为 proxy.ts。这是一个名称更改,但它是有道理的:该文件位于请求边界,它不是传统的后端样式通用中间件。
// proxy.ts import { NextRequest, NextResponse } from 'next/server'; export default function proxy(request: NextRequest) { const isLoggedIn = Boolean(request.cookies.get('session')); if (!isLoggedIn && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); }
这里的规则很简单:保持较小。重定向、身份验证路由、标头、必要的重写。如果它开始感觉像是第二个后端,那么它可能做得太多了。
我将如何真正迁移
我不会立即打开所有功能。我会这样做:
- 我更新 Next、React 和 React DOM;
- 我启动了官方的codemod;
- 我修复了
params、searchParams、cookies()、headers()和draftMode()上的重大更改; - 我将
middleware.ts迁移到proxy.ts; - 我检查构建和关键页面;
- 我在缓存当前产生摩擦的部分启用缓存组件;
- 我定义了标签和失效的约定;
- 我单独尝试React Compiler;
- 之前和之后的指标比较。
好的迁移并不是使用所有新功能的迁移。这使得应用程序的行为更具可读性。
思维方式发生了哪些变化
Next.js 16 最有用的一点是它迫使您更好地命名意图。函数不仅仅是“从数据库获取产品”。它是“获取产品,我可以将其缓存几个小时,我使用此标签使其无效”。组件不仅仅是“渲染页面”。它是“这是快速外壳,这是个人的,这是流媒体的。”
起初,这似乎需要更多的工作。然后它就变成了一种平静。性能决策不再隐藏在默认值、启发式和部落记忆的组合中。它们在代码中。