Qwik vs. Next.js:你的下一个 Web 项目应该选哪个框架?

作者 | Samuel Mendenhall

译者 | 平川

策划 | Tina

Qwik 是我进行 Web 项目开发的首选框架,而不是 Next.js。在本文中,我将探讨 Qwik 和 Next.js 的区别、优缺点。不过,我相信,由 Builder.io 创建的 Qwik 有潜力成为 Web 开发的未来。

为什么 Qwik 成为我的首选框架

最终,我选择了 Qwik 而不是 Next.js,原因有很多,其中包括开发经验、信号、可控程度、使用广大 React 生态系统的能力,以及 Qwik 框架的前瞻性特性。Next.js 是一个非凡的框架,我会毫不犹豫地推荐它。然而,Qwik 提供的开发体验是如此的引人入胜,设计是如此的新颖,以至于每次使用它编写代码我都会感到非常兴奋!

背景:从 jQuery 到 Qwik

作为一名全栈工程师,我在软件工程领域工作已经快 20 年了。我的前端之旅始于大约 15 年前。从纯 JavaScript 和 jQuery 开始,然后转向了 KnockoutJS、AngularJS 和 GWT。2013 年,React 出现,我成了一个非常早期的使用者,并从此爱上了它。近 10 年来,React 一直是我的首选库。在这个过程中,我也使用过各种其他的框架和库,但 React 一直是我事实上的前端库,直到今年我发现了 Qwik。

Qwik 是什么?

让我们看一下,Qwik 的文档是如何定义自己的:“Qwik 是一种具有可恢复性的新框架(没有 JS 的立即执行,也没有水合),为边缘而生,为 React 开发人员所熟悉。”这是什么意思呢?让我们来分析一下。

Qwik 使用了 JSX,所以感觉和 React 很像,但它有一个非常典型的特性:可恢复性。“可恢复性是指暂停在服务器上的执行,然后在客户端上恢复,而且无需重播和下载所有应用程序逻辑。”换句话说,就是可以渲染、暂停、恢复、渲染、暂停、恢复等等。

在大多数情况下,这对开发人员来说是透明的,不会增加复杂性。Qwik 和其他框架的根本区别就在于此。举例来说,在 React 中,页面在服务器上渲染,然后在客户端上水合,等所有必要的 JavaScript 都下载完成后,页面就可以交互了。当然,有一种例外情况是使用动态导入,但那仍然与可恢复性不同。

Qwik 的设计宗旨是使客户端 / 服务器之间的边界基本不再成问题。默认情况下,一切都在服务器上渲染,除非你特别使用函数(比如搭配使用 useVisibleTask$ 和 isBrowser)强制在客户端渲染。否则,除了少数特殊情况外,一般所有服务器渲染都是奏效的。

上述内容只是冰山一角。建议通过下面的 Qwik 文档链接详细了解相关概念,因为 Qwik 真的是一个非常独特的框架,可以解决其他框架中一直在设法缓解的问题。

Qwik 简介

Qwik 是一个相当新的框架,刚出现没几年。到目前为止,开发人员对它的讨论还比较少。我也是最近才在 All Things Open Conference 大会上发现了它。如果这是你第一次接触 Qwik 框架,还请花时间通读下文档。你会发现,这么做是值得的。

Qwik 是什么?

关于 Next.js 的文章很多,所以我就简单地说下。Next.js 是一个封装 React 库的重要框架。它是当前 React 开发的首选框架。按照其文档的说法,“Next.js 是一个用于构建全栈 Web 应用程序的 React 框架。开发人员可以使用 React Components 构建用户界面,使用 Next.js 开发附加功能并进行优化。在底层,Next.js 做了抽象,可以自动配置 React 所需的工具,比如打包、编译等等。这使得开发人员可以专注于应用程序构建,而不用把时间花在配置上。”

Qwik vs. Next.js

下面我从 7 个方面对 Qwik 和 Next.js 做了比较。对于每一个方面,我都会说明哪个框架更好。这样你就可以根据对你而言最重要的东西来评估每一个特性。

服务器 vs. 客户端

Next.js 对服务器和客户端组件做了非常明确的区分,而在 Qwik 中,在很大程度上,这完全不是个问题。在默认情况下,所有内容基本上都是在服务器渲染的,我认为这是件好事。

胜者:Qwik。

下面是 Next.js 文档 中的一个例子:

// Next.js code below

// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'
 
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <SearchBar />
      </nav>
      <main>{children}</main>
    </>
  )
}

// ---
'use client'

export default function SearchBar({ children }: { children: React.ReactNode }) {
  return (
    <>
      <main>Search!</main>
    </>
  )
}

// ---
'use client'

export default function Logo({ children }: { children: React.ReactNode }) {
  return (
    <>
      <main>Logo!</main>
    </>
  )
}

在 Qwik 中则无需定义“use client”:

// Qwik code below
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 允许开发人员像下面这样失效缓存:

// Next.js code below

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$s(在当前的页面层次结构中获取调用)。这是有效的,但是缺少细粒度控制。

React 生态系统

Next.js 生来就与整个 React 生态系统做了原生集成。Qwik 可以通过 qwikify$ 函数访问广大的 React 生态系统。但按照 Qwik 文档的说法,应该将此视为一种 迁移策略。这是因为,封装在 qwikify$ 中的任何 React 组件都是单独渲染和水合的,这可能会影响性能。不过,相应地,Qwik 为这种水合提供了很大的灵活性。例如,你可以告诉 Qwik 等到浏览器 空闲 时才水合 React 组件。除了空闲之外,Qwik 还提供许多其他的控制机制。

Qwik 另一个不错的特性是,在渲染包含该组件的页面之前,它甚至不会拉取 React 库。对于页面 B 上的 qwikified React 组件,在浏览器渲染该页面并且满足各种条件之前(比如它在页面上可见),Qwik 将永远不会加载 React 库。Qwik 提供的控制比 Next.js 多。虽然 qwikify$ 被认为是一种迁移策略,但它很有效,你可以通过各种方法来减轻任何潜在的性能问题。

胜者:Qwik。

// Next.js code below

'use client'
 
import { Carousel } from 'acme-carousel'
 
export default Carousel

// ---

import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
 
      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}

你会注意到,在 Next.js 中,你不能在服务器组件中直接使用客户端组件,你必须将第三方组件封装在另一个有“use client”的组件中。这个情况与 Qwik 类似,但是可控程度更高。对于 Qwik 的方法,我真正喜欢它的地方是其对水合的控制。在这里,Next.js 控制能力弱甚至没有,而 Qwik 允许你在加载、空闲、悬停等情况下控制水合。

// Qwik code below

/** @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>View pictures</p>
      <Carousel />
    </div>
  )
});

图表

在撰写本文时,Qwik 还没有原生图表库。在 React 中,你有大量的库可以选择,甚至是过多了。虽说把像 Chart.js 这样的东西集成到 Qwik 中非常简单,但仍然只能在客户端渲染。为了充分利用 Qwik 的强大功能,需要创建一个可以在服务器端渲染的图表库。在此之前,虽然集成任何图表库都很容易,但都只能在客户端渲染。用户体验还算不错,但怎么说还是少了原生的服务器端渲染选项。顺便说一句,你可能会使用 svg 图表库或手动 svg 来渲染服务器端,但我还没有看到一个正式的 Qwik 图表库可以做到这一点。

得益于 React 生态系统中的原生图表库,Next.js 胜出。

状态管理

Qwik 提供了原生信号。如果你用过信号和 React useState,那就没有什么可比的了。信号轻松获胜。在 Next.js 中获取信号是一个悬而未决的问题,而结论是这需要在 React 库中完成。虽然有一些用户利用“猴补丁”成功地将 Preact 信号集成到了 Next.js 中,但结果似乎好坏参半。

胜者:Qwik。

// Next.js code below

'use client'

function HomePage() {
  // ...
  const [likes, setLikes] = React.useState(0);
 
  function handleClick() {
    setLikes(likes + 1);
  }
 
  return (
    <div>
      {/* ... */}
      <button onClick={handleClick}>Likes ({likes})</button>
    </div>
  );
}
// Qwik code below

export default component$(() => {
  // ...
  const likes = useSignal(0);
 
  return (
    <div>
      {/* ... */}
      <button onClick={() => likes += 1}>Likes ({likes})</button>
    </div>
  );
})

你也可以将信号作为 props 传递给子组件,并在那里修改它们。在没有回调函数的 React 中,直接实现是不可能的。

// Qwik code below

// 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}>Likes ({props.likes})</button>
    </div>
  );
})

服务器

Qwik 使用了 Vite,而 Vite 正成为 Dev 服务器前端工作的主要支柱之一。Vite 提供了一些令人难以置信的特性,比如内置的反向代理和非常有效的模块处理和热模块重载。要了解更多信息,请查阅为什么选择 Vite。使用 SWC、Turbo 构建和开发 Next.js 仍然非常快,但 Vite 在这方面更有优势。

胜者:Qwik。

服务器端渲染

关于这一点,虽然我在“服务器 vs. 客户端”一节中已经介绍过,但我想在这里更深入地讨论下服务器端渲染。

当考虑渲染服务器组件以及浏览器何时从框架接收第一个 HTML 时,情况就复杂了。尽管方式不同,但 Next.js 和 Qwik 完成的任务相同。从表面上看,结果是相同的,只是不同框架特有的控制机制可以提供不同的开发体验。如果你读过 Next.js 的 loading-ui-and-streaming 文档,就会发现你可以利用 React Suspense 来实现 UI 的“即时”加载和渐进式解析。这非常好,Qwik 没有提供类似的即时功能,但你仍然可以实现相同的效果。

根据 Next.js 的说法,“导航是即时的,即使是以服务器为中心的路由。”关于这一点,让我更深入地描述一下其中的核心问题。首先,在服务器端渲染组件加载产品列表,如从某些外部源(很可能)加载产品列表。接下来,框架渲染组件并生成 HTML。在后端完全加载产品列表并生成 HTML 之前,你不会看到页面。因此,如果没有缓存,缓慢的外部 API(假设 5 秒)会使用户在整整 5 秒钟内看不到产品页面的任何 HTML。我们肯定都会同意,这种用户体验很糟糕,浏览器好什么都没做或没有响应。

Next.js 的处理方法是告诉你通过 loading.js 来使用 React Suspense。Suspense 使你可以在加载数据时呈现回退组件。然后,在数据加载完成时,用实际组件替换回退组件。这是一个非常好的特性,带来了很棒的开发体验。

Qwik 的处理方式有所不同。Qwik 有一个名为 routeLoader$ 的函数,它只在服务器上运行。Promise 必须在页面渲染之前完成解析。因此,对于上述产品组件,routeLoader$ 将被调用,而 Promise 将在 5 秒后解析完成,然后才渲染页面。Qwik 中没有类似 Suspense 的概念,但你可以借助 server$ streaming 完成同样的事情。不同之处在于,你必须自己管理数据加载,但同时,你对数据加载有了更多的控制权。例如,你可以加载前 10 个产品,然后渲染页面,然后再加载其余的产品。这是一个人为设计的例子,但可以说明问题。GitHub 上有一个关于 Qwik 的有趣的问题,它演示了一个用流加载数据的例子。你会看到,在 Qwik 中执行此类操作非常复杂。因此,在这方面,Next.js 因其简单性而胜出。

胜者:Next.js,因为它借助 React Suspense 提供了更好的开发体验。不过,Qwik 也可以完成同样的事情,并且提供了更细粒度的控制,只是没有那么丝滑。

我为什么选择 Qwik?

总的来说,Qwik 提供了更好的开发体验——在大多数情况下,你都无需管理服务器和客户端组件——用 Qwik 进行开发更容易。这并不是说 Qwik 特意做了什么抽象。这是 Qwik 的一个基本设计,所有东西最初都在服务器端渲染,除非你明确让它在客户端渲染。无需使用“use client”或“use serve”,它就可以工作,你根本不用考虑这个问题。

虽然 Qwik 生态系统还处于早期阶段,但你可以利用广大的 React 生态系统。是的,水合会有代价,不过在实践中通常可以忽略不计。Next.js 中也存在这种水合成本,而且没有其他选项。让人略感欣慰的是,在 Qwik 中,你都可以控制水合的时间,并且最终可以重写 / 重构 React 组件,使其成为 Qwik 原生组件。

信号优于 React useState,我认为在这一点上不会有太多分歧。如果有的话,可能有人会认为 RxJS 优于信号,不过这需要另外讨论了。

我相信,Qwik 的可恢复性可能会成为未来框架的基础特性。甚至是 React Server 组件所做的事情也是类似的,即渲染完成后将数据序列化到客户端。然而,在 RSC 中,“编写的所有服务器组件代码都必须是可序列化的。也就是说,你不能使用生命周期钩子,比如 useEffect() 或 state”,而 Qwik 没有这个限制。我相信,就目前来看,Qwik 的方法更好,尽管 RSC 也朝着正确的方向迈出了一步。不过,这并不意味着 Qwik 未来一定会成为事实上的框架,但它的方法解决了许多其他框架(如 Next.js)必须缓解的问题。

在默认情况下,在 Next.js(或任何 React 框架)中,你添加的第三方组件越多,浏览器收到的包就越大。这是一个线性关系。然而,在 Qwik 中,开发人员拥有更多的控制权,而不是直接的线性关系。除非特别需要,否则默认是不会向浏览器传递 JavaScript 的。例如,你有一个包含图表库的组件,即使页面导入了这个库,你也可以控制何时加载它。也就是说,如果你有一个仅用于模态的图表库,那么你可以告诉 Qwik 仅在打开模态窗口时加载该库。这是 Qwik 的一个巨大胜利。在 Next.js 中,你可以通过动态导入来做到这一点,但那不像 Qwik 那样直接。Qwik 提供的控制比我刚才提到的场景还要多很多。

Qwik 允许利用客户端 onClick 中的异步生成器对服务器响应做流式传输。如果你看下 这个例子,就会发现这是一种神奇的技巧。在 Next.js/React 中使用 React Server 组件来模仿这一点也不是不可能,但肯定无法做到和 Qwik 完全一样的方式,因为 Qwik 的基本设计就支持这样做。

useTask 和 React useEffect 很像,只是 Qwik 使用了 Signals,比 React 中的 useEffect + useState 要简单许多。它的样板代码少很多,逻辑也更合理。

小结:Qwik 框架获胜

使用 Next.js 或 Qwik 都不会错。两者都提供了很好的文档,都有良好的发展势头,都在生产中广泛使用。虽然我展示了许多在我看来 Qwik 更擅长的技术领域,但真正让我兴奋的是使用这个框架开发时的丝滑感觉。并不是每种框架或语言都能带来这种感觉。Qwik 可以,每次我用它编码时感觉都很棒。

本文文字及图片出自 InfoQ 架构头条

你也许感兴趣的:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注