我看 Next-js:一個更現代的海王

Next.js 是一個用於生產環境的 React 應用框架(官方介紹:The React Framework for Production),使用它可以快速上手開發 React 應用( enables you to build superfast and extremely user-friendly static websites,),而不需要花很多時間和精力去折騰各種開發工具。所謂的用於生產環境,是指功能和穩定性足夠,有大量的實際應用案例。

在整理編輯大人給的《狼書(卷 3):Node.js 高級技術》修訂版,給 next.js 一個明確觀點:" 整體來看,Next.js 在 Node.js Web 開發領域是一個非常優秀的 SSR 框架,其衆多優秀特性,外加 Blitzjs 這種周邊生態,對於開箱即用的項目來說是極好的。從架構的角度,筆者以爲 Next.js 是過度設計,從商業的角度,我認同 Next.js 的做法,易用性應該是開發領域最該重視的核心。"

之所以給出這樣一個觀點,是基於下面 5 個方面總結出來的。

內容有點多,大家需要有點耐心。

1、Next 是什麼?

針對上面的點評,還需要明確一下 next.js 的介紹要點,不衝突

下面看一下 next 的基本特性,如下。

簡單彙總一下。

在今天,習慣 Umi 類腳手架和 react 開發的人,基本上會認爲這些是標配,事實上,我們是需要感謝 next.js 團隊的開拓之功的。

1.1、易用極致

nextjs 很明顯選擇易用性,像一個海王一樣,太懂用戶的心了。所以它也是 for  企業,小客戶和個人開發者的通用方案,從基礎框架,到發佈運維都幫你解決了,是極爲方便的技術,以致於很多人都把它作爲第一梯隊的選擇。

早期的 next.js 寫法是非常簡單的,就只有 getInitialProps 一個靜態方法。

function Page(props) {
  return <div> {props.name} </div>
}

Page.getInitialProps = async (ctx) => {
  return Promise.resolve({
    name: 'Egg + React + SSR'
  })
}

export default Page

在 egg-react-ssr 技術調研期,我們分別看了 next.js 和 easywebpack。

在打包構建方面,本項目本地開發採用的方案爲直接將服務端 bundle 打包到本地硬盤,通過 webpack --watch 的方式,來實現更新,同時本地開發的時候每次加載之前清空 require bundle 的緩存保證刷新後 server 端的內容與 client 端一致。結合 webpack-dev-server,上手難度低,實現代碼更少。

爲了適應更多的渲染細化場景,基於 getInitialProps 都能完成,但 next 又進行了拆分,讓每個寫法都有特定的應用場景。好處是用的人簡單,缺點是增加學習成本。

除了核心特性上,做了渲染場景的細化外,next 還做了非常多的易用性上的小心思,大家討論最多的,大概就是 image 了。下面是山月在知乎上寫的關於 next image 的體驗。

內置 Image Proxy,對圖片進行轉換、壓縮,使得圖片體積最小化。並配合圖片懶加載與 srcset 一系列關於圖片優化的小點子優化網絡體驗 Next 團隊宣傳地也頗爲實在

In order to use images on web pages in a performant way a lot of aspects have to be considered: size, weight, lazy loading, and modern image formats.

舉一個栗子,如果你使用了一張 10000px x 10000px 的 PNG 圖片,放到了 100px x 100px 的小方格里。那麼 next.js 將會做以下操作優化性能。

  1. 內置 Proxy 服務把它壓縮成 100px 與 200px 兩張圖,大幅度壓縮體積

  2. 內置 Proxy 服務把它轉成 webp,一個體積更小的圖片格式 (比 jpg 小 30% 的體積)

  3. 內置 Proxy 服務把它轉成 75% 壓縮質量的 webp,一個更小的體積與幾乎無肉眼可見的圖片質量變化

  4. 懶加載,看不到圖片不加載

  5. 按需加載 (不像 Gatsby 那樣需要在部署項目前耗費大量精力去壓縮圖片)

  6. 由於內置 Proxy 運行時處理,可支持非本域名上的圖片處理

優點說完了,這裏說一下它的缺點吧

  1. 無法利用 Long Term Cache,瀏覽器二次加載時圖片速度慢,使 CDN 也無法性能最大化

  2. 小圖片無法內置爲 Data URI,大量的小圖片將造成多次 HTTP 請求影響性能,比如我的這個網站: 開發者武器庫

  3. 無法支持多分辨率屏幕

  4. CPU,如果部署在 Vercel 可以利用它的服務器資源做緩存服務,如果自部署處理圖片需要消耗 CPU。但這個也不算很缺點,只是引入了複雜狀態,國內可以利用 Ali_OSS 或公司共有 Image Proxy 做圖片緩存服務 自定義一個 loader,詳見文檔 https://nextjs.org/docs/api-reference/next/image#loader

  5. 還有一個算不上缺點的具有迷惑性的點:雖然響應的後綴名是 png,但是返回的 MIME 是 image/webp

感謝山月的分享內容。

avif 是下一代圖片 image 編解碼格式,AVIF 由開源組織 AOMedia 開發,Netflix、Google 與 Apple 均是該組織的成員。看到沒,幾個大佬都在,因此是一統天下的圖片格式。AVIF 是基於 AV1 的新圖像格式,使用 HEIF 作爲容器(和 Apple 的 HEVC 一樣)和 AV1 幀,壓縮質量還真是歎爲觀止,而且還支持 JS 解析。在 Next 12 版本里,增加了對 avif 的支持,是不是做的極爲貼心?像不像討好開發者的海王?哈哈哈。

1.2、生態體系

justjavac 說 next 和 vercel 不能分開看,這個觀點我是認同的。從文檔裏就可以看出來,結合 vercel 親爹的 first-class 支持,真的是用起來特別爽。

另外,關於 for productin 裏也有很多優化。

Vercel 的核心特性

如果把 nextjs 和 vercel 整個鏈路放到一起看,你會發現,它整合的是開發和運維,讓開發體驗更流程,這部分內容在講 next.js 的野心一節細講。

1.3、重倉 rust,改進開發體驗

next12 使用 swc 替換掉 babel 和 ts 部分,性能得到非常大的提升。

In early tests, previous code transformations using Babel dropped from ~500ms to ~10ms and code minification from Terser dropped from ~250ms to ~30ms using SWC. Overall, this resulted in twice as fast builds.

從這點上看,Rust 作爲構建高性能構建工具的語言,是極好的。今天,前端發展到了一個瓶頸,也忍受了很多,比如 webpack 構建,雖然前端也使用了很多優化手段,但 js 編寫的,你只能利用巧妙的設計來優化,這不像 rust/go 這種系統級語言,天生的性能優勢,於是有了 swc 和 esbuild 等編譯工具,這對前端生態來說是極好的。

swc 作者今年畢業,從 deno 轉入職 next 的母公司 vercel。parcel 的一個核心維護者也加入了 vercel,可謂兵強馬壯。

We're excited to announce DongYoon Kang, the creator of SWC, and Maia Teegarden, contributor to Parcel, have joined the Next.js team at Vercel to work on improving both next dev and next build performance. We will be sharing more results from our SWC adoption in the next release when it's made stable.

rust 做一些基礎模塊改造是很好的,合理利用語言本身的優勢。如果都用 rust 去做應用,把之前 js 寫過的 170 萬+模塊都重寫,想想都不現實,今天前端的瓶頸如果是編譯速度,那就真的卷死了。

最近 justjavac 在 postcss-rs,https://github.com/justjavac/postcss-rs 目前已經完成了 tokenizer 功能,性能極好,在單核 CPU 上,數據如下。

js:   0.11s user 0.02s system 126% cpu 0.102 total
rust: 0.00s user 0.00s system  66% cpu 0.006 total

# tokenize bootstrap-reboot.css               ~45x
js:   tokenizer/small(7K)                  3.063ms
rust: tokenizer/small(7K)                  0.068ms

# tokenize bootstrap.css                      ~26x
js:   tokenizer/fairly_large(201K)        25.672ms
rust: tokenizer/fairly_large(201K)         0.979ms

這性能也是沒誰了,隨隨便便 20 倍以上的提升,做 cpu 密集型任務,rust 真的有天然優勢。

正是因爲 postcss-rs 是 rust 寫的,且性能很好,於是 vercel 的 ceo 就主動找 justjavac,想收編。

綜上所述,你看 rust 寫編譯器,運行時是 js,這樣纔是最合理最高效的。一個商業公司,將開發體驗作爲目標,進一步拉攏開發者,是對商業和開發者都有利的事兒,必然會得道多助的。

1.4、渲染模式大而全

從服務端到瀏覽器,將 ssr 和 csr 整個過程梳理完,有 5 種。

我們現在所說的 react ssr 其實是第三者,基於 hydration 做的混合渲染。

Next.js 項目組成員 Parabola 說的:Next 不是 一個 SSR 框架。SSR 只是它的功能之一,它還支持 CSR、SSG、ISR 以及 API 路由(或是以任何方式組合在一起)。

starkwang 在《新一代 Web 建站技術棧的演進:SSR、SSG、ISR、DPR 都在做什麼?》https://zhuanlan.zhihu.com/p/365113639,有更詳細的描述。

先解釋一下文章裏用到的英文縮寫:

  1. CSR:Client Side Rendering,客戶端(通常是瀏覽器)渲染

  2. SSR:Server Side Rendering,服務端渲染

  3. SSG:Static Site Generation,靜態網站生成

  4. ISR:Incremental Site Rendering,增量式的網站渲染

  5. DPR:Distributed Persistent Rendering,分佈式的持續渲染

增量式更新(ISR)的概念,這個概念最早由 Next.js 在 9.5 版本中提出,既然全量預渲染整個網站是不現實的,那麼我們可以做一個切分:

1、關鍵性的頁面(如網站首頁、熱點數據等)預渲染爲靜態頁面,緩存至 CDN,保證最佳的訪問性能;

2、非關鍵性的頁面(如流量很少的老舊內容)先響應 fallback 內容,然後瀏覽器渲染(CSR)爲實際數據;同時對頁面進行異步預渲染,之後緩存至 CDN,提升後續用戶訪問的性能。

頁面的更新遵循 stale-while-revalidate 的邏輯,即始終返回 CDN 的緩存數據(無論是否過期);如果數據已經過期,那麼觸發異步的預渲染,異步更新 CDN 的緩存。

在 Next.js 中,你可以使用 getStaticPaths() 來定義哪些路徑需要預渲染,通過 getStaticProps() 來獲取預渲染需要的數據:

// 定義哪些頁面需要預渲染
export async function getStaticPaths() {
  return {
    // 只有 /posts/1 和 /posts/2 會被預渲染
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    // 其它頁面,如 /posts/3,都會返回 fallback 頁面,然後 CSR
    fallback: true,
  }
}

// 定義預渲染需要的數據
export async function getStaticProps({ params }) {
  // 拉取對應的文章內容
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  return {
    props: { post },
    revalidate: 60 // 數據有效期爲 60}
}

但 ISR 存在部分缺陷:

  1. 對於沒有預渲染的頁面,用戶首次訪問將會看到一個 fallback 頁面,此時服務端纔開始渲染頁面,直到渲染完畢。這就導致用戶體驗上的不一致

  2. 對於已經被預渲染的頁面,用戶直接從 CDN 加載,但這些頁面可能是已經過期的,甚至過期很久的,只有在用戶刷新一次,第二次訪問之後,才能看到新的數據。對於電商這樣的場景而言,是不可接受的(比如商品已經賣完了,但用戶看到的過期數據上顯示還有)。

爲了解決 ISR 的一系列問題,Netlify 在前段時間發起了一個新的提案:DPR,Distributed Persistent Rendering,感興趣的自己去看.

感謝 starkwang 的分享。

這是 csr 和 ssr 中間折中燈下黑之地,屬於優化範圍內的,它很實用,但也有後遺症,一旦加了 isr,在 api 層面必然會增加更多寫法和兼容,比如引入 getStaticPaths,getStaticProps,我不認爲這是明智之舉。

從用戶角度講,當然是直接用最好,從這個角度看,next 做好社區需求,這是非常好的。但從框架定位上來看,剋制一點,遵循 KISS 原因會更好。這種糾結,大概只有我願意講,其實商業公司本質上是逐利,在服務好客戶的前提下做技術,這就好比大公司在 kpi 體系下做開源是一樣的。next 是一門解決用戶問題的不可多得的好技術框架。這就夠了。大家都不容易,next 團隊是真心熱愛技術的,像我這樣雞蛋裏挑骨頭的不多。

1.5、blitzjs

blitzjs 其實就是在 Next.js 之上加入訪問數據庫的能力(Everything End-to-End From the Database to the Frontend),最終形成類似 Ruby on Rails 的一棧式開發框架。

官方簡介

Blitz is a batteries-included framework that's inspired by Ruby on Rails, is built on Next.js, and features a"Zero-API" data layer abstraction that eliminates the need for REST/GraphQL.

如果說,僅僅是加入了訪問 db 的能力,從 next 選一個後端框架,加個 orm 就可以了。blitzjs 是通 prisma 做到 "Zero-API" Data Layer,在技術選型方面,可謂是很有想法的。

下面是一個簡單的創建項目的代碼,可謂乾淨,漂亮。

// app/pages/projects/new.tsx
import { Link, Routes, useRouter, useMutation, BlitzPage } from "blitz"
import Layout from "app/core/layouts/Layout"
// Notice how we import the server function directly
import createProject, {CreateProject} from "app/projects/mutations/createProject"
import { ProjectForm } from "app/projects/components/ProjectForm"

const NewProjectPage: BlitzPage = () => {
  const router = useRouter()
  const [createProjectMutation] = useMutation(createProject)

  return (
    <div>
      <h1>Create New Project</h1>

      <ProjectForm
        submitText="Create Project"
        schema={CreateProject}
        onSubmit={async (values) => {
          // This is equivalent to calling the server function directly
          const project = await createProjectMutation(values)
          // Notice the 'Routes' object Blitz provides for routing
          router.push(Routes.ProjectsPage({projectId: project.id}}))
        }}
      />
    </div>
  )
}

NewProjectPage.authenticate = true
NewProjectPage.getLayout = (page) => <Layout>{page}</Layout>

export default NewProjectPage

crud 也特別乾淨

// app/projects/mutations/createProject.ts
import { resolver } from "blitz"
import db from "db"
import * as z from "zod"

// This provides runtime validation + type safety
export const CreateProject = z
  .object({
    name: z.string(),
  })

// resolver.pipe is a functional pipe
export default resolver.pipe(
  // Validate the input data
  resolver.zod(CreateProject),
  // Ensure user is logged in
  resolver.authorize(),
  // Perform business logic
  async (input) => {
    const project = await db.project.create({ data: input })
    return project
  }
)

另外,從項目,代碼質量,社區活躍度上看,這都是一個非常優秀的項目。

我以爲他最大的價值是爲 next.js 構建了全棧領域的實踐。

2、next.js 的野心

我在知乎上回答了《2021 前端會有什麼新的變化?》,單篇 39.8 萬的閱讀量,還是不錯的,這裏再講我對 Node.js 相關內容的看法。如果是今年選 Node.js Web 框架,大概只有 Midway、Nest 和 next.js 了

Node.js Web 框架 2021 年 8 大類分析

這裏講 next.js 最有野心的原因,原因從 2013 年到 2017 年,前端快速發展,整體難度是變大了很多,從 2018 年之後,創新放緩,慢慢的大家都在做沉澱和降低成本的事兒。從 cra 和 umi,固化 webpack 最佳實踐,可以看出前端自身的簡化方面做出的努力。next.js 除了前端簡化外,還在 ssr 等做了場景上的增強,打通從開發到部署的一站式開發,你只需要通過 git 提交代碼,自動部署。站在前端和 Node 基礎上,整合開發發佈鏈路,它爲未來端開發指明瞭發展方向,這是趨勢領導者。

這就是典型的入口和平臺思維。構建了核心能力,易用性上讓你上癮,即使現在貼錢做市場也是值得的。另外整合研發鏈路,簡化流程,進一步鎖客。我想,這纔是 next.js 的真正的商業價值。

3、ssr 降級問題

編寫一套代碼,可以在 ssr 崩潰是做兜底,就需要 csr 也可以支持。於是有了 ssr 可以無縫降級 csr 的特性。這是在真實大流量場景下,才需要考慮的事兒,之所以要做這個功能是真實需求裏誕生的。

按照 next.js 的邏輯,它可以 ssg,然後在網關層把 ssr 和 csr 做降級處理,理論上也可以實現,只是不那麼優雅和絲滑,不符合它之前在易用性做的那麼好的一貫認知。事實上,從 getInitialProps 上分裂那麼多細分方法,就註定這條路被堵死了。大概這也是框架設計者的取捨吧。

要做到無縫降級,最需要處理的就是數據獲取的問題,什麼時候應該在哪個端去加載數據。這塊的邏輯應該是在框架層去做的。Next.js 通過 next/router 的方式劫持開發者的 push/pop 操作,在其中做非常多黑盒複雜的邏輯,我認爲這非常的不優雅。事實上,在我們的項目中我們僅編寫了一個高階組件來完成這個功能。

在寫法上,可以支持 csr 和 ssr,在構建上更加簡單,結合我們在使用的 egg.js,就誕生了 egg-react-ssr。後面加了插件化和 serverless,就有了 ykfe/ssr。這裏不展開講,避免有蹭熱度之嫌。

不過 next 已經不只是 ssr 框架,這方面做得不夠完美,其實也還好。

4、對比三巨頭:cra、umi 和 next

前端框架和基本探索穩定後,大家就開始想如何更好的用,更簡單的用。各家大廠都在前端技術棧思考如何選型和降低成本,統一技術棧。

關於 Webpack 的封裝實踐有很多,比如知名的 af-webpack,ykit,easywebpack。

在 create-react-app(cra)項目裏使用的是 react-scripts 作爲啓動腳本,它和 egg-scripts 類似,也都是通過約定,隱藏具體實現細節,讓開發者不需要關注構建。在未來,類似的封裝還會有更多的封裝,偏於應用層面。

umi 是一個和 cra 類似的具有螞蟻特色的腳手架,它借鑑了 next.js 和 cra 的優點,同時基於 af-webpack,便於定製 webpack 配置,同時對 antd 經典 ui 庫支持最好,使得 umi 在螞蟻內部以及開源社區有相當大的用戶量。umi 同時支持 csr 和 ssr,ssg 等,功能還是非常強大的。

next.js 早期和 umi 很像,但新版不斷迭代,在易用性上做的非常好,外加 vercel(以前的 now.sh)搭配,真的是上手簡單,發佈更簡單。

next.js 的過度設計問題,我的思考如下。

我說它是過度設計,是因爲最初做某 c 端項目選型的時候認真的考慮過,但經過分析,它確實不適合我們的場景。如果大家在 next.js 自身推薦的場景外,想深度定製的話,必然會遇到上面的。

之所以說 next 是過度定製,是因爲我想深度定製。其實,next 本身是不建議深度定製的,它希望的是開箱即用,這樣纔是它推薦的使用方式。

5、我眼中好的框架設計

如果想要實現一個框架,在今天看起來,還是挺難的,如下圖。

你需要考慮 4 個問題:

如果以 next.js 爲例,你會發現它除了 vue,都內置了。易用性做到極致,打通開發和發佈鏈路,構建已 Rust 爲核心的開發體驗,這纔是真的現代。

如果不考慮易用性,我會說:“好的框架是剋制的,unoption 的,技術棧無關的”。如果站在易用性的角度上看,約定大於配置也許是更好的選擇。

6、總結

通過上面的講解,相信大家能夠理解我說的:“從架構的角度,我以爲 Next.js 是過度設計,從商業的角度,我認同 Next.js 的做法,易用性應該是開發領域最該重視的核心”。Next.js 是一款優秀的更 Modern 的框架,值得大家學習和使用。

此時,我們再回頭看上面提出的 5 個問題:

你理解多少呢?

說明

- 感謝 justjavac(postcss-rs 以及 rust 部分知道)、張宇昂(ykfe/ssr 部分)糾錯

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/5Ir7EoHLo37bs6W5WNa-Tw