作者:狼叔
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个方面总结出来的。
next.js是什么?有哪些优点?为啥狼叔觉得它看起来像一个海王?
对比cra,umi和next.js,它们之间的差异是什么?
next.js生态除了vercel,还有rust和blitzjs,你都了解吗?
实现一个框架有哪4方面的思考?
在服务端渲染领域,对比next.js和ykfe/ssr,有何异同?
内容有点多,大家需要有点耐心。
针对上面的点评,还需要明确一下next.js的介绍要点,不冲突
基于React,支持csr、ssr、isr、ssg等渲染或用于渲染的生成方式
支持ssr,但只是next.js的一个场景而已
next.js是Node web领域优秀的ssr框架,这只是其一,其实2019年之后,next.js也开始支持serverless了
搭配vercel部署,对serverless支持极好
开箱即用,简单易用
下面看一下next的基本特性,如下。
简单汇总一下。
基础页面,数据获取,布局,国际化等,都已经是前端领域最佳实践了,这点上umi和next几乎一模一样。各种优化手段,图片,字体,脚本等,基本上都是最潮最新的好东西。
文件即路由,从ruby on rails开始,前端也借鉴了,umi大概也是借鉴了next的。ykfe/ssr也是借鉴了的。
自动按页面拆分代码,其实是webpack做的事儿,NextJS 将每个页面单独打包,打开首页时会加载应用基础代码和首页代码,其它页面代码只会在打开时才去加载,这对于大型应用来说非常有用。
静态页面导出,可以将整个 NextJS 应用导出为一个静态网站,完美的JamStack
CSS-in-JS,内置了 styled-jsx 方案,样式写法遵循标准,并且样式作用域局限于组件内部而不是全局,避免了组件之间样式互相影响。
在今天,习惯Umi类脚手架和react开发的人,基本上会认为这些是标配,事实上,我们是需要感谢next.js团队的开拓之功的。
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。
easywebpack在使用上,我不认可使用 egg 的worker来做。easy-team的方案本地开发采用了在Node中启动webpack编译bundle的方案,将服务端bundle打包在内存中,agent进程通过memory-fs提供的api来读取文件内容,并且通过worker与agent进程的通信,来让worker进程可以获取到文件的字符串内容。然后采用了Node的runInNewContext api,类似于eval的方式去执行js字符。
next.js写法上是完美的。将请求方法抽取成静态方法,可以复用。另外在服务端渲染,先执行请求方法,是最高效的方式,比Biglet用的对象方法好。
在打包构建方面,本项目本地开发采用的方案为直接将服务端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 将会做以下操作优化性能。
内置 Proxy 服务把它压缩成 100px 与 200px 两张图,大幅度压缩体积
内置 Proxy 服务把它转成 webp,一个体积更小的图片格式 (比 jpg 小 30% 的体积)
内置 Proxy 服务把它转成 75% 压缩质量的 webp,一个更小的体积与几乎无肉眼可见的图片质量变化
懒加载,看不到图片不加载
按需加载 (不像 Gatsby 那样需要在部署项目前耗费大量精力去压缩图片)
由于内置 Proxy 运行时处理,可支持非本域名上的图片处理
优点说完了,这里说一下它的缺点吧
无法利用 Long Term Cache,浏览器二次加载时图片速度慢,使 CDN 也无法性能最大化
小图片无法内置为 Data URI,大量的小图片将造成多次 HTTP 请求影响性能,比如我的这个网站: 开发者武器库
无法支持多分辨率屏幕
CPU,如果部署在 Vercel 可以利用它的服务器资源做缓存服务,如果自部署处理图片需要消耗 CPU。但这个也不算很缺点,只是引入了复杂状态,国内可以利用 Ali_OSS 或公司共有 Image Proxy 做图片缓存服务 自定义一个 loader,详见文档 https://nextjs.org/docs/api-reference/next/image#loader
还有一个算不上缺点的具有迷惑性的点:虽然响应的后缀名是 png,但是返回的 MIME 是 image/webp
感谢山月的分享内容。
avif是下一代图片 image编解码格式,AVIF由开源组织AOMedia开发,Netflix、Google与Apple均是该组织的成员。看到没,几个大佬都在,因此是一统天下的图片格式。AVIF是基于AV1的新图像格式,使用HEIF作为容器(和Apple的HEVC一样)和AV1帧,压缩质量还真是叹为观止,而且还支持JS解析。在Next 12版本里,增加了对avif的支持,是不是做的极为贴心?像不像讨好开发者的海王?哈哈哈。
justjavac说next和vercel不能分开看,这个观点我是认同的。从文档里就可以看出来,结合vercel亲爹的first-class支持,真的是用起来特别爽。
另外,关于for productin里也有很多优化。
Vercel的核心特性
开发,基于next.js和serverless
预览,基于serverless和cdn能力快速预览效果
分发,真实发布
如果把nextjs和vercel整个链路放到一起看,你会发现,它整合的是开发和运维,让开发体验更流程,这部分内容在讲next.js的野心一节细讲。
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 totalrust: 0.00s user 0.00s system 66% cpu 0.006 total# tokenize bootstrap-reboot.css ~45xjs: tokenizer/small(7K) 3.063msrust: tokenizer/small(7K) 0.068ms# tokenize bootstrap.css ~26xjs: tokenizer/fairly_large(201K) 25.672msrust: tokenizer/fairly_large(201K) 0.979ms
这性能也是没谁了,随随便便20倍以上的提升,做cpu密集型任务,rust真的有天然优势。
正是因为postcss-rs是rust写的,且性能很好,于是vercel的ceo就主动找justjavac,想收编。
综上所述,你看rust写编译器,运行时是js,这样才是最合理最高效的。一个商业公司,将开发体验作为目标,进一步拉拢开发者,是对商业和开发者都有利的事儿,必然会得道多助的。
从服务端到浏览器,将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,有更详细的描述。
先解释一下文章里用到的英文缩写:
CSR:Client Side Rendering,客户端(通常是浏览器)渲染
SSR:Server Side Rendering,服务端渲染
SSG:Static Site Generation,静态网站生成
ISR:Incremental Site Rendering,增量式的网站渲染
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 存在部分缺陷:
对于没有预渲染的页面,用户首次访问将会看到一个 fallback 页面,此时服务端才开始渲染页面,直到渲染完毕。这就导致用户体验上的不一致。
对于已经被预渲染的页面,用户直接从 CDN 加载,但这些页面可能是已经过期的,甚至过期很久的,只有在用户刷新一次,第二次访问之后,才能看到新的数据。对于电商这样的场景而言,是不可接受的(比如商品已经卖完了,但用户看到的过期数据上显示还有)。
为了解决 ISR 的一系列问题,Netlify 在前段时间发起了一个新的提案:DPR,Distributed Persistent Rendering,感兴趣的自己去看.
感谢starkwang的分享。
这是csr和ssr中间折中灯下黑之地,属于优化范围内的,它很实用,但也有后遗症,一旦加了isr,在api层面必然会增加更多写法和兼容,比如引入getStaticPaths,getStaticProps,我不认为这是明智之举。
从用户角度讲,当然是直接用最好,从这个角度看,next做好社区需求,这是非常好的。但从框架定位上来看,克制一点,遵循KISS原因会更好。这种纠结,大概只有我愿意讲,其实商业公司本质上是逐利,在服务好客户的前提下做技术,这就好比大公司在kpi体系下做开源是一样的。next是一门解决用户问题的不可多得的好技术框架。这就够了。大家都不容易,next团队是真心热爱技术的,像我这样鸡蛋里挑骨头的不多。
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.tsximport { Link, Routes, useRouter, useMutation, BlitzPage } from "blitz"import Layout from "app/core/layouts/Layout"// Notice how we import the server function directlyimport 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 = trueNewProjectPage.getLayout = (page) => <Layout>{page}</Layout>export default NewProjectPage
crud也特别干净
// app/projects/mutations/createProject.tsimport { resolver } from "blitz"import db from "db"import * as z from "zod"// This provides runtime validation + type safetyexport const CreateProject = z .object({ name: z.string(), })// resolver.pipe is a functional pipeexport 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构建了全栈领域的实践。
我在知乎上回答了《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基础上,整合开发发布链路,它为未来端开发指明了发展方向,这是趋势领导者。
定位开箱即用,满足绝大部分场景,我的就是最好的,你不需要定制,所以它说提供了能力,但你定制起来极难,只要上了贼船,基本上下不来。
易用性做到极致,你想要的功能它都有,比如image,数据分析,动态分隔等,真的是不能再贴心了。这也是为啥我觉得它像海王的原因。这是好话,懂用户的技术产品才是好产品。
特定场景没有深耕,无伤大雅。当然,易用性之外再提深度,容易挨打。
这就是典型的入口和平台思维。构建了核心能力,易用性上让你上瘾,即使现在贴钱做市场也是值得的。另外整合研发链路,简化流程,进一步锁客。我想,这才是next.js的真正的商业价值。
编写一套代码,可以在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框架,这方面做得不够完美,其实也还好。
前端框架和基本探索稳定后,大家就开始想如何更好的用,更简单的用。各家大厂都在前端技术栈思考如何选型和降低成本,统一技术栈。
关于 Webpack 的封装实践有很多,比如知名的 af-webpack,ykit,easywebpack。
af-webpack 是支付宝定制的Webpack,把 webpack-dev-server 等Node.js模块直接打包进去,同时对配置做了更好的处理,以及插件化
ykit 是去哪儿开源的Webpack,内置 Connect 作为Web server,结合dev和hot中间件,对于多项目构建提效明显,对版本文件发布有不错的实践
easywebpack 也是插件化,但对解决方案,boilerplate 等做了非常多的集成,比如egg的ssr是有深入思考的,尽管不赞同这种做法。
在 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的过度设计问题,我的思考如下。
cra、umi、next.js的调试问题,webpack配置封装在框架里,要么eject,要么自己扒源码,那个过程还是非常痛苦的。
定制问题:next.js里按照它的方式做,是极好用的。可是想定制点啥,你发现很难实现。除了代码量很大外,很有可能牵一发而动全身
黑盒:比如next/dynamic这种单独封装,其实就react-loadable,用的时候如果文档里没有,能找死。
甜心:像内置的组件封住,是很好用的,比如image,但很明显这是前端react代码里要处理的,如果内置了,是有益处,但也会面临一个惯性,后面各种前端的优化是不是next都要集成,一旦集成就会有csr和ssr等场景兼容问题。类似于Next.js Analytics,这种统计类的功能,它很贴心,但从职责上看,放到框架是否合适,我理解这也是需要考虑的。
我说它是过度设计,是因为最初做某c端项目选型的时候认真的考虑过,但经过分析,它确实不适合我们的场景。如果大家在next.js自身推荐的场景外,想深度定制的话,必然会遇到上面的。
之所以说next是过度定制,是因为我想深度定制。其实,next本身是不建议深度定制的,它希望的是开箱即用,这样才是它推荐的使用方式。
如果想要实现一个框架,在今天看起来,还是挺难的,如下图。
你需要考虑4个问题:
解决的场景:csr还是ssr,还是都有?
支持的前端框架:react or vue2 or vue3,还是像next一样只支持react
构建:webpack和vite,你总要选一样,至于esbuld或swc是可选的,或者想next一样能用rust重写工具链
服务端运行时:传统Node.js web应用or Node Faas,好像有一种不支持都不好意思说自己的框架还不错
如果以next.js为例,你会发现它除了vue,都内置了。易用性做到极致,打通开发和发布链路,构建已Rust为核心的开发体验,这才是真的现代。
如果不考虑易用性,我会说:“好的框架是克制的,unoption的,技术栈无关的”。如果站在易用性的角度上看,约定大于配置也许是更好的选择。
通过上面的讲解,相信大家能够理解我说的:“从架构的角度,我以为Next.js是过度设计,从商业的角度,我认同Next.js的做法,易用性应该是开发领域最该重视的核心”。Next.js是一款优秀的更Modern的框架,值得大家学习和使用。
此时,我们再回头看上面提出的5个问题:
next.js是什么?有哪些优点?为啥狼叔觉得它看起来像一个海王?
易用性极佳,对用户需求理解的极好
重视开发和发布体验
使用Rust解决开发体验
对比cra,umi和next.js,它们之间的差异是什么?
cra是react开发简化的代表
umi借鉴了next和cra,同时对antd支持极好,国内最佳react脚手架
next不只是ssr框架,支持多种渲染模式,它的野心极大,想变成未来开发者的唯一入口
next.js生态除了vercel,还有rust和blitzjs,你都了解吗?
rust生态,提升开发体验,但仅限于工具链。js依然是应用层最受欢迎的语言
blitzjs是基于next,但加入了访问数据的能力,全栈型框架
实现一个框架有哪4方面的思考?
场景、前端框架选型、服务端运行时,构建
好的框架是克制的,unoption的,技术栈无关的
如果站在易用性的角度上看,约定大于配置也许是更好的选择
在服务端渲染领域,对比next.js和ykfe/ssr,有何异同?
易用性和通用性next更好,ssr细分领域专业程度ykfe/ssr更好,此处留在下一篇讲ykfe/ssr的时候在讲
你理解多少呢?
说明
- 引用了Starkwang、Parabola、山月的部分知乎内容,文中已标出
- 感谢justjavac(postcss-rs以及rust部分知道)、张宇昂(ykfe/ssr部分)纠错