Prelude 序言
In my previous blog post, How I Built My Portfolio, I discussed the technologies I used to create my portfolio site. While discussing Next.js, I gave a very high-level explanation of what partial pre-rendering is.
在我的上一篇博客文章《我是如何构建我的投资组合》中,我讨论了创建我的投资组合网站所使用的科技。在讨论 Next.js 时,我给出了关于部分预渲染的非常高级的解释。
In this post, I will dive deep into how partial pre-rendering works. I will also share my thoughts on whether you should consider using it or if it's just another hype train in the JavaScript ecosystem.
在这篇文章中,我将深入探讨部分预渲染的工作原理。我还会分享我的观点,关于你是否应该考虑使用它,或者它是否只是 JavaScript 生态系统中的另一场炒作。
Rendering Strategies 渲染策略
Over the years, innovations in web development have led to different strategies for delivering content on the internet. Let's quickly review these strategies:
多年来,网络开发领域的创新导致了在互联网上传递内容的不同策略。让我们快速回顾这些策略:
- Static Content 静态内容
- Server-Side Rendering (SSR)
服务器端渲染(SSR) - Client-Side Rendering (CSR)
客户端渲染(CSR) - Static Site Generation (SSG)
静态站点生成(SSG)
We'll go over each of them briefly to ensure we have a common understanding and avoid confusion.
我们将简要地过一遍每一项,以确保我们有一个共同的理解,避免混淆。
Static Content 静态内容
As the web started to become a mainstream platform for consuming information, people began sharing information by writing HTML documents. All the information was written as static markup and published to the internet. Each time something new needed to be shared, we would add another HTML document, and everything worked seamlessly.
随着网络开始成为消费信息的主流平台,人们开始通过编写 HTML 文档来分享信息。所有信息都以静态标记的形式编写并发布到互联网上。每次需要分享新内容时,我们就会添加另一个 HTML 文档,一切工作都无缝进行。
Server Side Rendering 服务器端渲染
Static content served its purpose, but it became challenging as web applications grew more dynamic. Consider a product details page as an example: the layout, styling, and markup remain the same across different products, with only the actual content varying. If you have 10 products to display, you'd need to duplicate the same markup with different content 10 times. Just copy-pasting seems easy at first, but imagine maintaining this for hundreds of pages or making design changes—it quickly becomes a daunting task.
静态内容已经完成了它的使命,但随着网络应用的动态性增强,它变得具有挑战性。以产品详情页为例:布局、样式和标记在不同产品中保持不变,只有实际内容有所不同。如果你要展示 10 个产品,你需要将相同的标记与不同的内容重复 10 次。一开始复制粘贴似乎很简单,但想象一下维护数百页或进行设计更改——这很快就会变成一项艰巨的任务。
Server Side Rendering (SSR) addresses these challenges by having a server handle the rendering. With SSR, your content resides in a database, and when a user requests the details page of a product, the server retrieves the content and generates a new HTML document using templates. This approach allows for dynamic content generation on the server before sending it to the client, providing a personalized and efficient user experience.
服务器端渲染(SSR)通过让服务器处理渲染来应对这些挑战。使用 SSR,您的内容存储在数据库中,当用户请求产品的详情页面时,服务器检索内容并使用模板生成新的 HTML 文档。这种方法允许在发送到客户端之前在服务器上动态生成内容,提供个性化的高效用户体验。
Client Side Rendering 客户端渲染
Okay, SSR solved the problems we had, right? Then why did front-end frameworks like React, Angular, etc., become so popular and push the entire developer ecosystem to build websites rendered by the browser instead of a server? The answer is rich interactivity. With Client Side Rendering (CSR), we can provide instantaneous feedback to the user, thereby offering a native experience. In this approach, JavaScript on the client is responsible for constructing the entire web page from a blank HTML document with some script tags to load the actual JS. Whenever the page requires more data, an API call can be made to a backend hosted independently, which provides the required data in a format agreed upon by the client and server (e.g., JSON).
好的,SSR 解决了我们遇到的问题,对吧?那么为什么像 React、Angular 这样的前端框架会如此流行,推动整个开发者生态系统构建由浏览器渲染的网站而不是服务器端渲染的网站呢?答案是丰富的交互性。通过客户端渲染(CSR),我们可以为用户提供即时反馈,从而提供原生体验。在这种方法中,客户端的 JavaScript 负责从空白 HTML 文档和一些脚本标签构建整个网页,以加载实际的 JS。每当页面需要更多数据时,可以独立托管的后端进行 API 调用,以客户端和服务器商定的格式(例如 JSON)提供所需数据。
The Problem with CSR
企业社会责任的问题
-
Poor SEO: Since the entire web page is constructed by the client, there will be poor SEO as crawlers cannot execute JS. Additionally, when the page loads, there is no meaningful content, which severely affects the indexing of web pages by search engines.
糟糕的 SEO:由于整个网页是由客户构建的,因此 SEO 会很差,因为爬虫无法执行 JS。此外,当页面加载时,没有有意义的内容,这严重影响了搜索引擎对网页的索引。People argue that crawlers can now execute JS and index CSR'ed websites, but I still believe an HTML document with full meaningful content will always have the upper hand.
人们争论说爬虫现在可以执行 JS 并索引 CSR'ed 网站,但我仍然相信一个包含完整有意义内容的 HTML 文档将始终占据优势。 -
The Loading Spinners Hell: The most irritating problem I call "The Loading Spinners Hell." Let's break this down in detail because this is interesting:
加载旋转地狱:我称之为“加载旋转地狱”的最令人烦恼的问题。让我们详细分析一下,因为这很有趣:- Step 1: Your HTML document loads (1)
第一步:您的 HTML 文档加载(1) - Step 2: JS starts to render the page.
第二步:JavaScript 开始渲染页面。 - Step 3: Oh wait, I need more data to render this part of the page; I'll get it from the API. Meanwhile... (2)
第 3 步:哦,等等,我需要更多数据来渲染这个页面部分;我会从 API 获取它。同时...(2) - Step 4: I got the data; let's continue rendering. Oh wait, I need more data to finish rendering this part; I'll get it from the API. Meanwhile... (3)
步骤 4:我已经获取了数据;让我们继续渲染。哦,等等,我需要更多数据来完成这部分渲染;我会从 API 获取。同时...(3) - Step 5: Repeat Step 4 another 3-4 times.
步骤 5:重复步骤 4 3-4 次。 - Step 6: Finally, my UI is ready to interact!
第 6 步:最后,我的 UI 准备交互了!
Trust me, I am not joking. This happens in real production applications used by real users, and I have experienced this myself. Unfortunately, we cannot avoid this as our requirements grow larger and more complex.
相信我,我并没有开玩笑。这种情况确实发生在真实的生产应用中,这些应用被真实用户使用,我自己也经历过。不幸的是,随着我们的需求变得更大和更复杂,我们无法避免这种情况。I don’t know if you are getting irritated or not, but for me, seeing those three spinners at a time makes my head spin!!!
我不知道你是否感到烦躁,但对我来说,一次看到这三个旋转器让我头晕目眩!!! - Step 1: Your HTML document loads (1)
Static Site Generation 静态网站生成
Static Site Generation (SSG) is an advanced version of static content. With SSG, you can write the markup more intuitively using markdown or React (with Next.js). At build time, your custom code is transformed into plain HTML, CSS, and JS, which can be deployed to a CDN. This approach ensures fast initial page loads since all the content is already present, resulting in excellent SEO and good core web vitals. The downside is that there won't be any dynamic content. This strategy is extremely useful for writing articles, documentation, etc.
静态站点生成(SSG)是静态内容的进阶版本。使用 SSG,您可以使用 Markdown 或 React(Next.js)更直观地编写标记。在构建时,您的自定义代码会被转换成纯 HTML、CSS 和 JS,可以部署到 CDN。这种方法确保了快速的初始页面加载,因为所有内容都已存在,从而实现了优秀的 SEO 和良好的核心 Web 指标。缺点是不会有任何动态内容。这种策略对于撰写文章、文档等非常有用。
Now that we have a solid understanding of how different web rendering strategies work, let's address the elephant in the room "Partial Pre-Rendering".
现在我们已经对不同的网页渲染策略有了稳固的理解,让我们来谈谈房间里的大象“部分预渲染”。
Partial Pre-Rendering 部分预渲染
Partial Pre-Rendering is a feature that allows static portions of a route to be pre-rendered and served from the cache, with dynamic content streamed in, all in a single HTTP request. (The official definition as per Next.js docs)
部分预渲染是一个功能,允许将路由的静态部分预先渲染并从缓存中提供,同时动态内容通过单个 HTTP 请求流式传输。(根据 Next.js 文档中的官方定义)
There are some big words in this definition, so let's break them down step by step.
这里定义中有一些大词,所以我们一步一步来分解它们。
Chapter 1 - The Why
第一章 - 为什么
We have briefly gone through various rendering strategies. Each has its own set of pros and cons, and we can choose any one of them according to our requirements. Trust me, if the right solution is picked based on the project's needs, it will work flawlessly.
我们已经简要地了解了各种渲染策略。每种策略都有其自身的优缺点,我们可以根据需求选择其中任何一种。相信我,如果根据项目需求选择正确的解决方案,它将完美运行。
But why invent another technology and make things more complicated?
但是为什么要发明另一种技术让事情变得更复杂呢?
Because we are humans, and humans are greedy. We want all the pros of all the rendering strategies so that everyone is happy and we can earn millions 🤑🤑
因为我们都是人类,而人类是贪婪的。我们想要所有渲染策略的优点,这样每个人都能开心,我们也能赚得数百万 🤑🤑
Just kidding. But what if there existed an ideal technology that actually combined all the best parts of the techniques we discussed earlier?
只是开玩笑。但如果真的存在一种理想技术,能够结合我们之前讨论过的所有最佳技术呢?
Vercel saw an opportunity here, invested their resources, and I must say they actually did it.
Vercel 看到了这里的机会,投入了他们的资源,我必须说他们实际上做到了。
Yes, Partial Pre-Rendering is exactly that. It combines all the good parts of the rendering strategies we discussed until now.
是的,部分预渲染正是如此。它结合了我们至今讨论的所有渲染策略的优点。
Chapter 2 - The How
第二章 - 如何
Partial Pre-Rendering (PPR) is a new experimental feature introduced in Next.js 14. To understand how PPR works, a basic understanding of React and Next.js is beneficial. However, even if you're new to these technologies, you should still grasp the implementation theory.
部分预渲染(PPR)是 Next.js 14 中引入的一项新实验性功能。要了解 PPR 的工作原理,对 React 和 Next.js 的基本理解很有帮助。然而,即使你对这些技术不熟悉,你也应该掌握实现理论。
Next.js is a meta-framework for React that offers powerful features on top of React, such as server-side rendering, file-based routing, and more.
Next.js 是一个在 React 之上的元框架,它提供了诸如服务器端渲染、基于文件的路由等功能。
With the introduction of the new App Router in Next.js 13, the framework heavily leverages streaming and concurrent features of React.js. The App Router utilizes all the features of server-side React, such as React Server Components (RSCs), Suspense Boundaries, and Streaming.
随着 Next.js 13 中引入新的 App Router,该框架充分利用了 React.js 的流和并发功能。App Router 利用了服务器端 React 的所有功能,例如 React 服务器组件(RSC)、Suspense 边界和流。
Since PPR is still an experimental feature, it is only available in the canary channels. So you need to use next@canary
or next@rc
. (PPR should not be used in production with real users.)
由于 PPR 仍然是一个实验性功能,它仅在 canary 渠道中可用。因此,您需要使用 next@canary
或 next@rc
。 (PPR 不应在生产环境中与真实用户一起使用。)
PPR is built on top of React Server Components and Suspense Boundaries, so there are no new APIs to learn. You use the concepts you are already familiar with.
PPR 是基于 React Server 组件和 Suspense 边界构建的,因此无需学习新的 API。您可以使用您已经熟悉的概念。
When PPR is enabled, all the content is treated as static content, meaning it will be the same for every user. If the part of a page needs to have dynamic content rendered on a per-request basis, that particular component can be wrapped inside a Suspense boundary.
当启用 PPR 时,所有内容都被视为静态内容,这意味着它对每个用户都是相同的。如果页面的某个部分需要根据每个请求动态渲染内容,则可以将该特定组件包裹在 Suspense 边界内。
Let's Understand this with an Example
让我们用一个例子来理解这个
Consider a Course Details Page as shown below.
考虑以下所示的课程详情页面。
Note: This is just an example for illustration purposes and the use case might vary in real scenarios. In this example, I assume that course details don't change very often.
注意:这只是一个示例,用于说明目的,实际场景中的用例可能会有所不同。在此示例中,我假设课程详情变化不频繁。
The corresponding JSX to render this page looks like this:
相应的 JSX 用于渲染此页面的代码如下:
import Image from 'next/image';
export default function Home() {
return (
<div className='flex min-h-[100dvh] flex-col'>
<section className='w-full bg-muted py-12 md:py-24 lg:py-32'>
<div className='container m-auto px-4 md:px-6'>
<div className='grid gap-6 lg:grid-cols-[1fr_400px] lg:gap-12 xl:grid-cols-[1fr_600px]'>
<div className='flex flex-col justify-center space-y-4'>
<div className='space-y-2'>
<h1 className='text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none'>
Mastering React: A Comprehensive Course
</h1>
<p className='max-w-[600px] text-muted-foreground md:text-xl'>
Dive deep into the world of React and become a proficient
frontend developer. This course covers everything from
fundamental concepts to advanced techniques.
</p>
</div>
<button className='bg-primary text-primary-foreground'>
Enroll Now
</button>
</div>
<Image
src='https://generated.vusercontent.net/placeholder.svg'
width='550'
height='550'
alt='Course Hero'
className='mx-auto aspect-video overflow-hidden'
/>
</div>
</div>
</section>
<section className='w-full py-12 md:py-24 lg:py-32'>
<div className='container m-auto px-4 md:px-6'>
<div className='grid gap-12 lg:grid-cols-2'>
<div>
<h2 className='text-3xl font-bold tracking-tighter'>
Course Curriculum
</h2>
<div className='mt-6 space-y-4 text-muted-foreground'>
<div>
<h3 className='text-xl font-bold'>Introduction to React</h3>
<p>
Learn the fundamentals of React, including components,
state, and props.
</p>
</div>
<div>
<h3 className='text-xl font-bold'>Advanced React Concepts</h3>
<p>
Explore advanced topics like hooks, context, and performance
optimization.
</p>
</div>
<div>
<h3 className='text-xl font-bold'>
Building Real-World Apps
</h3>
<p>
Apply your knowledge by building complex, production-ready
applications.
</p>
</div>
</div>
</div>
<div>
<h2 className='text-3xl font-bold tracking-tighter'>
About the Instructor
</h2>
<div className='mt-6 space-y-4 text-muted-foreground'>
<div className='flex items-center gap-4'>
<div className='flex h-16 w-16 items-center justify-center rounded-full border'>
<Image
src='https://generated.vusercontent.net/placeholder.svg'
alt='Instructor'
width={64}
height={64}
className='h-full w-full rounded-full object-cover'
/>
<span className='sr-only'>JD</span>
</div>
<div>
<h3 className='text-xl font-bold'>John Doe</h3>
<p>Senior Frontend Engineer</p>
</div>
</div>
<p>
John Doe is a seasoned frontend engineer with over 10 years of
experience. He has worked on a wide range of projects, from
small startups to large enterprises, and is passionate about
sharing his knowledge with others.
</p>
<div>
<h3 className='text-xl font-bold'>Course Duration</h3>
<p>24 hours of video content</p>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
);
}
Oh, by the way, I am using v0.dev by Vercel to generate the UI used in the example. Give it a try if you haven't already; it's really awesome!
哦,顺便说一下,我正在使用 Vercel 的 v0.dev 来生成示例中使用的 UI。如果你还没有尝试过,不妨试试;它真的很棒!
All of the content here is static. That is, for each user, this course information doesn't change. We don't require any request-time information to render this page. The text hardcoded in this markup can be stored in a database, and technically, we can generate this page only once at build time, according to our previous assumption.
此处所有内容均为静态。也就是说,对于每个用户,这些课程信息不会改变。我们不需要任何请求时间信息来渲染这个页面。在这个标记中硬编码的文本可以存储在数据库中,技术上我们可以在构建时根据之前的假设只生成这个页面一次。
So, technically, we generate a static HTML page with this information (SSG), resulting in faster page loads and excellent SEO.
因此,从技术上讲,我们使用这些信息生成静态 HTML 页面(SSG),从而实现更快的页面加载速度和优秀的 SEO 效果。
Here is the build logs for the above use case:
这里是上述用例的构建日志:
Now let's increase the complexity a bit more. We need to add a customer review section as shown below:
现在让我们增加一些复杂性。我们需要添加一个如下所示的客户评价部分:
The tricky part here is that the reviews get added and edited very often since our platform is very popular. Therefore, the reviews must be dynamically generated for each request.
这里棘手的部分在于,由于我们的平台非常受欢迎,评论经常被添加和编辑。因此,评论必须为每个请求动态生成。
To make this use case even more interesting, we need to show an edit and delete button for the review that the currently logged-in user has added. We might also need to show a text area to add review in the first place if the user is logged in. I will leave this part as an exercise. I hope this use case makes sense.
为了使这个用例更有趣,我们需要显示当前登录用户添加的审阅的编辑和删除按钮。我们可能还需要显示一个文本区域,以便用户登录后首先添加审阅。我将这部分留作练习。我希望这个用例是合理的。
Let's update our JSX to include this dynamic review section:
让我们更新我们的 JSX 以包含这个动态评论部分:
------
import { getReviews } from './db';
import { ReviewCard } from './components/review-card';
import { Suspense } from 'react';
import { cookies } from 'next/headers';
async function Reviews() {
const user = cookies().get('user')
const reviews = await getReviews();
return reviews.map(({ review, userName }) => (
<ReviewCard key={userName} review={review} userName={userName} />
));
}
export default function Home() {
return (
<div className='flex flex-col min-h-[100dvh]'>
------
<section className='w-full py-12 md:py-24 lg:py-32 border-t'>
<div className='container m-auto px-4 md:px-6'>
<h2 className='text-3xl font-bold tracking-tighter mb-8'>
Customer Reviews
</h2>
<Suspense fallback='loading..'>
<Reviews />
</Suspense>
</div>
</section>
</div>
);
}
How it currently works is that, all the content until the Suspense
boundary along with the fallback UI provided to Suspense is returned and immediately when a request is made. In the meantime the suspended children in our case the Reviews
component will start resolving the promises. Once the promises are resolved the UI for the suspended children gets streamed in the same response. On Client side react will seamlessly swap the fallback UI with the streamed content.
当前的工作方式是,在 Suspense
边界之前的所有内容以及提供给 Suspense 的备用 UI 都会在请求立即返回。与此同时,我们案例中的 Reviews
组件将开始解析承诺。一旦承诺解析完成,挂起的子组件的 UI 将通过相同的响应流式传输。在客户端,React 将无缝地将备用 UI 与流式传输的内容交换。
Remember everything here happens in single Request-Response Cycle so no client waterfall. And you have all the meaningful content on your page in a SINGLE Request.
记住这里所有的事情都在单个请求-响应周期中发生,所以没有客户端瀑布。并且你所有的有意义的页面内容都在一个请求中。
With the above changes, our entire route is now dynamically rendered because we want to render reviews dynamically for each request. The sad part is that we are wasting resources and doing redundant work: only the reviews section is dynamic, but all the other parts are the same for every request. Even though the content is the same for every request, we are rendering it every time. This increases the Time to First Byte (TTFB) since the entire page must be dynamically generated for each request.
经过以上更改,我们的整个路线现在可以动态渲染,因为我们希望为每个请求动态渲染评论。遗憾的是,我们在浪费资源并做重复工作:只有评论部分是动态的,但其他所有部分在每个请求中都是相同的。尽管内容在每个请求中都是相同的,但我们每次都会渲染它。这增加了首次字节时间(TTFB),因为每个请求都必须动态生成整个页面。
Here the build logs for the above use case:
这里是为上述用例生成的构建日志:
Now comes the actual fun part. Remember earlier I said the PPR is built on top of existing React APIs? We have used the Suspense
boundary in the above code to show a fallback UI while our Reviews
component is suspended to get the data from the database.
现在到了真正有趣的部分。记得我之前说过 PPR 是建立在现有的 React API 之上的吗?我们在上面的代码中使用了 Suspense
边界来显示回退 UI,同时我们的 Reviews
组件挂起以从数据库获取数据。
PPR takes this Suspense
with the streaming model to the next level:
PPR 将此 Suspense
与流模型提升到下一个层次:
-
Build Time: All the content up to the
Suspense
boundary, along with the fallbacks, is statically generated only ONCE at build time and suspends the rendering at build time (Pre Rendering).
构建时间:所有内容直到Suspense
边界,包括回退,仅在构建时静态生成一次,并在构建时暂停渲染(预渲染)。 -
Request Time: At request time, the pre-rendered static shell is immediately served to the client. Meanwhile, Next.js resumes rendering from where it had suspended at build time.
请求时间:在请求时间,预渲染的静态外壳立即提供给客户端。同时,Next.js 从构建时暂停的地方继续渲染。 -
Streaming to Client: Once the suspended children resolve, the UI is streamed to the client in the same response. On the client side, React swaps the fallbacks with the streamed content.
流式传输到客户端:一旦挂起的子组件解决,UI 就在同一响应中流式传输到客户端。在客户端,React 将回退内容与流式内容交换。
So How do we achieve this?
那么我们如何实现这一点呢?
Simple updated your next.config.js
file to have the experimental ppr turned on (Make sure you are on either next@canary or next@rc)
Simple 已更新 next.config.js
文件以启用实验性 ppr(确保您处于 next@canary 或 next@rc 之一)
next.config.js
/** @type {import('next').NextConfig} */ const nextConfig = { experimental: { ppr: true, }, }; export default nextConfig;
With this one change we are now PPRing our page. Here is the build output with PPR turned ON
通过这个改动,我们现在正在对页面进行 PPR 处理。以下是开启 PPR 后的构建输出
Chapter 3 - How It Compares to Other Rendering Strategies
第三章 - 与其他渲染策略的比较
-
Static Content / Static Site Generation (SSG) - The initial static shell is pre-rendered at build time, ensuring fast initial page loads and excellent SEO.
静态内容 / 静态站点生成(SSG)- 在构建时预先渲染初始静态外壳,确保快速初始页面加载和优秀的 SEO。 -
Server-Side Rendering (SSR) - Suspended components are streamed at request time, providing dynamic content tailored to the user while maintaining efficient resource usage.
服务器端渲染(SSR)- 在请求时流式传输挂起的组件,同时保持高效资源使用,提供针对用户的动态内容。 -
Client-Side Rendering (CSR) - Rich client-side interactivity is achieved with React client components, allowing for a highly responsive and engaging user experience.
客户端渲染(CSR)- 通过 React 客户端组件实现丰富的客户端交互,提供高度响应和吸引人的用户体验。
Combining the Best of All Strategies
结合所有最佳策略
Partial Pre-Rendering (PPR) effectively combines the best aspects of these rendering strategies:
部分预渲染(PPR)有效地结合了这些渲染策略的最好方面:
- SSG provides a fast, SEO-friendly initial load by pre-rendering static content.
SSG 通过预渲染静态内容提供快速、SEO 友好的初始加载。 - SSR ensures that dynamic, personalized content is delivered efficiently by streaming it on demand.
SSR 确保通过按需流式传输,高效地提供动态、个性化的内容。 - CSR enables rich interactivity on the client side, enhancing the user experience with responsive UI components.
CSR 在客户端实现丰富的交互性,通过响应式 UI 组件提升用户体验。
Final Thoughts 最后思考
Isn't this a true marvel? With Partial Pre-Rendering (PPR), we achieve fast initial page loads with meaningful content, personalized dynamic content, and rich interactivity all in a single page rendered through a single request.
这不是一个真正的奇迹吗?通过部分预渲染(PPR),我们实现了快速加载初始页面,包含有意义的内容,个性化的动态内容,以及丰富的交互性,所有这些都在一个页面中通过单个请求渲染。
I believe that PPR has the potential to significantly improve the web experience. Currently, this technology is exclusive to Next.js, so you must be familiar with Next.js to take full advantage of PPR.
我相信 PPR 有潜力显著提升网络体验。目前,这项技术仅限于 Next.js,因此您必须熟悉 Next.js 才能充分利用 PPR。
As an experimental feature, PPR is still in its early stages and will undoubtedly see many improvements as a larger community begins to adopt and refine it.
作为一个实验性功能,PPR 目前仍处于早期阶段,随着更大社区开始采用和改进它,无疑将看到许多改进。
Lastly, a big salute 🫡 to all the engineers behind PPR. You have truly made the web a better place.
最后,向 PPR 背后的所有工程师致以崇高的敬意🫡。你们真正让网络变得更美好。
Thank you so much for patiently reading this long blog post. I appreciate your time 🙏🙏🙏. I'd love to hear your thoughts in the comments. Happy PPRing!
非常感谢您耐心阅读这篇冗长的博客文章。我非常感激您的时间 🙏🙏🙏。我很乐意在评论中听到您的想法。祝您 PPR 愉快!
A complete version of example code used can found here and the corresponding demo
一个完整的示例代码版本可以在以下链接找到,相应的演示也在这里