作为 Remix 的联合创始人,Ryan Florence 常常会被问到一个问题:
Remix 到底和 Next.js 有什么区别?
看来这个问题是避不开了!那么,为了能更好地回答这个问题,也为了避免不必要的麻烦,作者决定给予各位观众一个直球答复。但如果你对 Remix 爱得深沉,希望你在分享本文到社交平台时能保持批判的态度。要知道作者和 Vercel 团队的友谊甚至可以追溯到 Vercel 成立之前!潮起潮落,百舸争流,作者是非常尊重 Vercel 他们所做的工作,也认为他们的工作非常了不起。
但话说回来,作者还是认为 Remix 是要胜过 Next.js 的。
希望各位观众老爷能够读完整篇文章,毕竟文章中众多花哨的图表和动画并不能涵盖所有作者想表达的意图。最后,希望你会在你的下一个项目中考虑用到 Remix(完全没有在双关)。
tl;dr
在静态内容加载方面,Remix 与 Next.js 旗鼓相当
在动态内容加载方面,Remix 略胜一筹
即使网速不佳,Remix 所提供的用户体验仍比 Next.js 要好
Remix 可以自动处理错误、中断,以及争用条件,但 Next.js 不行
在提供动态内容时,Next.js 鼓励用户侧 JavaScript,而 Remix 不会
在处理突变数据时,Next.js 需要用户侧 JavaScript,而 Remix 不需要
在构建时间上,Next.js 随数据量增加呈线性增长,而 Remix 则与数据解耦,且近乎即时
Next.js 对应用结构有要求,在数据扩容后还会牺牲部分性能
作者认为 Remix 的抽象会引导向更好的应用代码
背景
为让这场框架间的对决更加公平,作者决定以 Next.js 的官方使用示例为评比标准。他的理由也很充分,既然 Vercel 团队以其为示范,那就说明这个例子一定可以反映他们对自己作品的期望,并能够展示 Next 中最亮眼特征。
他所选择的是 Next.js 官网上网站实例中的一个制作精良的商业模板,模板所包含的各类实际开发中会用到的功能也深得 Florence 喜爱,包括:
对电子商务至关重要的起始加载页
搜索页面的动态数据
购物车功能的数据突变
支持多种后端,展示了框架抽象的能力
最终搭建的项目实际有两个版本:
一是改动最小的版本。本质上只是复制粘贴并微调了 Next.js 的项目,使其能够在 Remix 上运行,并且最后也是一样部署到 Vercel。这个版本非常适合用于对比,因为除了框架之外一切都是一模一样的。
二是推翻重写的版本。两个框架实际上并没有多少共用的 API,Remix 所运行的基础架构可以说与 Next.js 完全不同,如果想要充分发挥 Remix 的设计思路,就只能将其重新改写成百分百的 Remix,并在应用中添加一个快速图像优化的路径以适应 Remix。
不过这个应用并不能完全展示 Remix 中的全部特性,比如说作者颇为自豪的嵌套路由。但请不要担心,在回答完这个问题之后,我们有的是时间仔细唠。
另外,这篇文章在发布之前其实还给 Vercel 看过,他们看完之后说官网上的例子实际上是在旧版本的 Next.js 上运行的,于是 Vercel 和作者都相继更新了自己的项目的版本。
没开玩笑,我们喜欢 Vercel!
在尝试过几乎所有的托管平台之后,作者认为只有 Vercel 才是 Remix 的最佳部署目标,其开发体验让作者受益匪浅,恨不得引为知己。他们常说的“开发、预览、发布”,非常有用。就在前两天,作者和推特好友 @gt_codes 遇到了个生产问题,而正是 Vercel 所提供的部署预览截图功能,让作者几乎能在瞬间就找到出问题的代码提交版本。
但真要说 Remix 与 Vercel 的关系,那可就复杂了。他们不仅仅是朋友,是技术伙伴,还是框架间的竞争对手,而 Vercel 的开发总管 Lee 对这为朋友、搭档、竞争对手所撰写的文章提供了充分的理由:
当 DevTools 鹬蚌相争时,开发者总会渔翁得利:
Svelte 推动了 React
Remix 推动了 Next.js
Prisma 推动了 ORMs
Deno 推动了 Node.js
Supabase 推动了 Firebase
esbuild / SWC 推动了 JS tooling
Bun 推动了 SWC
还有谁?
— Lee Robinson(推特 @leeerob)发布于 2021 年 11 月 30 日
欲知详情可见原推文。
描述出真知
作者以为,各位软件开发者对自家应用的描述总是能透露出不少东西的,就比如说各位可以在作者自己的推特上看到非常全面的 Remix 迭代过程。
而 Next.js 对自己的描述是
这是一个用于生产环境的 React 框架。Next.js 为您提供生产环境所需的所有功能以及最佳的开发体验,包括:静态及服务端融合渲染、 支持 TypeScript、智能化打包、 路由预取等功能 无需任何配置。
Next.js 是由 Vercel 搭建的,而 Vercel 的 GitHub 仓库是这么描述的:
Vercel 是提供静态网站和前端框架托管的云平台,旨在集成您的无头内容、商务,以及数据库。
再看看 Remix 对自己的描述:
Remix 是一款边缘原生的全栈 JavaScript 框架,用于构建现代、快速且有弹性的用户体验,它将客户端、服务器以及网站基础相结合,让用户可以专注于产品而非代码。
这三者的个中区别不言而喻。
主页,页面的视觉完成度
Remix 真的和 Next.js 速度一样快?
这个问题常常被提起。不过既然 Next.js 常把“默认即优化”挂在嘴边,那就让我们看看 Remix 和 Next.js 两个框架哪个能更快地完成页面的视觉渲染。
这场比赛通过 WebPageTest 进行,文章中所展示 gif 也都是由该网站生成。每轮比赛中三种应用都有五次机会,最终结果取表现最佳的一次。
所有的结果展示动画上方都会有一条指向 WebPageTest.com 的跳转链接,在链接页面中点击“rerun test”即可复现页面加载详情,欢迎各位对比赛结果进行监督。
首先测试的场景是从弗吉尼亚州发起,通过电缆路由接入到 Web 的连接方式。
Remix 加载时间为 0.7 秒,Next 为 0.8 秒
嘘,先别下结论。这三个应用的加载速度都非常地快,零点几秒的优势完全没有意义。再说,这次的例子对 Next.js 也不太公平,因为那个小小的 cookie 提示也被算在了页面完成度里面,而 Remix 搭建的页面并没有这个。再来一遍慢动作回放:
Remix 加载 用时 0.7 秒,Next 用时 0.8 秒
慢动作下可以看出 Next.js 的实际视觉完成所需的加载时间仅为 0.8 秒。另外,作者还在 3G 的网络连接下运行了同样的测试,结果依旧,三种应用加载速度都非常之快,且所用时间相差无几。
✅ 由此作者得出结论,Remix 与 Next.js 的加载速度一样快。
为什么加载速度会这么快?
首先,用 Next.js 搭建首页页面利用的是静态网站生成(SSG)中的 getStaticProps 方法。在构建时,Next,js 从 Shopify 读取数据,将页面转为 HTML 文件形式并存储到公共文件夹中。在网站部署成功后,静态文件会由 Vercel CDN 之外的边缘服务器提供,而非是直接访问单独某个源服务器。这样,数据加载和渲染早在请求接入前就已完成,而 CDN 则可以在收到页面请求后直接提供文件,用户也不用再承担网站下载和渲染的流量了。另外,由于 CDN 是全球分布,用户可以直接接入离自己最近的节点(即所谓的“边缘”),静态文件生成的请求则完全不用从源服务器跨越大半个地球传递给用户。
至于 Remix,它虽然不支持 SSG,但借助 HTTP 的 stale-while-revalidate 缓存策略(注,SWR 与 Vercel 的 swr 客户端获取包不是同一个东西)可以达到同样的效果:静态文件边缘获取(虽然同样使用的是 Vercel 的 CDN),唯一的不同大概就是文件上传的途径了。
与常规的在构建或部署时获取所有数据并将页面以静态文件形式渲染不同,Remix 在流量到达时便准备好缓存,从中提取出文件,同时在后台准备好接收下一个访问的重新验证。和 SSG 一样,在流量到达时用户无需为下载和渲染花费流量。至于缓存未命中,这一点我们将在后面仔细谈及。
如果说你想找 SSG 的替代品,那么 SWR 可以说是个很好的答案,而这个答案更棒的是 Vercel 的 CDN 也支持 SWR,这让其部署变得更加顺滑。
或许你会想知道为什么导入版的 Remix 运行速度会比 Next.js 慢?这是因为 Remix 还没有内置的图片优化系统,这个版本的应用是直接引用的 Next.js 版本的图片路径 🤫。也就说浏览器得先打开一个连接两个域的连接,导致图片加载慢了 0.3 秒,这一点也可以从网络瀑布分析里看出。如果说图片是自托管的,那么这个版本的应用应该是可以加速到和另外两个差不多速度,也就是 0.7 秒左右。
和其他两个版本不一样,重写版本的 Remix 没有选择使用 SSG 或 SWR 在边缘缓存文件,而是直接在 Redis边缘缓存数据,或者说,这个版本的 Remix 也是在边缘运行,不过用的是 Fly.io。再加上资源路由的图片优化功能,将数据写入一个持久存储的卷,约等于是拥有了一个自己的 CDN 😎.。
在几年前要是想搭建这种类型的应用大概会非常困难,但现如今服务器的规模已经天翻地覆,而未来只会更好。
动态页面加载
Remix 与 Next.js 有什么不同?
这是作者常会收到的另一个问题。
这二者光是在功能集上就相差很多,而其中最重要的架构差异在于 Remix 并不依赖 SSG 来提速。可以说,在任何一个应用中,你早晚都会遇到某个 SSG 并不支持的场景。在这个应用中,我们需要测试的是它的搜索页面。其原因在于,用户可以提交无数次的查询请求,而鉴于宇宙当下对空间和时间的限制,静态生成无限制页面并不可能,因此 SSG 不在考虑范围内。
由于 SSG 对动态页面不适用,Next.js 便转而从用户浏览器中获取客户端侧的数据,这两张瀑布图对比可以清晰地展示为什么 Next.js 的应用会比 Remix 慢上 2.3 倍。
Remix 搜索页加载
Next.js 搜索页加载
可以看出,在 Next.js 才刚刚开始载入图片的时候,Remix 就已经完成了页面的加载。对于 Remix 来说,网页性能中最重要的便是并行化网络瀑布,他们对此非常热衷。
再看 Next.js,考虑到 SSG 可能不会适用于某些场景,Next 开创了一种 “网络瀑布请求链”的策略,并从用户浏览器中获取搜索结果。在成功加载出图片之前应用得先获取到数据,而数据获取又要先完成 JavaScript 的加载、解析和评估。
客户侧获取数据意味着要传递更多的 JavaScript 文件,更长的时间来解析和评估。有时候你可能都忘了还有解析和评估这回事,但仔细看这个瀑布图其中的第 15 条请求,其所花费的时间甚至比整个文件下载的时间还要长!相比 Remix 所发送的 371 kb 未打包数据,Next.js 发送了 566 kb 的 JavaScript,多发送了足足 0.5 倍的文件,即使是上传压缩后也多了有 50 kb(172 kb 对比 120 kb)。
给浏览器增加工作量的后果也逐渐显现出来了。在瀑布图中最底下一行的 CPU 利用率和浏览器的主线程活动情况,Next.js 应用一直在忙着处理那个大红条的“长任务”,以至于无暇顾及其他。
要问为什么 Remix 的加载速度可以和主页加载一样快?那是因为这两个用 Remix 写的例子都不用和 Shopify 的 API 对话。虽说 SSG 并不能缓存搜索页,但 Remix 应用可以不用 SWR 或 Redis 的情况下做到页面缓存。如果你只用单一一种动态方式生成页面,那么通过调整缓存策略,我们可以在无需修改程序代码的情况下,SSG 即可快速加载常用浏览页面。可以采取的方法有很多,启用“/search”页面,或者使用左侧导航中的类别和常见查询字段,比如“T 恤衫”之类。
动态页面缓存未命中
那缓存未命中怎么说?
说出来你可能不信,但 Remix 确实是在缓存为空的情况下出现了未命中情况。
其实图里的 Remix 重写版本是缓存命中的,但未命中的却要快上 0.6 秒。不过作者怕没人相信他,于是放了个慢版本的缓存命中的图。
这怎么可能!
事实证明,Shopify 的 API 其实还蛮快的。
由于 Next.js 的应用是直接从浏览器获取到 Shopify API 的,而从网络流量图中可以看出,请求只花了 224 ms,而浏览器与 API 建立连接所花费的时间甚至比发出请求的时间更长!个中原因可以解释为是他们在初始 HTML 中通过 <link rel="preconnect"/> 来提速。
如果说用户的浏览器也能够如此之快地发送到 Shopify 的请求,那么 Remix 的服务器必然也能把速度提上去。但用户与云之间的连接速度永远都会比服务器与云之间的连接要慢,所以最好还是把数据留在服务端。
说到底,在使用 Shopify API 时,缓存几乎是不必要的,无论缓存命中或未命中,在加载速度的表现上没什么太大的区别。
这一点可以通过减速用户网络来更好地展示,这一次用的是香港发起的 3G 连接,并且缓存未命中。
即使在缓存未命中的情况下,Next.js 也比 Remix 慢了 3.5 秒,这是怎么回事?
前面不是刚说完 Shopify 的 API 很快的吗?
Next.js 不能在数据加载完成前开始图片加载,而数据加载要等到 JavaScript 完成加载,而 JavaScript 的加载又要等文件先完成加载。用户的网络速度又给整条加载链中的每一步都所需要的时间乘了个倍数😫。
在 Remix 中,整个载入链中唯一需要率先完成加载的只有文件,这是因为 Remix 的设计便是如此,永远从服务端获取数据,去除用户网络对加载速度的影响。
在接收到请求后,Remix 可以立刻开始从 Shopify 中获取数据,不用等浏览器完成文件和 JavaScript 的下载,无论用户的网络是什么速度,服务端到 Shopify API 的数据获取速度都不会变,一直都保持在 200ms 左右。
架构的不同
Next.js 在客户端中获取数据所牺牲的不仅仅是用户体验。这个应用程序其实是有两套与 Shopify 连接的抽象,一套是 SSG 在用,另一套则是给浏览器用的。
架构上不不同往往又会带来更多的问题:
浏览器里是否有身份验证?
API 是否支持 CORS?
API SDK 在浏览器中是否可用?
构建和浏览器中代码如何共享?
将 API token 暴露给浏览器是否安全?
分发给访问者的 token 都有什么权限?
方法中 process.env 是否可用?
方法能否读到 window.location.origin?
如何让发出的网络请求在双方都有效?
相应该缓存在什么地方?
是否应该在连接双方处都创建一个同构缓存对象,并将其传递给不同的数据抓取函数?
对于只需要在服务端抽象 Shopify API 的 Remix 来说,这些问题的答案如下:
浏览器里是否有身份验证?——不
API 是否支持 CORS?——支持与否都可以
API SDK 是否需在浏览器中可用?——不需要
构建和浏览器中代码如何共享?——无需共享
将 API token 暴露给浏览器是否安全?——无需暴露 token
分发给访问者的 token 都有什么权限?——没有权限
方法中 process.env 是否可用?——可用
方法能否读到 window.location.origin?——不能
如何让发出的网络请求在双方都有效?——随意,请求不在浏览器中处理
相应该缓存在什么地方?——很多地方,HTTP、redis、lru-cache、持久存储、sqlite……
是否应该在连接双方处都创建一个同构缓存对象,并将其传递给不同的数据抓取函数?——不需要
这些问题的答案越是简单,就代表应用的抽象做得越好,代码也就越简单。
如果 Next.js 的应用能放弃从客户端侧的抓取,转而使用 getServerSideProps 方法,它与 Remix 应用在速度间的差距大概能缩小不少,面对前面那些问题也会有更好的答案。不过如果你通读了 Next.js 的官方文档,你大概会发现他们其实更鼓励开发者们选择 SSG 或者客户端侧的数据抓取,而非是作者更赞同的服务端数据获取、
如果不需要预渲染数据,则应考虑客户端侧数据获取、
对于用户数据,Next.js 也是鼓励从客户端侧进行抓取,这点更是表现了其与 Remix 在架构上的不同。
(客户侧数据获取)非常适合用在用户信息首页等页面,因为信息页是更私人的,更针对单独用户的,这种页面中并不涉及 SEO。
但这其中最主要的差别在于获取页面中数据所用到的“模式”。Next.js 用了四种:
getInitialProps - 服务端和客户端调用
getServerSideProps - 服务端调用
getStaticProps - 在构建时调用
客户端提取 - 在浏览器中调用
而 Remix 只有一种模式。“loader”。想也知道,在一个地方抽象一个东西远比在三个地方分别抽象四个要简单许多。
不同架构背后的代价
架构分歧不可避免,那么其背后的成本要如何量化呢?作者认为全部应用开发中最困难的任务应当是抽象其商业后端,毕竟这款应用的设计初衷是为能让其轻松接入任何的电商接口,无论是 Shopify、BigCommerce、Spree、Saleor 等等。
在用 Next.js 写的应用中,Shopify 相关的内容是放在这个文件夹的。运行 cloc 命令后会发现:
约 8,000 行代码,横跨近百个文件,换一个接口也是差不多结果,都是约一万行代码横跨百个文件。而这近万行的代码几乎都传到了浏览器中。
一个文件
608 行代码
0 行代码被送到浏览器
以上就是 Remix 和 Next.js 架构间区别所带来的成本。Next.js 的抽象需要预测并参与构建和浏览器的运作,而 Remix 的抽象只作用于服务器。
你或许会质疑这两个 Shopify 接口所提供的是否是同一套功能?或许作者作弊了?确实,这两个应用都有用于验证和心愿单功能的代码,但 Shopify 接口虽然给这些功能准备了导出模块,但最终一个都没用。成品网站页面看起来没什么不同,如果说作者确实遗漏了什么功能,那在全部可见功能只用了这 7,000 行代码十分之一的情况下,很难想象会有什么东西需要这么多代码才能实现。
即是说 Next.js 转而使用 getServerSideProps 方法来构建搜索页,那他们还是得需要这么多的代码来完成数据突变的功能,不过这些就要在后面再提了。
边缘原生
前文中提到过多次的“在边缘部署”,到底是什么呢?还是通过例子来看,以下是另一个从香港发起的缓存未命中,但这次我们用让用户的网速快一点:
这次我们要分析的是两个 Remix 应用间的不同;前文中已通过网络瀑布图探讨过 Next.js 应用加载缓慢的原因。
既然两个 Remix 应用都是从服务器中取的数据,那么为什么改写版会比重写版慢上这么多?
原因很简单,Remix 改写版是在 Vercel 方法中运行的,而 Vercel 方法并不会在边缘运行你的代码,他们会在某个地区运行,默认是华盛顿特区,这离香港可有大半个地球呢!
这也就意味着,这个情景里的用户访问请求得先从香港长途跋涉到华盛顿,然后服务器才能开始从 Shopify 下载数据,等服务器处理完了,还要千里迢迢地把数据再发回给用户。
但对于重写版的应用来说,虽然它也是在华盛顿运行的,但它还有一部分是在香港跑的。这就意味着不仅用户接入 Remix 服务器的速度变快了,而且页面的加载速度也快了。
打个比方,改写版是骑单车进城,而重写版则是骑单车到车站然后坐火车进城。
🚲-----------------------------------------🏢
🚲-----🚊====🏢
这一点也可以从网站瀑布图中看出来:
重写版 Remix,边缘运行
Remix 改写版,美国东部运行
二者在基础架构上的差异主要体现在代表 HTML 文档的第一个蓝条上。在改写版中,蓝条肉眼可见的长,相当于是用户骑着单车跨越半个地球;而在重写版中,用户搭着火车上了 Shopify 的 API,往返车程快了很多。
再加上这个版本的 Remix 是通过 Fly.io 运行的,而 Fly 所提供这些服务器可以在遍布全球十几个区域中的 Node.js 服务器上运行。不过 Remix 并不完全依赖 Node.js,它其实可以在任何的 JavaScript 环境中运行,其中之一便是 Cloudflare Worker,他们拥有的服务器足足有250个,并且遍布全球,还有什么用户是他们接触不到的吗!
这也就是为什么用户会称 Remix 是“边缘原生”。与之相反,Next.js 完全依赖于 Node.js,所以他们在边缘部署的能力在如今会受到诸多限制。
当然,Remix 在这一领域的也不能说是全知全能,提升开发者开发体验的路任重而道远。目前 Remix 官方仅支持 Node.js 和 Cloudflare,而 Deno 仍在开发中,开发社区则选择了 Fastly。
像是 Remix 这种“边缘原生”的框架,开发者们不用再发愁怎么让不同用户所享受的体验相差无几了,所有用户都能有光明的未来。
如你所见,“边缘”这一理念前景非常好,对作者而言,Vercel 的团队还在边缘部署的漫漫长路上,而 Remix 则已经做好了一切的准备,只待实战。
客户端转换
无论是 Remix 还是 Next.js,这两个框架都可以通过链接的预取(prefetch)实现实时转换,只不过 Next.js 的预取只对由 SSG 创建的页面支持。由此看来,搜索页面也不能成为我们这次的比赛项目了。
相比之下,由于任何页面在数据加载方面都没有架构上的不同,因此 Remix 可以预取任何的网页,无论是用户驱动的未知搜索页面 URL 还是已知的产商品链接,甚至可以不局限于链接,只要是页面,Remix 可以随时随地进行预取。以下是一个用户输入时预取的搜索页面示例:
搜索输入预取,3G
(视频见原文)
既没有下拉菜单也没有骨架屏,甚至是在慢速网络链接下,也能带来即时的用户体验 🏎。
这段代码也相当简单:
可以看出,Remix 用的是 HTML 的 <link rel='prefetch'>,而不是像 Next.js 一样用的内存缓存,因此,实际发出请求的是浏览器而不是 Remix。再看前面视频里用户中断当前的获取后,Remix 是如何取消请求的:完全不需要多一个字的代码就能优秀完成异步处理。
数据突变
在这一方面,可以说 Remix 和 Next.js 之间没有任何共同点了。既然应用程序的大半代码都与数据突变有关,那么为什么不让 web 框架也向它看齐呢?
Next.js 中的数据突变:无。<button onClick={itsAllUpToYou}>,这行代码能解决一切。一般来说下,我们是通过管理表单状态来获取发布内容的,从添加一个发布用的 API 路由,到手动跟踪加载和错误状态、重新验证数据状态和其在整个 UI 中的传播变化,最后处理错误、中断和争用条件(不过说老实话,最后一条大概没几个人真的会去做)。
Remix 中的数据突变:Remix 用的是 HTML 表格。不过别着急下定论,Remix 的 API 完全可以处理当代 web 应用的需求,毕竟它的开发者全部的职业生涯都在和 web 应用高强度打交道。就算 Remix 看着像是老古董的 PHP,并不意味着它没办法适应现如今复杂多变的用户体验。对作者常常说,Remix 不仅可以扩展,还可以向下扩展。那么让我们回到美好的旧日子,更好地理解 Remix 的设计思路。
自互联网的诞生开始,突变都是借助表单和服务器页面来处理的。而如果完全不用 Remix,那么代码应该长这个样子:
浏览器用表单序列化后的数据通过 POST 导航到“/add-to-cart”页面,添加待定的用户界面,完成后再用数据库中的所有新数据渲染一个新页面。
Remix 和 HTML 表单的作用差不多,不过用首字母大写的 <Form> 标签和一个 action 路由函数进行优化(如果说 Next.js 的页面也用自己的 API 路由……)。通过 fetch 发布而无需重新加载文档,让服务器重新验证页面上的所有数据以保持 UI 界面与后端保持同步。这一切都和开发者们在 SPA 里做的差不了多少,不过这里是 Remix 在帮忙管理了。
除了表单和服务端的操作之外,Remix 不需要应用程序的代码和服务器进行任何沟通,也不需要应用提供上下文或者全局的状态管理手段来将变化传递到 UI 的其他部分。这也是为什么 Remix 的打包比 Next.js 要小近 30%,毕竟 Remix 不需要用所有的代码来和那个“API 路由”对话。
不过话说回来,刚才那一串的代码确实可以在 Remix 里用上,如果用的是首字母小写的 <form> 标签,那么处理这些的将会是浏览器而不是 Remix。要是 JavaScript 没加载出来那这些将会派上大用场,具体的后面会详细说。
除此之外,我们还可以让 Remix 借助常用下拉框和已发布的进度和数据来优化 UI 并进一步扩展到高级的界面效果。这一系列操作中,HTML 表单是空白的画布,而绘制所依赖的则是设计师的灵感。如果说设计有了变化,页面的架构也不用有大改动。
更小的打包和更简单的突变 API 并不是 Remix 能做到的唯一事情。
因为 Remix 负责应用与服务器之间的所有互动,包括数据加载和突变,其在网络框架领域的独特能力可以应对网络的各种长期问题。
未处理错误
如果“添加到购物车”操作的后端处理程序抛出错误,那会发生什么?下面这个视频中,我们在向购物车添加物品时,拦截了到路由的请求,看看会发生什么。
Next.js 的 POST 请求失败
(视频见原文)
错误处理非常难搞,和这里的处理一样,很多的开发者直接跳过了这一步骤,但作者觉得这样会导致糟糕的用户体验,所以让我们看看 Remix 在这一步是怎么做的:
Remix 的 POST 请求失败
(视频见原文)
Remix 可以处理应用中所有涉及数据和渲染的错误问题,即是这个错误是在服务器那边的。
开发者们所要做的就是在应用程序的底层定义一个错误边界,甚至进一步细化,只处理页面中出现错误的部分。
而 Remix 能做到而 Next.js 却不能原因只有一个,Remix 的数据抽象并没有停留在将数据引入应用程序这一层面,而是进一步拓展至数据的改变。
中断
用户总是难免会在页面上同一个地方多点上几次鼠标,而多数的网页应用并不能很好地解决这个问题。但难免会有某个按钮,你会希望用户能够快速多次点击,并且网页 UI 也能立刻做出回应。
在我们的这个例子中,用户可以改变购物车中物品的数量,他们可能会想要快速增减数量。
让我们先看看 Next.js 的应用是如何处理中断问题的:
Next.js 的中断处理
(视频见原文)
事情发生太快很难看清到底发生啥了,但如果你左右调一下进度条,就会发现在第五六秒左右发生了很神奇的事情,不过最后几帧才是重头戏。可以看到,第四秒的时候最后发送的一个请求落地,而在几帧之后第一个请求才落地!而商品数量的变化则是“5-6-5-4-2”,期间完全没有用户互动,这种 UI 真的很让人头疼。
再看代码,也是没有任何对争用条件、中断或重新验证的处理,所以才会造成这种用户界面和服务器不同步的情况,而最终的商品数量是 2 还是 4 完全取决于最后到达服务端的到底是什么。如果代码里能够对中断和突变后的数据重新验证有一定的管理,那么将有效避免这种情况的发生。
有一说一,争用条件和中断要处理起来确实麻烦,所以大多数的应用程序都不愿意去做。即使是业内专业技术数一数二的 Vercel 开发团队也跳过了这一步。
无独有偶,在作者之前的一篇文章中也遇到过一样的情况,在移植 React Core 团队所搭建的 React 服务器实例时,他们也无视了争用条件和中断的处理。
那 Remix 是怎么做的呢?
Remix 的中断处理
(视频见原文)
可以看到在中断之后 Remix 便取消了请求,并且在 POST 完成之后又重新验证了数据。如此确保了不仅是这个表单,整个页面的 UI 都与服务器上的改变是同步的。
或许你会觉得是作者作弊了,毕竟他们在做的时候会投入更多的细节,而 Next.js 的应用只是个示例。但刚刚展示的这些特征并不是通过应用的代码实现,而是内置在它的数据突变 API 中的,Remix 其实做的仅仅是浏览器和 HTML 表单之间的互动。
作者认为,Remix 在客户端和服务器之间的无缝集成和过渡可以说是史无前例的。
Remix 和 Web 之间的爱
回想作者在几十年前与网络开发打交道的岁月里,一切都曾是那么的简单。表单里塞进一个按钮,让它指向数据库里的一个页面,之后再搞定重定向和更新后的用户界面,不能更轻松了。
Remix API 的设计总是以平台为准。比如突变的工作流,既然 HTML 表单 API 和服务端处理程序是正确选项,那么 Remix 就要围绕这个组合搭建。而在搭建的过程中,诞生了一个绝赞的副产品,那就是常规的 Remix 应用程序核心功能不需要 JavaScript 就可以运行!
没有 JavaScript 的 Remix
(视频见原文)
虽然说 Remix 这样也能工作,但并不建议各位开发者们在搭建站点的时候不用任何 JavaScript 的脚本。作者对这款框架抱有的野心不小,为此,JavaScript 必不可少。
但与其说“Remix 可以在没有 JavaScript 的情况下工作”,作者还是更喜欢“Remix 在 JavaScript 之前工作”的说法。举个例子,如果用户在火车进入隧道之前点开了你的网页,而页面此时正在加载 JavaScript,那么如果在用户出隧道之后页面仍能正常展示,这样用户的体验一定很好。Remix 想做的只是追求 HTML 页面的简单性,但却在追寻的路上构建完成了一个如此具有弹性的框架。
即使是在编写服务端代码,Remix 也是将 web 平台放在了首位。它并没有大费周折开发一个全新的 JavaScript 请求和响应的 API,而是选择使用 Web Fetch API。对于 URL 的搜索参数处理,它使用的是一个内置的 URLSearchParams 方法。而表单则是通过内置的 FormData 方法进行操控。
对于 Remix 的初学者来说,Remix 的文档和 MDN的文档同样重要。这是因为作者希望 Remix 的学习能帮助开发者们搭建更好的网站,即使未来 Remix 已不再是他们所使用的框架了。
Remix 学的好,网页就搭得好。
这是 Remix 开发团队所奉行的理念。虽然全文看下来,Remix 的速度确实是非常之快,但在开发的过程中,性能并不是他们的第一要务,他们只是想提供更好的用户和开发者体验。他们从平台中寻找问题的答案,为框架提供更丝滑的使用体验,而剩下的则会由平台自己解决。
为变化而优化
在介绍完两个框架的工作原理后,让我们再来看看这些应用程序是如何应对变化的。“为变化而优化”这句话深得作者喜爱,并在设计 Remix API 的时候也常常提及此。
改动主页
假如说你想要更改主页上商品内容,那么要怎么做呢?在 Next.js 中,你有两个选项:
重新构建并部署应用程序。具体的构建时间将随着页面中产品数量的增加而呈线性增长,这是因为每次的构建都需要从 Shopify 那边获取到每个产品的数据。即使是简单修改页面中的一个错别字,都需要从 Shopify 下载所有的产品以成功部署这个改动。当你的店铺产品数量增长到成千上万后,这将会带来极大的不便。
通过增量静态再生(Incremental Static Regeneration)完成。这是 Vercel 团队在面对构建时间问题是所提出的解决方案。在请求需要改动的页面时,服务器会发送一个已缓存的版本,然后在后台用变更后的数据进行重建。如此,之后页面的访问者将会收到新缓存过后的页面。
而在部署时没有完成构建的页面,将由 Next.js 以服务器方式对页面进行渲染,然后再缓存到 CDN 上。这和 HTTP 的 stale-while-revalidate 的作用相同,唯一的区别在于 ISR 还有一个非标准的 API 和供应商锁。
Remix 就不一样了。开发们只需要用 Shopify 更新要改动的商品,缓存 TTL 内就会有相应的改动。当然,你也可以用一个下午的时间设置一个 webhook,让用户在主页的查询无效。
虽然这种基础设施使用起来工作量要比 SSG 的大,但也正如我们先前所见,它是可以扩展到任何规模的商品数目的,任意模式的搜索页面 UI,并且在用户数量更多的情况下也要比 SSG 快(常用搜索条目可以直接从缓存中读取)。开发者无需与特定的主机相联系,也几乎不用和框架有什么接触,Remix 主要都是用标准的 Web API 来实现相应的应用逻辑。
除此之外,Remix 认为,仅在服务端加载数据会引导向更简洁的抽象。
那缓存未命中怎么说?
好问题。服务器和 HTTP 的缓存只会在网页接受到流量时才能起作用,但网站的业务也只有在接收流量时才有用。每天缓存两个页面才能让网站速度快上一秒并没什么用,你需要的应该是个邮件列表。
Remix 产品页面的空缓存命中与 Next.js 站点的搜索页面(搜索页面无法使用 SSG)速度相当。没有搜索框的购物体验简直糟糕。在缓存中填充入常用搜索语句之后,加载速度将会更上一层楼。
常用的登录页面几乎总是会被预先准备好,而 Remix 的预取功能可以让下一步转换即时完成。对于 Remix 来说,任何页面都可以预取,不管是动态还是其他。但 Next.js 就不行。
在 SSG 的页面到达一定规模后就需要切换到 ISR。而不在最后一次部署中的页面也将出现同样的缓存未命中问题。
如果缓存未命中的请求在你的网页访问中占据了很大一部分,那么百分百的缓存命中并不能让你的业务更好,你面临的不是技术问题而是营销问题。
个性化
下一个场景。如果产品团队来找你,说想要把主页改成显示与用户曾购买过商品类似的产品列表,而不是一个固定不变的产品列表。
和搜索页面一样,这种情况同样是用不了 SSG,默认情况下网页性能也是和它有关,毕竟 SSG 的用例确实有限。
网站必定是为用户服务的,而随着网站的发展,你会希望能向用户提供更多个性化的内容。而每对用户做一次个性化展示,都会是一次客户端获取。如果你页面中大多数内容都是从客户端获取的话,那么你网站的性能一定不会太好。
但对于 Remix 来说,这一切也就是在后端进行的不同数据库查询而已。
可以参考下海外电商龙头老大亚马逊。首页全部内容都是个性化内容,这让他们收获了成功。架构的投资会让你有机会成为亚马逊,而当产品团队需要调整首页个性化显示时你所必须要舍弃的东西并不能让你走向成功。
最后
Remix 中简单但有效的 <Form> + action + loader API 组合,以及它尽可能多地将加载任务放到服务器上的设计常常被轻视。但这些 API 可以让 Remix 更快地完成页面加载、转换,并为突变相关的中断、争用条件和错误带来更好的用户体验,让开发者的代码负担减轻。
Remix 应用程序的速度得益于其后端的基础设施和预取功能。Next.js 的速度则要归功于 SSG,然而,SSG 的使用情况并不能覆盖所有的需求,尤其在功能和数据量扩展的情况下,SSG 所建立的速度优势将不复存在。
SSG 和 Jamstack 都是低速后端服务的很好解决方案。可新一代的平台和数据库的速度都很快,并且未来也只会越来越快,即使支撑这些应用程序的 Shopify API 能在 200 毫秒内从世界上的任何地方发回查询的相应,这些方案大概不会有太大的作用。为此,作者已经在除了南极洲外的每一个大洲都做了相应的测试。
老实说,如果你想跳过本文中所有的缓存策略,让每个请求都命中 Shopify 的 API 也没问题。这样,你的加载时间将从 1.2 秒延长到 1.4 秒,如果说你的后端 API 速度不快,那么就把时间投入到后端加速中去。如果你没办法搞定这些,那就把部署自己的服务器和缓存,并通过它们为你的用户加速。
和 SSG 的情况一样,后端提速的投入将会造成性能的下降,但这些换来的将会是扩展性上的便利。虽然这么做前期的工作量会有提升,但从长远来看,无论是用户还是你的代码都将从中受益。
数据加载也只是网页的一部分而已。在 Remix 中,数据抽象也可以封装数据突变。将所有的代码都留在服务器上,以获得更好的应用代码管理和更好的打包。
用 Next.js,将意味着开发者必须向服务器发送自己的数据突变代码,才可以和 API 路由进行互动,并将更新传播到 UI 的其他部分。正如我们在这篇文章中所见到的一样,即使是再顶尖的团队也都难免会在错误、中断和争用条件方面处理不当。
那 getServerSideProps 怎么说?
可能会有人说了,getServerSideProps 完全可以替代 Remix 啊。但请容作者解释。
如前文所述,这个方法确实可以加速搜索页面,但它却解决不了数据突变的问题。我们需要结合 getServerSideProps、API 路由,以及浏览器的代码中与这二者相沟通的部分才能解决包含错误处理、中断、争用条件、重定向和重新验证等突变相关的问题。而 Remix 的宗旨则是搭建属于开发者们自己的系统,这正是作者想要表达的意思。
那么,既然大部分的问题都得到了回答,作者将在未来的文章中真正展现 Remix 的强大功能。
查看英文原文:
Remix vs Next.js by Ryan Florence
评论 2 条评论