为什么 Qwik 成为了我的新宠框
在众多的前端开发框架中,我最终选择了 Qwik[1],而不是 Next.js[2]。我的选择基于几个原因:开发者体验、信号机制、控制层面、能够使用更广泛的 React 生态系统,以及 Qwik 框架的前瞻性特性。Next.js 无疑是一个杰出的框架,我对此毫无保留。然而,Qwik 提供了如此吸引人的开发者体验和新颖的设计,每次使用它编码时,我都感到无比兴奋!
从 jQuery 到 Qwik 框架的旅程
我作为全栈工程师已经有将近 20 年的软件工程经验了。大约 15 年前,我的前端之旅开始了。我最初使用的是纯 JavaScript 和 jQuery,然后转向了 KnockoutJS、AngularJS 和 GWT。当 React 在 2013 年出现时,我成为了它的早期使用者,并深深爱上了它。近 10 年来,React 一直是我的首选库。当然,在此过程中,我也使用过其他各种框架和库,但在我今年发现 Qwik 之前,React 一直是我事实上的前端库。
Qwik 是什么?
让我们看看 Qwik 的文档是如何定义自己的:“Qwik 是一种新型的框架,它是可恢复的(没有急切的 JS 执行,没有水合),为边缘计算构建,对 React 开发者来说很熟悉。”这是什么意思呢?让我们来分解一下。
Qwik 利用了 JSX,所以它给人的感觉就像是 React,但它的一个决定性特性是其可恢复性。“可恢复性是关于[3] 在服务器上暂停执行,在客户端恢复执行,而不需要重新播放和下载所有的应用程序逻辑。”换句话说,就是渲染、暂停、恢复、渲染、暂停、恢复,等等。
对开发者来说,这大多是透明的,不需要增加复杂性。这是 Qwik 和其他框架之间的一个根本区别。例如,在 React 中,页面在服务器上渲染,然后在客户端水合,一旦所有必要的 JavaScript 下载完毕,页面就变得可交互了。这里唯一的例外是如果使用了动态导入,但这与可恢复性还是有所不同。
Qwik 被设计成让客户端/服务器边界基本上不成问题。默认情况下,一切都是在服务器上渲染的,除非你明确使用像 useVisibleTask[4] 这样的函数,结合 isBrowser[5] 来强制仅在客户端渲染。否则,所有的服务器渲染基本上都能普遍工作,只有少数例外。
这只是冰山一角。我鼓励你阅读下面链接的 Qwik 文档中的 Concepts 页面,因为 Qwik 确实是一个独特的框架,用于解决其他框架继续需要缓解的问题。
- Think Qwik[6]
- Resumable[7]
- Progressive[8]
- Reactivity[9]
Qwik 还很新,只有几年的历史。到目前为止,它在开发者中的曝光率还很低。我最近在 All Things Open Conference[10] 上才发现了它。如果这是你第一次接触 Qwik 框架,请花时间阅读下面的文档。这绝对值得。
Next.js 是什么?
关于 Next.js 已经有很多的讨论了,所以我会尽量简短而精炼。Next.js 是一个包裹了 React 库的杰出框架。它是当前 React 的首选框架。引用文档的话,“Next.js 是一个用于构建全栈 Web 应用程序的 React 框架。你使用 React 组件来构建用户界面,Next.js 用于额外的特性和优化。在底层,Next.js 还抽象并自动配置了 React 所需的工具,比如打包、编译等。这让你能够专注于构建你的应用程序,而不是花时间在配置上。”
Qwik 与 Next.js 的比较
在我对 Qwik 和 Next.js 的比较中,我评估了七个关键领域。对于每个领域,我都会选出一个胜者,这样你就可以根据对你来说最重要的特性来评估。
服务器与客户端
Next.js 强制在服务器和客户端组件之间做出非常明确的区分,而 Qwik 在大多数情况下,基本上让这个问题变得无关紧要。默认情况下,一切都是服务器渲染的,我认为这总体上是件好事。
胜者: 优势倾向于 Qwik
以下是 Next.js 文档[11] 中的一个示例:
代码语言:javascript复制// 下面的代码是 Next.js 的
// SearchBar 是一个客户端组件
import SearchBar from './searchbar'
// Logo 是一个服务器端组件
import Logo from './logo'
// Layout 默认是一个服务器端组件
export default function Layout({ children }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
// ---
'use client'
export default function SearchBar({ children }) {
return (
<>
<main>Search!</main>
</>
)
}
// ---
'use client'
export default function Logo({ children }) {
return (
<>
<main>Logo!</main>
</>
)
}
在 Qwik 中,没有必要定义 'use client' 或 'use server':
代码语言:javascript复制// 下面的代码是 Qwik 的
import { component$ } from '@builder.io/qwik';
import SearchBar from './searchbar'
import Logo from './logo'
export default component$(() => {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<slot />
</>
)
});
// ---
// SearchBar.tsx
export default component$(() => {
return (
<>
<main>Search!</main>
</>
)
});
// ---
// Logo.tsx
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return (
<>
<main>Logo!</main>
</>
)
});
代码看起来非常相似,这是意料之中的——因为它们都使用了 JSX。关键点在于,在 Qwik 中没有必要定义 'use client' 或 'use server',因为默认情况下一切都是服务器渲染的。 这极大地简化并改善了开发者体验。 虽然上面的示例很简单,但如果你曾经使用过 Next.js,你就会知道在服务器和客户端组件之间工作是一个持续的设计选择和实现考虑。
缓存
Next.js 在缓存控制方面提供了更多的灵活性。Qwik 也有缓存机制,你可以控制持续时间,但不能直接控制失效。这是否会成为一个问题还有待观察。在实践中,这并不是一个重大问题,但我可以预见它可能成为一个痛点。
胜者: Next.js
Next.js 允许你像这样使缓存失效:
代码语言:javascript复制// 下面的代码是 Next.js 的
export default async function Page() {
const res = await fetch('https://...', { next: { tags: ['collection'] } })
const data = await res.json()
// ...
}
'use server'
import { revalidateTag } from 'next/cache'
export default async function action() {
revalidateTag('collection')
}
这是一个很好的特性,也是 Qwik 缺少的一个巨大特性。Qwik 的方法是,在当前页面层次结构中的服务器操作可能导致变异时,重新运行所有的 routeLoader$(当前页面层次结构中的获取调用)。这种方法是可行的,但是缺少细粒度控制。
React 生态系统
Next.js 与完整的 React 生态系统自然地集成。Qwik 通过 qwikify[12] 函数,可以访问更广泛的 React 生态系统,Qwik 文档称这应该被视为一种迁移策略[13]。这是因为任何用 qwikify$ 包装的 React 组件都是独立渲染和水合的,这可能会影响性能。然而,Qwik 在水合发生时也提供了很多灵活性。例如,你可以告诉 Qwik 等到浏览器空闲[14]时再水合 React 组件。除了空闲之外,还有很多其他的控制机制。
另一个不错的特性是,Qwik 甚至不会在页面包含组件之前就拉取 React 库。如果你在页面 B 上有一个 qwikified 的 React 组件,React 库在浏览器访问页面 B 并且满足各种条件(比如它在页面上可见,想想一个尚未可见的模态框)之前,永远不会被加载。Qwik 提供的控制比 Next.js 多得多。虽然 qwikify$ 被视为迁移策略,但它工作得很好,你可以通过各种方式来缓解任何潜在的性能问题。
胜者: 倾向于 Qwik
代码语言:javascript复制// 下面的代码是 Next.js 的
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
// ---
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>查看图片</p>
{/* 因为 Carousel 是一个客户端组件,所以这里工作正常 */}
<Carousel />
</div>
)
}
你会注意到,在 Next.js 中,你不能在服务器端组件中本地使用客户端组件,所以你还必须用另一个有 'use client' 的组件包装第三方组件。
Qwik 的情况类似,但是控制层面更大。我真正喜欢的是 Qwik 对水合的控制。Next.js 在这里几乎没有或很少有控制,而 Qwik 允许你在 加载、空闲、悬停等[15] 上控制水合。
代码语言:javascript复制// 下面的代码是 Qwik 的
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { Carousel } from 'acme-carousel'
export default qwikify$(Carousel, { eagerness: 'hover' })
// ---
// SomeComponent.tsx
import { component$ } from '@builder.io/qwik';
import Carousel from './carousel'
export default component$(() => {
return (
<div>
<p>查看图片</p>
<Carousel />
</div>
)
});
图表
截至本文撰写时,Qwik 没有原生的图表库。在 React 中,你有访问众多库的选择,选择多到几乎令人眼花缭乱。话虽如此,像 Chart.js[16] 这样的库可以很容易地集成到 Qwik 中,尽管它将仅限于客户端渲染。要利用 Qwik 的全部能力,需要创建一个能够服务器端渲染的图表库。在那之前,与任何图表库的集成都很容易,但它们都将仅限于客户端渲染。用户体验是好的,但没有选择原生服务器端渲染仍然是一个缺失。顺便说一下,你可能会使用一个 SVG 图表库或手动 SVG 来实现服务器端渲染,但我还没有看到有正式的 Qwik 图表库这样做。
胜者: Next.js,因为 React 生态系统中有原生的图表库
状态管理
Qwik 原生支持信号(Signals)。如果你用过信号并与 React 的 useState 比较过,那么就没有可比性。信号无疑是赢家。Next.js 有一个开放的 issue[17] 来获取信号,但结论是这需要在 React 库本身中完成。有一些用户报告说通过将 Preact 信号“猴子补丁”(monkey patching)到 Next.js 中取得了成功,但结果似乎参差不齐。
胜者: Qwik
代码语言:javascript复制// 下面的代码是 Next.js 的
'use client'
function HomePage() {
// ...
const [likes, setLikes] = React.useState(0)
function handleClick() {
setLikes(likes 1)
}
return (
<div>
{/* ... */}
<button onClick={handleClick}>点赞 ({likes})</button>
</div>
)
}
代码语言:javascript复制// 下面的代码是 Qwik 的
export default component$(() => {
// ...
const likes = useSignal(0)
return (
<div>
{/* ... */}
<button onClick={() => likes = 1}>点赞 ({likes})</button>
</div>
)
})
你还可以将信号作为属性传递给子组件,并在那里进行变异。在 React 中,没有回调函数是不可能直接这样做的。
代码语言:javascript复制// 下面的代码是 Qwik 的
// Parent.tsx
export default component$(() => {
// ...
const likes = useSignal(0)
return (
<div>
<Child likes={likes} />
</div>
)
})
// Child.tsx
type Props = { likes: Signal<number>; }
export default component$<Props>((props) => {
return (
<div>
<button onClick={() => props.likes = 1}>点赞 ({props.likes})</button>
</div>
)
})
开发服务器
Qwik 使用 Vite[18] 作为开发服务器,Vite 正在成为一个主要的前端开发服务器工具。Vite 有一些令人难以置信的特性,比如内置的反向代理和非常高效的模块处理以及热模块重载。有关更多信息,请参见 Why Vite[19]。Next.js 仍然使用 SWC[20] 构建得非常快,并且使用 Turbo[21] 进行开发,但在这里 Vite 有优势。
胜者: 倾向于 Qwik
服务器端渲染
虽然在服务器与客户端的部分已经提到了这一点,但我想在这里更深入地探讨服务器端渲染。
当考虑到框架渲染服务器组件以及浏览器首次接收其 HTML 时,故事很快就会变得复杂。Next.js 和 Qwik 以不同的方式完成了同样的任务。表面上看,结果实际上是相同的,但是框架特定的控制机制提供了不同的开发者体验。如果你阅读了 Next.js 的 loading-ui-and-streaming 文档[22],你可以利用 React Suspense 来实现“即时”加载,然后逐步解析 UI。这非常好,Qwik 中没有立即的类比,但你仍然可以使用 server streaming[23] 来实现同样的事情。这里的不同之处在于,你必须自己管理数据加载,然而你拥有更多的控制权。例如,你可以先加载前 10 个产品,然后渲染页面,然后加载其余的产品。这是一个人为的例子,但它说明了这一点。有一个有趣的 GitHub issue[24] 展示了 Qwik 加载数据与流式传输的示例。你会注意到在 Qwik 中这样做的复杂性。这就是 Next.js 以简单性获胜的地方。
胜者: Next.js,因为 React Suspense 提供了更好的开发者体验。然而,Qwik 可能拥有更细粒度的控制,并且可以完成同样的事情,只是没有那么无缝。
我为什么选择 Qwik
- Qwik 使得开发更简单,因为它提供了更好的开发者体验 —— 你大部分时间不需要管理服务器与客户端组件之间的差异。Qwik 并不是特意去抽象这一点,这是 Qwik 的一个基本设计,其中一切都是最初在服务器上渲染的,除非你明确地使其在客户端渲染。
- 尽管 Qwik 的生态系统还处于早期阶段,但你仍然可以访问更广泛的 React 生态系统。 是的,水合有一个惩罚,这在实践中通常是微不足道的,但无论在 Next.js 中如何,水合惩罚都是存在的。Qwik 的一线希望在于,你对水合发生的时间有控制权,并且你最终可以重写/重构 React 组件,使其成为 Qwik 原生的。
- 信号比 React useState 优越,我认为在这一点上不会有太多异议。如果有什么争议的话,一些人可能会为 RxJS 而不是信号辩护,但那是另一个讨论。
- 我相信 Qwik 的可恢复性方式代表了未来框架的一个可能的基石。 即使是 React Server Components 也做了类似的事情,将数据在渲染后序列化到客户端。然而,对于 RSCs,"所有为服务器组件编写的代码都必须是可序列化的,这意味着你不能使用生命周期钩子,比如 useEffect() 或状态",而 Qwik 没有这个限制。我相信 Qwik 的方法目前是优越的,尽管 RSCs 是朝着正确方向迈出的一步。这并不意味着 Qwik 本身将来一定会成为事实上的框架,但它是未来和前瞻性的,它的方法是解决其他框架(如 Next.js)必须缓解的许多问题。
- 默认情况下,在 Next.js(或任何 React 框架)中,你添加的第三方组件越多,浏览器的捆绑包大小就会越大。这里存在线性关系。然而,在 Qwik 中,有更多的控制,并不是直接的线性关系。默认情况下,除非特别需要,否则不会向浏览器交付任何 JavaScript。 你可以有一个包含图表库的组件,即使该库在页面上被导入,你也可以控制何时加载该库。这意味着,如果有一个只在模态框中使用的图表库,你可以告诉 Qwik 只在打开模态框时才加载该库。这是 Qwik 的一个巨大胜利。在 Next.js 中,你可以通过动态导入来做到这一点,但它并不像 Qwik 那样直接。Qwik 还比我刚提到的情境有更多的控制特性。
- Qwik 允许从客户端的 onClick 异步生成器流式传输服务器响应。 如果你查看 这个示例[25],这绝对是一些魔法般的特性。在 Next.js/React 与 React Server Components 中,通过 React Server Components 来模拟这种行为并非不可能,但不会完全像 Qwik 那样做,因为这是 Qwik 支持这一点的一个基本设计。
- useTask[26] 就像 React 的 useEffect,但是 由于 Qwik 使用了信号,其使用方式比 React 中的 useEffect useState 直接得多。它的样板代码更少,逻辑更清晰。
结论:Qwik 框架赢得了胜利
你选择 Next.js 或 Qwik 都不会出错。两者都有出色的文档,都有动力,都在生产中使用。虽然我认为 Qwik 在许多技术领域都表现出色,但我真正兴奋的是开发框架时那种难以捉摸的感觉。并不是每个框架或语言都有那种难以捉摸的感觉。Qwik 有,每次使用它编码时都感觉很棒。
本文译自:https://outshift.cisco.com/blog/qwik-vs-nextjs
Reference
[1]
Qwik: https://qwik.dev/
[2]
Next.js: https://nextjs.org/
[3]
可恢复性是关于: https://qwik.dev/docs/concepts/resumable/
[4]
useVisibleTask$: https://qwik.dev/docs/components/tasks/#usevisibletask
[5]
isBrowser: https://qwik.dev/docs/guides/qwik-nutshell/#isbrowser-conditional
[6]
Think Qwik: https://qwik.dev/docs/concepts/think-qwik/
[7]
Resumable: https://qwik.dev/docs/concepts/resumable/
[8]
Progressive: https://qwik.dev/docs/concepts/progressive/
[9]
Reactivity: https://qwik.dev/docs/concepts/reactivity/
[10]
All Things Open Conference: https://2023.allthingsopen.org/
[11]
文档: https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#moving-client-components-down-the-tree
[12]
qwikify: https://qwik.dev/docs/integrations/react/
[13]
迁移策略: https://qwik.dev/docs/integrations/react/#use-qwikify-as-a-migration-strategy
[14]
空闲: https://qwik.dev/docs/integrations/react/#clientidle
[15]
加载、空闲、悬停等: https://qwik.dev/docs/integrations/react/#adding-interactivity
[16]
Chart.js: https://www.chartjs.org/docs/latest/getting-started/
[17]
issue: https://github.com/vercel/next.js/issues/45054
[18]
Vite: https://vitejs.dev/
[19]
Why Vite: https://vitejs.dev/guide/why.html
[20]
SWC: https://nextjs.org/docs/architecture/nextjs-compiler
[21]
Turbo: https://nextjs.org/docs/app/api-reference/next-config-js/turbo
[22]
Next.js 的 loading-ui-and-streaming 文档: https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
[23]
server streaming: https://qwik.dev/docs/server$/#streaming-responses
[24]
GitHub issue: https://github.com/BuilderIO/qwik/issues/4178
[25]
这个示例: https://qwik.dev/docs/server$/#streaming-responses
[26]
useTask: https://qwik.dev/docs/components/tasks/#track