Transcript

00:00:00(欢快的音乐) - 好的,大家好。
00:00:06我叫奥罗拉。
00:00:07我是一名来自挪威的网页开发者。
00:00:09我在 Crane Consulting 担任顾问,目前在我的咨询项目中积极使用 Next.js App Router 进行开发。
00:00:16今天,我将向大家介绍现代 Next.js 中关于组合、缓存和架构的设计模式,这些模式将帮助您确保应用的可扩展性和高性能。
00:00:24首先,让我回顾一下本次演讲最基本的核心概念:静态渲染和动态渲染。
00:00:30在 Next.js App Router 中,我们都会遇到它们。
00:00:33静态渲染能帮助我们构建更快的网站,因为预渲染的内容可以被缓存并全球分发,从而确保用户能够更快地访问。
00:00:42例如,Next.js Conf 网站。
00:00:46静态渲染减少了服务器负载,因为内容无需为每个用户请求生成。
00:00:51预渲染的内容也更容易被搜索引擎抓取工具索引,因为内容在页面加载时就已经可用。
00:00:58另一方面,动态渲染允许我们的应用程序显示实时或频繁更新的数据。
00:01:05它还使我们能够提供个性化内容,例如仪表盘和用户资料。
00:01:09例如,Vercel 仪表盘。
00:01:12通过动态渲染,我们可以访问只能在请求时才能知道的信息。
00:01:16在这种情况下,是哪个用户正在访问他们的仪表盘,也就是我。
00:01:20有些 API 会导致页面动态渲染。
00:01:25使用传递给页面或其等效 Hook 的 params 和 search params 属性会导致动态渲染。
00:01:32然而,对于 params,我们可以使用通用的静态 params 预定义一组预渲染页面,我们也可以缓存用户生成页面时的内容。
00:01:40此外,读取传入请求的 cookie 和 header 会使页面选择动态渲染。
00:01:46但是,与 params 不同的是,尝试使用 header 或 cookie 缓存或预渲染任何内容会在构建期间抛出错误,因为这些信息无法提前得知。
00:01:56最后,使用带有 data cache 配置 no store 的 fetch 也会强制进行动态渲染。
00:02:00所以这些是少数,还有一些其他的 API 会导致动态渲染,但这些是我们最常遇到的。
00:02:06在 Next.js 的早期版本中,页面要么完全静态渲染,要么完全动态渲染。
00:02:13页面上的一个动态 API 就会使整个页面选择动态渲染。
00:02:17例如,对 cookie 值进行简单的身份验证检查。
00:02:20通过利用带有 Suspense 的 React Server Components,我们可以在动态内容(如个性化欢迎横幅或推荐)准备就绪时进行流式传输,并在显示静态内容(如新闻通讯)时仅提供 Suspense 的 fallback。
00:02:34然而,一旦我们在动态页面上添加多个异步组件,比如特色产品,它们也会在请求时运行,即使它们不依赖于动态 API。
00:02:45因此,为了避免阻塞初始页面加载,我们也会暂停并流式传输这些组件,做额外的工作,创建骨架屏,并担心诸如布局偏移之类的问题。
00:02:56然而,页面通常是静态和动态内容的混合。
00:03:01例如,一个依赖用户信息的电子商务应用,但仍然包含大部分静态数据。
00:03:07被迫在静态或动态之间做出选择,会导致服务器上对从不或很少更改的内容进行大量冗余处理,这不利于性能。
00:03:19为了解决这个问题,在去年的 Next.js Conf 上,use cache 指令被宣布。
00:03:26今年,正如我们在主题演讲中看到的,它已在 Next.js 16 中可用。
00:03:30所以有了 use cache,页面将不再被迫进行静态或动态渲染。
00:03:36它们可以兼而有之。
00:03:37Next.js 不再需要根据页面是否访问 params 等内容来猜测页面是什么。
00:03:43默认情况下,一切都是动态的,use cache 让我们明确选择缓存。
00:03:47Use cache 实现了可组合的缓存。
00:03:51我们可以将页面、React 组件或函数标记为可缓存。
00:03:55在这里,我们实际上可以缓存特色产品组件,因为它不需要请求和处理,也不使用动态 API。
00:04:03这些缓存片段可以进一步预渲染并作为静态外壳的一部分包含在部分预渲染中,这意味着特色产品现在在页面加载时就可用,无需流式传输。
00:04:14现在我们有了这些重要的背景知识,让我们来演示一下。
00:04:19改进一个 Next.js 应用中常见的代码库问题。
00:04:24这些问题包括深度 prop 钻取,导致难以维护和重构功能,冗余的客户端 JavaScript 和具有多重职责的大型组件,以及缺乏静态渲染,导致额外的服务器成本和性能下降。
00:04:36好的,让我们开始吧。
00:04:37请稍等片刻。
00:04:50好的,太棒了。
00:04:54这是一个非常简单的应用程序。
00:04:56它受到了一个电子商务平台的启发。
00:04:59让我在这里进行初步演示。
00:05:01我可以加载这个页面。
00:05:03我有一些内容,比如这个特色产品。
00:05:06我有特色类别,不同的产品数据。
00:05:09这里还有一个“浏览所有”页面,我可以在这里看到平台上的所有产品并进行分页。
00:05:20然后我们这里有一个“关于”页面,它是纯静态的。
00:05:24我也可以以用户身份登录。
00:05:27这将把我登录到我的用户。
00:05:30然后我可以在我的仪表盘上获得个性化内容。
00:05:33例如,推荐产品或个性化折扣。
00:05:38所以请注意,这里有一个很好的组合。
00:05:42哦,还有一个页面我忘了给你们看。
00:05:45产品页面,最重要的一个。
00:05:47在这里,我们也可以看到产品信息,如果喜欢,可以为我们的用户保存。
00:05:52所以请注意,由于我们所有依赖用户的功能,这个应用程序中静态和动态内容混合得非常好。
00:05:59我们再来看看代码,它在这里。
00:06:05我当然在 Next.js 16 中使用了 App Router。
00:06:08我有所有不同的页面,比如“关于”页面、“所有”页面、我们的产品页面。
00:06:13我还在使用特性切片来保持我的 app 文件夹整洁。
00:06:17我有一些不同的组件和查询,通过 Prisma 与我的数据库通信。
00:06:23所以,我故意放慢了所有这些速度。
00:06:25这就是为什么我们有这么长的加载阶段,只是为了我们能更容易地看到发生了什么。
00:06:31所以我们想要解决的常见问题,也就是我们在这个应用程序中实际存在的问题,是 prop 钻取,这使得维护和重构功能变得困难,过多的客户端 JavaScript,以及缺乏静态渲染导致额外的服务器成本和性能下降。
00:06:47所以这次演示的目标基本上就是通过一些关于组合、
00:06:51缓存和架构的智能模式来改进这个应用程序,以解决这些常见问题,使其更快、
00:06:58更具可扩展性且更易于维护。
00:07:01那么,让我们开始吧。
00:07:02我们要解决的第一个问题实际上与 prop 钻取有关。
00:07:05它就在页面这里。
00:07:10请注意,我在顶部有一个 loggedIn 变量。
00:07:15你可以看到我把它传递给了几个组件。
00:07:17它实际上已经通过多层传递到了这个个性化横幅中。
00:07:20所以这会使在这里重用东西变得困难,因为我们的欢迎横幅总是依赖于这个 loggedIn。
00:07:28对于服务器组件,最佳实践实际上是将数据获取下推到使用它的组件中,并在树的更深层解析 Promise。
00:07:37为了使其获得身份验证,只要它使用 fetch 或 React cache 之类的东西,我们就可以复制多次调用,并且可以在组件内部的任何地方重用它。
00:07:48所以重用它是完全没问题的。
00:07:51所以现在我们可以把它移到个性化部分。
00:07:54我们不再需要这个 prop 了。
00:07:57直接把它放进去——哎呀——放这里。
00:08:01我们不再需要传递这个了。
00:08:04既然我们现在将这个异步调用移到了个性化部分,我们就不再阻塞页面了。
00:08:09我们可以继续使用一个简单的 Suspense 来暂停它。
00:08:13我们不需要这个 fallback。
00:08:16至于欢迎横幅,我想我们也会做同样的事情。
00:08:22但是尝试在这里使用——获取 loggedIn 变量或值,它不起作用,对吧?
00:08:27因为这是一个客户端组件。
00:08:29所以我们需要用不同的方式解决这个问题。
00:08:30我们将使用一个非常巧妙的模式来解决这个问题。
00:08:33我们实际上将进入布局,并用一个 auth provider 包装所有内容。
00:08:39所以我要把这个放在我的整个应用程序周围,并在这里获取这个 loggedIn 变量。
00:08:45我绝对不想阻塞我的整个根布局。
00:08:48让我们继续删除这里的 await。
00:08:50然后将这个作为 Promise 传递给这个 auth provider。
00:08:55这个可以只包含那个 Promise。
00:08:57它可以在那里“闲置”,直到我们准备好读取它。
00:09:01所以现在我们已经设置好了。
00:09:03这意味着我们实际上可以继续,首先,摆脱这个 prop。
00:09:09我们将摆脱这个向下钻取到个性化横幅的 prop。
00:09:12我们也将摆脱这里的 prop 钻取或签名。
00:09:16现在我们可以使用这个 auth provider,在个性化横幅内部使用我们刚刚创建的 useAuth 来本地获取这个 loggedIn 值。
00:09:26并用 use 读取它。
00:09:28所以这实际上会以一种我们需要在它解析时暂停它的方式工作。
00:09:33所以现在我将那个小的数据获取共同定位在个性化横幅内部。
00:09:37我不需要传递那些 props。
00:09:40当它正在解析时,让我们也暂停这个,并带有一个 fallback。
00:09:44让我们在这里只做一个通用横幅,以避免任何奇怪的累积布局偏移。
00:09:51最后,也摆脱这个。
00:09:53所以现在这个欢迎横幅是可组合的。
00:09:58它是可重用的。
00:09:59我们在主页上没有任何奇怪的 props 或依赖项。
00:10:02既然我们能够如此轻松地重用它,让我们实际上也把它添加到这个浏览页面,它会在这里。
00:10:11我可以直接在这里使用它,没有任何依赖项。
00:10:15通过这些模式,我们能够利用 React cache 和 React use 保持良好的组件架构,并使我们的组件更具可用性和可组合性。
00:10:30好的。
00:10:31让我们解决下一个常见的挑战,即过多的客户端 JavaScript 和具有多重职责的大型组件。
00:10:40实际上,那也在“所有”页面这里。
00:10:43我们又要处理这个欢迎横幅了。
00:10:46它目前是一个客户端组件。
00:10:48它是一个客户端组件的原因是我有一个非常简单的“关闭”状态。
00:10:53我只需点击它。
00:10:54这是一个很好的 UI 交互。
00:10:56这没问题。
00:10:57但不太好的是,正因为如此,我将整个组件转换成了一个客户端组件。
00:11:04我甚至使用 swr 进行客户端获取。
00:11:07我现在有了这个 API 层。
00:11:08我的数据不再有类型安全。
00:11:11是的,这没有必要。
00:11:12而且我们还打破了关注点分离,因为我们把 UI 逻辑和数据混在一起了。
00:11:18所以让我们使用另一个巧妙的模式来解决这个问题。
00:11:21它被称为“甜甜圈模式”。
00:11:23基本上,我要做的是将其提取到一个客户端包装器中。
00:11:27所以让我们在这里创建一个新组件。
00:11:29我们称之为 banner container。
00:11:32它将包含我们带有 use client 指令的交互逻辑。
00:11:37我们可以创建签名。
00:11:38我们可以粘贴我们刚才所有的内容。
00:11:42我将在这里插入一个 prop,它将是 children,而不是使用这些横幅。
00:11:48这就是为什么它被称为甜甜圈模式。
00:11:50我们只是围绕服务器渲染内容创建这个包装器 UI 逻辑,或者它可以是服务器渲染内容。
00:11:56然后,由于我们不再有这个客户端依赖,我们可以继续删除 use client。
00:12:01我们可以改用我们的 isAuth 异步函数。
00:12:06我们可以把它变成一个异步服务器组件。
00:12:09我们甚至可以用服务器端获取替换客户端获取。
00:12:11所以让我继续直接在这里获取折扣数据。
00:12:16折扣数据。
00:12:18并像以前一样利用我们常规的思维模型,带有类型安全。
00:12:24这意味着我也可以删除这个我不想使用的 API 层。
00:12:29最后,对于 isLoading,我们可以在这里导出一个新的欢迎横幅,其中包含我们的甜甜圈模式横幅容器,其中包含服务器渲染内容。
00:12:38这意味着我们不再需要这个 isLoading。
00:12:40所以我们基本上把整个东西重构成了服务器组件,并提取了一个 UI 逻辑点。
00:12:46但那是什么?
00:12:48我好像又有一个错误。
00:12:51这实际上是因为 Motion。
00:12:53使用 Motion。
00:12:54它是一个非常棒的动画库,但它需要 useClient 指令。
00:12:59同样,我们不必仅仅为了动画就使用 useClient。
00:13:03我们可以再次创建一个甜甜圈模式包装器,并为这些动画提取包装器。
00:13:10这意味着我们不必将这里的任何东西转换为客户端。
00:13:14我可能在这里漏掉了一些东西。
00:13:17是的。
00:13:18好了。
00:13:21所以现在这里的一切都已转换为服务器。
00:13:23我们有相同的交互。
00:13:24我们仍然有我们的交互逻辑,但现在我们有一种方式来获取数据。
00:13:29我们的客户端 JS 少了很多。
00:13:31实际上,我正在为这个 UI 边界辅助工具使用这种甜甜圈模式,它看起来像这样。
00:13:42你看到了吗?
00:13:43所以这再次展示了我的意思,对吧?
00:13:45使用甜甜圈模式,我们有一个客户端组件围绕着一个服务器组件。
00:13:49我还用这个 UI 辅助工具标记了我的许多其他组件。
00:13:53这里也有更多的服务器组件。
00:13:56既然我们现在已经很擅长了,让我们也来改进它们。
00:14:01它们在页脚。
00:14:04这些类别——我的意思是,我有一个很好的组件,它获取自己的数据。
00:14:08我只是想添加这个 showMore 功能,以防它变得非常长。
00:14:14使用甜甜圈模式,我可以在这里包装一个 showMore 组件。
00:14:20这将包含我的 UI 逻辑。
00:14:23它看起来像这样,对吧?
00:14:27非常酷。
00:14:28现在它包含了客户端逻辑,允许我们使用状态。
00:14:33我们正在使用 children count 和 to array 来切片。
00:14:36这里最酷的是,这两个现在是完全可组合、可重用的组件,它们像这样协同工作。
00:14:42所以这真的是我们在这里学习的这些模式的美妙之处。
00:14:45你可以用它做任何事情。
00:14:50我也把它用在了这个模态框上。
00:14:52是的,下次当你考虑向服务器组件添加任何客户端逻辑时,请记住这一点。
00:14:59好的,我们知道甜甜圈模式。
00:15:01我们知道如何利用它来创建这些可组合组件并避免客户端 JavaScript,所以我们可以继续解决最后一个问题。
00:15:10让我再关闭这个。
00:15:13所以那将是缺乏静态渲染策略的问题,对吧?
00:15:18看看我的构建输出,我实际上每个页面都是动态页面。
00:15:24这意味着每当我在这里加载某些内容时,它都会为每个用户运行。
00:15:29抱歉。
00:15:30每个打开它的用户都会看到这个加载状态。
00:15:33这会浪费服务器成本,降低性能。
00:15:37这也意味着我的页面中有什么东西导致了动态渲染,或者强制所有页面进行动态渲染。
00:15:45实际上,它在我的根布局中。
00:15:49我不知道你有没有遇到过这种情况。
00:15:51它在这里。
00:15:53在我的 header 中,我有一个用户资料。
00:15:57这当然是使用 cookie 来获取当前用户,这意味着其他所有内容也都是动态渲染的。
00:16:02因为页面要么是动态的,要么是静态的,对吧?
00:16:06这是一个相当常见的问题,在 Next.js 的早期版本中也曾解决过,所以让我们看看我们能做些什么。
00:16:13我们可以做的一件事是创建一个路由组,并将我们的应用程序分成静态和动态部分,这将允许我提取我的“关于”页面。
00:16:23我可以静态渲染它。
00:16:25这对于某些应用程序来说没问题,但在我的情况下,重要的页面是产品页面,它仍然是动态的,所以帮助不大。
00:16:33这种策略怎么样?
00:16:35我在这里创建了一个请求上下文参数,将某个状态编码到我的 URL 中,然后我可以使用 generate static params 来生成我的页面的所有不同变体。
00:16:46这实际上,结合客户端获取用户数据,将允许我在产品页面上获得缓存。
00:16:54这绝对是一种可行的模式。
00:16:55Vercel Flags SDK 推荐它,我记得它被称为预计算模式。
00:17:00但这真的很复杂,而且我有多种获取数据的方式。
00:17:04实际上,我不想把我的整个应用程序重写成这样。
00:17:07那么,如果我们不必做任何这些变通方法呢?
00:17:10如果有一种更简单的方法呢?
00:17:12嗯,有的。
00:17:14让我们再次回到我们的应用程序。
00:17:17所以我们实际上可以进入 next config 并启用缓存组件。
00:17:23哦,不错。
00:17:25好的,正如你从主题演讲中了解到的,这将使我们所有的异步调用都选择请求时或动态。
00:17:34当我们的异步调用未暂停时,它还会给我们错误,并且它会给我们这个 use cache 指令,我们可以用它来精细地缓存页面、
00:17:47函数或组件。
00:17:48所以,是的,让我们继续利用这一点。
00:17:51我们可以从主页开始。
00:17:55让我们看看。
00:17:56所以,我再次混合了静态和动态内容。
00:17:59我有我的欢迎横幅,还有一些为你准备的。
00:18:03让我们再用这个 UI 辅助工具看看。
00:18:06例如,横幅是动态渲染的。
00:18:10而我将这个标记为混合渲染,因为 hero 正在获取这个异步内容,而且速度很慢。
00:18:18但它不依赖于任何用户数据或动态 API。
00:18:21这意味着这里所有混合渲染的内容实际上都可以在请求之间和用户之间重用。
00:18:27我们可以在它上面使用 use cache 指令。
00:18:30所以让我们在这里添加 use cache 指令,并将其标记为已缓存。
00:18:35这将允许我——每当我重新加载这个页面时——我没有保存它。
00:18:43好了。
00:18:44它不会重新加载这部分,因为它已缓存。
00:18:47它现在是静态的,对吧?
00:18:49还有其他相关的 API,比如 cache tag,允许我精细地标记或验证特定的缓存条目,或者定义我的重新验证周期。
00:19:01但对于这个演示,我们只关注普通的指令。
00:19:05现在我有了这个 use cache 指令,我实际上可以删除 hero 周围的 suspense 边界。
00:19:10这意味着——嗯,这将使部分预渲染能够将其包含在静态预渲染外壳中,这样 hero 在这种情况下将成为我构建输出的一部分。
00:19:23让我们对页面上所有可以共享的其他内容做同样的事情。
00:19:28例如,我这里有特色类别。
00:19:31让我们也这样做。
00:19:33并添加 use cache 指令并将其标记为已缓存。
00:19:37哎呀。
00:19:39然后我们可以删除 suspense 边界。
00:19:40我们不再需要它了。
00:19:43特色产品也是如此。
00:19:44让我们添加 use cache 并将其标记为已缓存。
00:19:48哎呀。
00:19:50然后删除 suspense 边界。
00:19:52所以请注意,我在这里能够消除多少复杂性。
00:19:55我不再需要担心我的骨架屏,我之前做的累积布局偏移。
00:20:00而且页面不再——或者我们不再有页面级别的静态与动态限制。
00:20:07所以现在当我加载这个页面时,你会看到这里的一切都被缓存了,除了那些真正用户特定的内容。
00:20:16对。
00:20:18所以这很酷。
00:20:19让我们去浏览页面,在那里做同样的事情。
00:20:24是的。
00:20:25我已经在这里标记了所有的边界,这样你就能很容易理解发生了什么。
00:20:29我至少想缓存这些类别。
00:20:33不过,我好像遇到了一个错误。
00:20:37也许你认出了这个。
00:20:38这意味着我有一个阻塞路由。
00:20:40而且我没有在使用 suspense 边界,而我应该使用。
00:20:43刷新这个,是真的,是吧?
00:20:46这真的很慢。
00:20:47它导致了性能问题和糟糕的用户体验。
00:20:50所以这很棒。
00:20:51Use cache 或缓存组件正在帮助我识别我的阻塞路由。
00:20:55让我们看看里面发生了什么。
00:20:57所以这就是问题所在,对吧?
00:20:59我正在顶层获取这些类别,而且它上面没有任何 suspense 边界。
00:21:03基本上,我们需要做出选择。
00:21:05要么我们在上面添加一个 suspense 边界,要么我们选择缓存。
00:21:09让我们先做简单的事情,只在这里添加一个 loading TSX。
00:21:12让我们在这里添加一个加载页面,一些漂亮的骨架 UI。
00:21:21这很不错。
00:21:21它解决了错误,但我在等待时页面上没有任何有用的东西。
00:21:25我甚至无法搜索。
00:21:27所以对于缓存组件,动态就像——或者静态与动态就像一个天平。
00:21:33由我们来决定页面中需要多少静态内容。
00:21:37所以让我们把这个页面更多地转向静态,然后再次删除这个 loading TSX。
00:21:43然后利用我们之前学到的模式,将这个数据获取推送到组件中,并与 UI 共同定位。
00:21:50所以把它移到我的响应式类别过滤器中。
00:21:54我有两个,因为响应式设计。
00:21:57我实际上可以直接把它加在这里。
00:22:01哎呀。
00:22:03并导入这个。
00:22:05我不再需要这个 prompt 了。
00:22:06实际上,我的组件变得更具可组合性。
00:22:09与其暂停它,不如直接添加 use cache 指令。
00:22:14这应该就足够了。
00:22:16所以请注意,我被迫更多地思考我在哪里解析我的 Promise,并通过这种方式实际上改进了我的组件架构。
00:22:24我不需要暂停它。
00:22:25这将被包含在静态外壳中。
00:22:28产品列表,让我保持新鲜。
00:22:35这样我每次都可以重新加载它。
00:22:37而底部的类别,我也想缓存它。
00:22:41所以让我们继续进入页脚。
00:22:44由于我在这里使用了甜甜圈模式,即使它在这个交互式 UI 部分内部,它实际上也可以被缓存。
00:22:54所以这完全没问题。
00:22:55所以这种模式不仅对组合有好处,对缓存也有好处。
00:22:58我想我那里还有一个错误。
00:23:03让我们看看那是什么。
00:23:04仍然有这个错误。
00:23:08这实际上是因为这些搜索框。
00:23:10搜索框,我们知道,是一个动态 API。
00:23:12我不能缓存它。
00:23:13但我可以将其解析得更深,以显示更多我的 UI 并使其静态化。
00:23:18所以让我们继续将其下移,作为 Promise 传递给产品列表。
00:23:24我们将其类型化为 Promise,像这样。
00:23:30让我们在产品列表内部解析它,使用已解析的搜索参数,在这里和这里。
00:23:36由于它在这里被暂停了,错误就会消失。
00:23:40所以重新加载这个,这里唯一重新加载的只是我特意选择动态的部分。
00:23:48其他一切都可以缓存。
00:23:49这意味着我可以与我的横幅互动,甚至搜索,因为那部分已经预渲染了。
00:23:57好的,让我们做最后一个页面,也就是产品页面,这是最困难也是最重要的一个。
00:24:05现在真的很糟糕。
00:24:08这对于一个电子商务平台来说非常重要,显然。
00:24:11好的,让我们也来修复那个。
00:24:15所以这里我有这个产品页面。
00:24:18让我们开始缓存这里可重用的内容,例如产品本身。
00:24:23只需在这里添加 use cache 并将其标记为已缓存。
00:24:27这应该没问题。
00:24:28这意味着我们可以删除这里的 suspense 边界。
00:24:33好的,这不再在每次请求时重新加载了,对吧?
00:24:38对于产品详情,我们做同样的事情。
00:24:40让我们添加 use cache。
00:24:41让我们将其标记为已缓存,看看是否也能奏效。
00:24:47没有。
00:24:48实际上,这是一个不同的错误。
00:24:50它告诉我我正在尝试在这个缓存片段中使用动态 API。
00:24:54这是真的。
00:24:54我正在使用“保存产品”按钮,对吧?
00:24:56它允许我点击并切换保存状态。
00:25:00那么你认为我们可以用这个做什么呢?
00:25:03我们可以再次使用甜甜圈模式。
00:25:06实际上,我们也可以将动态片段插入到缓存片段中。
00:25:10所以我们像以前一样交错它们,但这次是带有缓存的。
00:25:12所以这很酷。
00:25:14让我们继续在这里添加 children,像这样。
00:25:19这将消除错误。
00:25:21我只需将其包装在我页面中这个动态片段周围,删除 suspense 边界,然后为页面中那个动态部分添加一个非常小的书签 UI。
00:25:34让我们看看现在它看起来怎么样。
00:25:40所以请注意,几乎整个 UI 都可用,但我有一个小的动态部分,这没关系。
00:25:47其他一切都还在。
00:25:48让我们让评论保持动态,因为我们可以保持它们新鲜。
00:25:53还有一个错误。
00:25:54让我们快速解决它。
00:25:56同样,这是 params。
00:25:58我得到了帮助,我需要做出选择,要么添加一个加载 fallback,要么缓存它。
00:26:04在这种情况下,我们只使用 generate static params。
00:26:07这取决于你的用例和数据集。
00:26:10但对于这种情况,我将添加几个预渲染的预定义页面,然后只缓存用户生成时其余的页面。
00:26:17这将消除我的错误。
00:26:20所以我想我实际上已经完成了重构。
00:26:22让我们看看部署后的版本是什么样子。
00:26:26所以我刚刚在 Vercel 上部署了它。
00:26:27请记住,我故意放慢了很多数据获取的速度。
00:26:35然而,当我最初加载这个页面时,一切都已可用。
00:26:40这里唯一的例外就是那几个动态片段,比如折扣和为你推荐。
00:26:46浏览所有页面也是如此。
00:26:47所有的 UI 都已经可用。
00:26:50对于产品本身,它感觉就像是即时加载的。
00:26:54再次提醒,所有这些缓存片段都将通过部分预渲染包含在静态外壳中。
00:27:00并且可以使用新的 Next 16 客户端路由器中改进的预取功能进行预取。
00:27:05这意味着每次导航都感觉如此之快,对吧?
00:27:09好的,总结一下,有了缓存组件,不再有静态与动态之分。
00:27:17我们不需要避免动态 API 或牺牲动态内容。
00:27:28我们可以跳过这些复杂的技巧和变通方法,只为了那一次缓存命中而使用多种数据获取策略,正如我所展示的。
00:27:37因此,在现代 Next.js 中,动态与静态是一个权衡。
00:27:40我们决定在应用程序中需要多少静态内容。
00:27:43只要我们遵循某些模式,我们就可以拥有一个默认情况下高性能、可组合且可扩展的思维模型。
00:27:50那么,让我们回到幻灯片。
00:27:53如果你还没有被它的速度所震撼,这就是 Lighthouse 的得分。
00:27:56我通过 Vercel Speed Insights 收集了一些现场数据。
00:28:00所以我们在所有最重要的页面,主页、产品页面和产品列表上都获得了 100 分,即使它们是高度动态的。
00:28:08所以最后总结一下,这些模式将确保 Next.js 应用程序的可扩展性和性能,并使我们能够利用最新的创新并获得这样的分数。
00:28:18首先,我们可以通过在组件树深处解析 Promise 并使用 React Cache 在组件内部本地获取数据来重复工作,从而优化我们的架构。
00:28:28我们可以通过结合 React Use 的上下文提供者来避免向客户端组件传递过多的 props。
00:28:35其次,我们可以使用甜甜圈模式组合服务客户端组件,以减少客户端 JavaScript,保持清晰的关注点分离,并允许组件重用。
00:28:43这种模式将进一步使我们能够稍后缓存我们组合的服务器组件。
00:28:50最后,我们可以使用 use cache 按页面、
00:28:53组件或函数进行缓存和预渲染,以消除冗余处理,提高性能和 SEO,并让部分预渲染静态渲染应用程序的这些片段。
00:29:01如果我们的内容确实是动态的,我们可以使用适当的加载 fallback 暂停它。
00:29:07请记住,所有这些都是相互关联的。
00:29:09所以你的架构越好,就越容易组合,也就越容易缓存和预渲染,并获得最佳结果。
00:29:15例如,在树的深处解析动态 API 将允许你创建一个更大的部分预渲染静态外壳。
00:29:22有了这个,这就是应用程序完成版本的仓库。
00:29:25里面有很多我甚至没有展示的东西,你可以去看看。
00:29:29你可以扫描二维码找到我的社交媒体,以及仓库,如果你不想自己拍照并输入的话。
00:29:36好的,我的分享就到这里。
00:29:37感谢 Next.js Conf 邀请我来这里。
00:29:39[音乐播放]

Key Takeaway

现代Next.js通过`use cache`指令、数据下推和“甜甜圈模式”等设计模式,实现了静态与动态内容的灵活组合与精细缓存,从而构建出高性能、可扩展且易于维护的应用程序。

Highlights

Next.js 16引入`use cache`指令,允许页面同时拥有静态和动态内容,实现可组合缓存,打破了传统静态与动态渲染的二元限制。

通过将数据获取下推到组件内部并使用`React.cache`,优化服务器组件架构,有效避免了Prop Drilling,提高了组件的可用性和可组合性。

利用“甜甜圈模式”将交互逻辑封装在客户端组件中,同时保持核心内容为服务器组件,从而减少了客户端JavaScript并提高了组件复用性。

`use cache`指令支持按页面、组件或函数进行精细缓存和预渲染,显著提升了应用程序的性能和SEO。

通过部分预渲染和Next.js 16客户端路由器的预取功能,实现了高度动态应用的即时加载体验和Lighthouse 100分的优异性能。

强调架构、组合和缓存模式之间的相互关联性,良好的架构是实现高性能和可扩展性的基础,并能最大化静态外壳的利用。

Timeline

Next.js中的静态与动态渲染

演讲者首先介绍了Next.js App Router中静态渲染和动态渲染的核心概念。静态渲染通过预渲染内容实现更快的网站加载、减少服务器负载和优化SEO,适用于不常更新的内容。动态渲染则支持实时或频繁更新的数据,提供个性化内容,如用户仪表盘。演讲者详细列举了导致动态渲染的API,如`params`、`cookies`、`headers`和带有`no-store`配置的`fetch`,并指出在旧版Next.js中,一个动态API会使整个页面动态化。这种限制导致在混合内容页面中出现冗余处理和性能问题。

`use cache`指令:实现可组合缓存

为了解决静态与动态渲染的限制,Next.js 16引入了`use cache`指令,允许页面同时具备静态和动态特性。该指令让开发者能够明确选择缓存策略,不再需要Next.js猜测页面类型。通过`use cache`,页面、React组件或函数可以被标记为可缓存,例如“特色产品”组件,即使在动态页面上也能被预渲染。这些缓存片段可以作为静态外壳的一部分进行部分预渲染,确保内容在页面加载时即刻可用,无需流式传输,从而提升用户体验。

常见代码问题及演示应用概览

演讲者指出了Next.js应用中常见的代码库问题,包括深度Prop Drilling、冗余的客户端JavaScript、职责过重的大型组件以及缺乏静态渲染导致的额外服务器成本和性能下降。随后,他展示了一个基于Next.js 16 App Router构建的电子商务演示应用,该应用故意放慢了数据获取速度,以更好地展示待解决的问题。应用包含了主页、浏览页、关于页、产品页和用户仪表盘等,混合了静态和动态内容。本次演示的目标是通过智能的组合、缓存和架构模式来改进这个应用,使其更快、更具可扩展性且更易于维护。

数据下推与`React.use`解决Prop Drilling

针对Prop Drilling问题,演讲者演示了如何优化服务器组件架构。最佳实践是将数据获取下推到实际使用数据的组件内部,并在组件树更深层解析Promise。对于身份验证状态(如`loggedIn`),可以在`PersonalizedBanner`内部本地获取。对于客户端组件,则通过在根布局中创建一个`AuthProvider`来包装整个应用,并将`loggedIn`的Promise传递给它,然后客户端组件可以使用`useAuth` hook和`React.use`来本地读取该值。这种模式消除了Prop Drilling,使组件更具可用性和可组合性,例如`WelcomeBanner`可以轻松地在不同页面重用。

“甜甜圈模式”优化客户端JavaScript

为了解决客户端JavaScript过多和大型组件职责不清的问题,演讲者介绍了“甜甜圈模式”。该模式的核心思想是将客户端交互逻辑(如关闭状态、动画)提取到一个客户端包装器组件中,而将核心的服务器渲染内容作为`children`传递给这个包装器。通过这种方式,原本因少量交互而被迫成为客户端组件的整个部分,现在可以作为服务器组件存在,只将必要的交互逻辑隔离到小型客户端组件中。这不仅减少了客户端JavaScript的加载量,保持了关注点分离,还提高了组件的复用性和缓存能力,例如在页脚的类别显示和动画效果中应用此模式。

`use cache`实现精细化静态渲染策略

演讲者指出,由于根布局中的用户资料组件使用了`cookies`,导致整个应用默认动态渲染,带来了额外的服务器成本和性能问题。他展示了Next.js 16中启用`cachedComponents`配置后,如何利用`use cache`指令对页面、组件或函数进行精细缓存。通过将`use cache`应用于主页上不依赖用户数据的组件(如`Hero`、`FeaturedCategories`),并移除其Suspense边界,这些内容可以被部分预渲染并包含在静态外壳中。对于“浏览所有”页面中的阻塞路由和动态搜索参数,通过将数据获取下推到组件内部并使用Suspense或`generate static params`来解决。在产品页面,即使存在动态交互(如“保存产品”按钮),也可以通过“甜甜圈模式”将动态部分包裹起来,同时缓存大部分静态产品信息,从而实现高度优化的混合渲染。

部署成果、性能优化与模式总结

经过优化后的应用部署到Vercel后,即使在数据获取被故意放慢的情况下,初始页面加载也变得即时,只有少数用户特定的动态片段会重新加载。所有缓存片段都通过部分预渲染包含在静态外壳中,并通过Next.js 16客户端路由器的预取功能实现快速导航。演讲者强调,`use cache`消除了静态与动态的二元对立,允许开发者根据需求平衡静态和动态内容。最终,优化后的应用在Lighthouse测试中获得了主页、产品页和产品列表页的100分。他总结了三种关键模式:优化架构(数据下推、`React.cache`)、组件组合(甜甜圈模式)和精细缓存(`use cache`),并强调它们相互关联,共同确保Next.js应用的可扩展性和高性能。

Community Posts

View all posts