Next面试题及解析
Next.js 与 React
1. 常见问题
1.1 Next.js 与 React 技术选型的区别在哪里?使用 Next.js 比使用 React 有什么优势?
1.2 解题思路
考察对 Next.js 和 React 的基本理解。其实 React 和 Next.js 的差别可以说很多,也可以一句话概括。重点不在于列出所有不同点,而在于说明核心区别,并引申到技术选型的选择或实际使用经历。最好在使用经历中埋下一些引导词,吸引面试官继续提问。
1.3 八股答案
React 是一个用于构建用户界面的 JavaScript 库,提供了 JSX 语法用于快速创建可复用的组件,并基于数据驱动的理念,建立单向数据流,简化了调试流程。底层使用虚拟 DOM 和 Fiber 优化渲染性能。
Next.js 是一个基于 React 的用于构建全栈 Web 应用程序的生产框架。不仅可以使用 React 构建用户界面,还可以使用 Next.js 的各种功能和优化,使用户可以专注于构建应用程序,而不是花时间进行配置。
从使用的角度来看,React 关心的是状态管理和渲染,所以 React 是一个库,通常需要搭配其他库一起使用。而 Next.js 则是一个大而全的框架,提供了开箱即用的脚手架,内置了各种优化组件和 API。Next.js v12 实现了 SSR、SSG、ISR,Next.js v13 基于 React Server Components 重构,实现了 App Router、服务端组件和客户端组件、Server Actions、路由处理程序、中间件、内置各种 CSS 支持等功能。
使用 Next.js 的优势:
- 开箱即用的脚手架,零配置,支持 TypeScript、ESLint、Tailwind.css 等常用技术选型,快速上手使用。
- 内置各种优化组件,如图片、字体、脚本、链接等。
- SEO 友好,基于服务端组件和客户端组件进行客户端渲染和服务端渲染,自动进行静态、动态路由优化,支持流式传输。
- 基于文件的路由系统,支持布局、嵌套路由、加载状态、错误处理等高级路由功能,解决了复杂场景下的路由实现。
- 内置多种 CSS 支持,如 CSS Modules、Tailwind CSS 和 CSS-in-JS。
1.4 回答参考
React 是一个纯前端库,而 Next.js 是基于 React 的全栈框架。
传统的前后端分离架构,虽然带来了“前端”的繁荣,但也带来了诸如 SEO 优化、首屏加载时间过长等问题,由此诞生了 Next.js,主要解决了服务端渲染以及内置了各种全栈开发会用到的组件、工具、API 等,提高了开发效率。(简明扼要的说明区别就好)
我大概是 2020 年接触的 Next.js,这几年也用 Next.js 做过一些项目,我对 Next.js 的理解就是大而全的全栈框架,既可以写页面,又可以写接口,一把梭全部搞定。(假装自己这几年持续有关注,一直在学习实践)
我认为 Next.js 作为一个生产框架,追求的就是一个字——“快”。(说出自己的真实感受,有自己的理解,而不是直接背答案,吸引面试官)
当然这个“快”有两方面的解释,一个是应用程序本身性能上的快,比如它从 SSR 到 SSG 到 ISR 以及 v13 基于 RSC 进行重构,推出了 App Router,我理解都是为了进一步提升应用程序的性能。(抛出技术名词,吸引面试官提问如 SSR 原理、App Router、RSC 方向)
第二个“快”是开发效率上的快,比如它提供开箱即用的脚手架,内置各种优化后的组件比如图片、字体等,像图片组件我也可以自己写,自己实现懒加载,但是 Next.js 直接内置了,这就便捷了很多。此外 Next.js 还内置了各种 CSS 支持,比如 CSS Module,Tailwind CSS,当然我个人只用 Tailwind CSS。
还有 App Router,除了简化常见的加载状态、错误状态处理等,还提供了拦截路由、平行路由等高级路由功能,解决了特定场景下的路由问题。加上 Next.js 背靠 Vercel,部署也很方便,我基本做全栈的个人项目都是用 Next.js。(抛出一堆技术名词,继续吸引面试官提问,尽可能让面试官在自己熟悉的技术领域提问,而不要回到他擅长但你不一定擅长的技术领域)
2. 服务端渲染
2.1 常见问题
什么是服务器端渲染(SSR)?SSR 的原理是什么?了解渐进式渲染和流式渲染吗?
2.2 解题思路
考察对 SSR 实现原理的理解,这种对原理的考察是为了了解面试者的学习深度,是否在实现应用之外,有对实现原理有所了解。为了表达出自己真实研究过,可以引申到自己的实际研究经历。
2.3 八股答案
服务器端渲染 (SSR) 是一种网页渲染技术,它在服务器端生成完整的 HTML 页面,然后将页面发送给浏览器。与客户端渲染 (CSR) 不同,SSR 在服务器端完成页面渲染,浏览器只需解析和显示 HTML 页面即可。
SSR 的工作原理:当用户请求一个页面时,服务器会执行以下步骤:
- 获取页面数据和模板。
- 使用数据和模板生成完整的 HTML 页面。
- 将 HTML 页面发送给浏览器。
- 浏览器解析和显示 HTML 页面。
SSR 的优势:
- 更快的首屏加载速度: 由于浏览器无需等待 JavaScript 下载和执行,SSR 可以提供更快的首屏加载速度。
- 更好的 SEO: 搜索引擎可以更容易抓取和索引 SSR 生成的页面,从而提高网站的 SEO 排名。
- 更易于访问: SSR 生成的页面无需 JavaScript 即可访问,这对于禁用 JavaScript 的用户或低带宽连接的用户来说非常有用。
流式渲染是指将 HTML 文档分块传输给客户端,客户端收到每一段内容时进行分批渲染。与一次性发送整个 HTML 文档相比,流式渲染可以更早地向用户展示内容,提升页面加载速度和用户体验。
流式渲染适用于各种需要快速展示内容的场景,例如:
- 大型电商网站的商品列表页面
- 社交媒体的动态消息流
- 实时数据更新的仪表盘
流式渲染可以通过多种技术实现,例如:
- HTTP/1.1 的分块传输编码
- WebSockets
2.4 回答参考
所谓服务端渲染就是……在服务端进行渲染……
这样说好像有点草率……不过服务端渲染不就是这个意思吗……(这看似草率的回答说明我可没有背八股……)
其实我以前用 Webpack 和 React 实现过 React SSR (参考《源码篇 | 手写 React SSR》,至于你是否真的实现过,你讲得清原理就可以说成自己真的实现过)。
其实 React 的 react-dom/server 提供了服务端渲染的 API,像 renderToString,当然也有用于流式渲染的 renderToPipeableStream 等 API,服务端渲染成具体的 HTML 后传给客户端,同时 React 提供了用于客户端的 hydrateRoot API 进行水合,实现客户端事件的绑定,就这样完成了一个基本的 SSR。
当然这是最早期的 SSR 实现,虽然解决了 SEO 等问题,但还有很多问题。(SSR 没有什了不起的,且听我和你逼逼)
首先,SSR 需要在服务端完全渲染完毕后才能传给客户端。其次,为了保持服务端组件树和客户端组件树一致,所有的组件代码都要打包到客户端 bundle 中。最后,一旦开始水合,整个过程是阻塞的,必须全部完成水合后,用户才能开始操作。
为了缓解这个问题,就出现了 Streaming SSR,也就是您问的流式渲染。React 用的是 Suspense 组件来实现的,Suspense 的巧妙之处就在于它在渲染的时候先用一个占位符替代,等数据获取完毕的时候,在流式传输给 HTML,用脚本替换之前的占位符,从而实现渐进式渲染内容。
这个实现很好,在我看来,解决了 2 个问题:
一个是组件并不一定非要在服务端渲染完毕再传输给客户端组件,可以边渲染边传输,体验更好。第二个是选择性水合,页面分多个部分进行水合,甚至可以根据用户操作提高水合的优先级。
但问题并没有完全解决,客户端该下载的代码并没有少,所有的组件都需要水合,哪怕它只是需要静态渲染并不涉及客户端操作,也要水合一遍,这就浪费了性能。目前的终极解决方案就是 RSC。
3. React Server Components
3.1 常见问题
RSC 的实现是为了解决什么问题?说说你对 RSC 的理解?
3.2 解题思路
依然考察的是对原理的理解,毕竟 RSC 是 Next.js v13 的实现基础,如果没有接触过 React,可能对 RSC 感到陌生。
3.3 八股答案
RSC 是 React 设计的一种新架构,这种方法旨在利用服务器和客户端环境的优势,实现用户体验、易于维护和高性能的三角平衡。
使用服务端组件有很多好处:
- 数据获取:通常服务端环境(网络、性能等)更好,离数据源更近,在服务端获取数据会更快。通过减少数据加载时间以及客户端发出的请求数量来提高性能。
- 安全:在服务端保留敏感数据和逻辑,不用担心暴露给客户端。
- 缓存:服务端渲染的结果可以在后续的请求中复用,提高性能。
- Bundle 大小:服务端组件的代码不会打包到 bundle 中,减少了 bundle 包的大小。
- 初始页面加载和 FCP:服务端渲染生成 HTML,快速展示 UI。
- Streaming:服务端组件可以将渲染工作拆分为 chunks,并在准备就绪时将它们流式传输到客户端。用户可以更早看到页面的部分内容,而不必等待整个页面渲染完毕。
3.4 回答参考
RSC 是一个很难理解的概念,至少当时卡了我很久,后来我还是自己实现了一个简单的 RSC,才理解了一些。(我可不是背答案,我真的研究过)
我觉得 RSC 它主要解决了 2 个问题:
第一个是 bundle size,将组件拆分为客户端组件和服务端组件后,服务端组件在服务端渲染即可,客户端只需要最后的渲染结果,所以服务端组件的依赖项不需要打包到客户端 bundle 中,这就减少了客户端 JS 的大小。
第二个是局部渲染和水合,传统的 SSR 实现中,所有的组件代码都要下载到客户端以进行水合,但是在 RSC 中,因为明确进行了组件区分,所以可以做到只有客户端组件进行水合。在后续导航的时候,RSC 将组件的渲染放在服务端,并渲染成特殊格式的 RSC Payload,根据 RSC Payload,客户端可以进行局部渲染和更新,由此实现了状态的保持。
不过这里我说的是纯 RSC,跟 Next.js 的实现还不一样,Next.js 更像是 RSC、SSR、Streaming 的结合。比如所有组件,无论客户端组件还是服务端组件都会在服务端进行渲染,Streaming 流式传输 HTML。后续导航的时候,RSC Payload 也针对流进行了优化,同样实现了 Streaming。RSC 和 SSR 相辅相成,共同提升了应用的性能。
4. 服务端组件和客户端组件
4.1 常见问题
服务端组件和客户端组件的区别有哪些?什么时候用服务端组件,什么时候用客户端组件?
4.2 解题思路
Next.js 的基础问题,正常回答即可。
4.3 八股答案
在 Next.js 中,组件默认就是服务端组件。如果在文件顶部添加一个 "use client" 声明,则是声明为客户端组件。
使用服务端组件的好处:
- 数据获取:通常服务端环境(网络、性能等)更好,离数据源更近,在服务端获取数据会更快。通过减少数据加载时间以及客户端发出的请求数量来提高性能。
- 安全:在服务端保留敏感数据和逻辑,不用担心暴露给客户端。
- 缓存:服务端渲染的结果可以在后续的请求中复用,提高性能。
- Bundle 大小:服务端组件的代码不会打包到 bundle 中,减少了 bundle 包的大小。
- 初始页面加载和 FCP:服务端渲染生成 HTML,快速展示 UI。
- Streaming:服务端组件可以将渲染工作拆分为 chunks,并在准备就绪时将它们流式传输到客户端。用户可以更早看到页面的部分内容,而不必等待整个页面渲染完毕。
使用客户端组件的好处:
- 交互性:客户端组件可以使用 state、effects 和事件监听器,意味着用户可以与之交互。
- 浏览器 API:客户端组件可以使用浏览器 API 如地理位置、localStorage 等。
组件如何选择:
需求 | 服务端组件 | 客户端组件 |
---|---|---|
获取数据 | ✅ | ❌ |
访问后端资源(直接) | ✅ | ❌ |
保留敏感信息(访问令牌、API 密钥等) | ✅ | ❌ |
使用依赖包,减少客户端 JavaScript 大小 | ✅ | ❌ |
添加交互和事件侦听器(onClick(), onChange() 等) | ❌ | ✅ |
使用状态和生命周期(useState(), useReducer(), useEffect()等) | ❌ | ✅ |
使用仅限浏览器的 API | ❌ | ✅ |
使用依赖于状态、效果或仅限浏览器的 API 的自定义 hook | ❌ | ✅ |
使用 React 类组件 | ❌ | ✅ |
4.4 回答参考
简单的来说,如果需要添加事件、使用状态和生命周期以及使用浏览器的 API,都需要声明为客户端组件,除此之外,都应该尽可能使用服务端组件。
为了尽可能多的使用服务端组件,常用的技巧有两个,一个是客户端组件下移,尽可能减少客户端组件的范围,降低客户端组件在组件树的位置。另一个是将服务端组件作为 props 传给客户端组件。
服务端组件和客户端组件的最大区别,表明上看是各种用法上的差别,但我理解根本上还是渲染原理上的差别。像服务端组件在服务端进行渲染,渲染为 HTML 传给客户端,流程就结束了。但是客户端组件它会先在服务端进行一次预渲染,传给客户端后还要进行一次水合,添加事件处理程序,最后根据客户端事件进行更新。所以客户端组件我觉得可以简单粗暴的理解为“SSR + 水合 + CSR”。
5. 性能优化
5.1 常见问题
如何优化 Next.js 应用程序的性能?Next.js 有哪些常用的性能优化手段?
5.2 解题思路
考察对 Next.js 常用性能优化方法的理解,性能优化更多是开发者完成工作之外做的事情,考察这些事情是为了发现面试者的做项目的能力和态度,所以在回答问题本身之外,可以引申到自己在实际项目中做的一些性能优化相关的工作经历。
5.3 八股答案
Next.js 有哪些常用的性能优化手段?
渲染策略:
Next.js 存在三种不同的服务端渲染策略:静态渲染、动态渲染和 Streaming,选择合适的渲染策略可以大大提高加载速度。缓存策略:
Next.js 中有四种缓存策略:基于 React 的函数记忆化、跨请求和部署的数据缓存、完整路由缓存和客户端路由缓存。默认情况下,Next.js 会尽可能多的使用缓存以提高性能和降低成本。减少 JavaScript 大小:
尽可能多的使用服务端组件,将客户端组件树的位置尽可能下移,减少客户端组件 bundle 的大小。Next.js 也提供了next/dynamic
实现组件懒加载,进一步拆分代码。优先使用内置组件:
Next.js 内置的 Image 组件内置了懒加载、图片优化等功能,内置的字体组件可以在构建的时候将谷歌字体下载到本地,避免重复请求。内置的 Link 组件默认带预加载功能等。由于自带多种优化,在使用 Next.js 的时候,优先使用内置组件。使用性能测量工具:
Next.js 提供了@next/bundle-analyzer
插件用于分析管理 JavaScript 模块大小。可以根据分析图,删除不必要的依赖项或者拆分代码。同时 Next.js 内置了性能测量和上报。可以使用useReportWebVitals
hook 进行上报或者依托于 Vercel 自动进行收集。
5.4 回答参考
Next.js 本身就内置了各种性能优化方法。比如它提供了三种服务端渲染策略,静态渲染、动态渲染和 Streaming,有四种缓存策略,基于 React 的函数记忆化、跨请求和部署的数据缓存、完整路由缓存和客户端路由缓存。默认情况下,Next.js 会自动判断渲染策略,并尽可能多的使用缓存以提高性能和降低成本。
在开发的时候,尽可能使用服务端组件,对于非首屏的代码可以使用 next/dynamic
实现懒加载,进一步降低客户端 bundle 的大小。Next.js 内置的各种组件都自带了各种优化,比如 Image 组件内置懒加载和图片优化,Link 组件默认预加载等。尽可能使用内置组件。
此外,Next.js 还内置了性能测量和优化工具,比如 useReportWebVitals
hook 可以上报性能数据,也有 bundle-analyzer
插件用于分析 bundle 依赖等。
其实性能优化的方法本身是有很多的,哪怕不从 Next.js 本身的层面,从网络层面、服务端层面也有一些优化方法。但我个人觉得,做性能优化最重要的是不要空想,不能自嗨,一定要数据先行。(引申到自己的性能优化体验和经历)
性能优化只是改善前端项目的方式之一,最终你要改善的是用户体验和开发体验,为用户、公司和开发者带来价值才行。所以一要准确的发现问题,二是要选择尽可能 ROI 高的优化方式,三是要用数据和案例证明它的价值。
所以做性能优化,要先把性能数据的测量和上报做好。像浏览器有 Lighthouse 可以直接测量性能。Vercel 本身提供了 speed-insights 可以快捷接入,看到页面的核心 Web 指标数据,但是免费版有限制,我后面试着接入了 Prometheus 和 Grafana,通过上报 Web 指标建立可视化后台,不过遇到了一些问题卡住了。(留个悬念,面试官对这种“失败”的事情可好奇了,做好对应的故事准备即可)
而在具体的数据指标中,其实主要看的就是 6 个指标,就是 Chrome 总结的那 6 个,核心是三个,衡量加载速度的 LCP,衡量互动的 INP,衡量视觉稳定性的 CLS,除此之外还有第一字节时间 (Time to First Byte,TTFB)、首次内容绘制 (First Contentful Paint,FCP)、首次输入延迟 (First Input Delay ,FID),性能上的优化就是将这些值都优化到合理的范围内,监控这些数据也是对应用的用户体验进行客观衡量。
注:最后应该再上一个故事,比如我接手过一个前台 APP 应用(把锅甩给同事),用户反馈页面卡顿(临危受命),但是相关的各种指标监控都没有(从零到一),我先用工具测试了一下页面性能,然后进行了一些常见的优化工作,加载时间从之前的 12s 提升到了 4s(先解决问题),然后我开始着手用建立了性能监控后台(系统解决问题),发现页面的 XXX 指标时间比较长,经过排查是 XXX 惹的祸,最终进行了优化,将加载时间提升到了 2s,然后就这些事情总结了技术方案,做了团队分享,拿到了领导同
事的表彰(升华一下)……balabala
6. 使用中的问题
6.1 常见问题
Next.js 服务端和客户端渲染不一致导致的水合报错该怎么解决?
6.2 解题思路
这是使用 Next.js 的常见问题,通过考察这类常见问题的解决方法,判断面试者是否对日常开发中遇到的问题进行过系统总结和思考。
6.3 八股答案
使用 Next.js 的时候经常会遇到这类报错:
Error: Hydration failed because the initial UI does not match what was rendered on the server.
其实这个报错并不来自于 Next.js,而是来自于 react-dom。比如我们在《源码篇 | 手写 React SSR》中就使用 React 的客户端 API hyrateRoot
进行水合。
之所以出现这个报错,是因为服务端预渲染的 React 树和浏览器首次渲染的 React 树不一致,换句话说,服务端渲染的内容应该和水合时传入的内容一致,否则水合的时候无法正确复用 DOM,导致水合报错。
导致水合错误的原因有很多:
- 渲染的时候,使用了诸如
typeof window !== 'undefined'
这样的判断:
export default function Page() {
if (typeof window !== 'undefined') return <div>Hello Browser</div>
return (
<div>
Hello Node!
</div>
);
}
比如这段代码,在服务端渲染的时候,因为在 Node 环境,渲染结果为 <div>Hello Node!</div>
,在浏览器环境,渲染结果为 <div>Hello Browser</div>
,前后内容不一致,就会导致水合错误。
- 渲染的时候,使用了仅限浏览器的 API,比如
window
或localStorage
。 - 渲染的时候,使用了时间相关的 API,比如
Date()
。 - 浏览器扩展修改了 HTML。
常用的解决方法有:
- 使用
useEffect
仅在客户端运行:
import { useState, useEffect } from 'react'
export default function App() {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
return <h1>{isClient ? 'This is never prerendered' : 'Prerendered'}</h1>
}
- 禁用特定组件的 SSR 渲染:
import dynamic from 'next/dynamic'
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })
export default function Page() {
return (
<div>
<NoSSR />
</div>
)
}
- 使用
suppressHydrationWarning
消除警告:
如果实在无法避免,可以添加 suppressHydrationWarning
属性消除警告:
<time datetime="2016-10-25" suppressHydrationWarning />
6.4 回答参考
这个问题很常见,本质上是服务端预渲染的 React 树和水合时的 React 树不一致导致,导致这个错误的原因有很多,我专门研究过还写过文章分享,大致有几种原因:
第一种是使用了 window
、document
等客户端 API 判断,使用了浏览器 API,如 localStorage
等。
第二种是使用了时间这类 API。
我遇到的就这两种,但可能原因还有很多,据我所知,还有标签嵌套错误,比如 button
嵌套 button
,浏览器插件导致渲染不一致,错误配置 CSS-In-JS 导致等等。
解决方案有几种:
第一种是使用 useEffect
,将在客户端运行的内容放在 useEffect
中执行。
第二种是禁用组件的 SSR 渲染,据我所知,目前 Next.js 只提供了 dynamic
方法可以禁用组件的预渲染。
第三种是添加一个属性,具体我忘了,这是在无法避免水合错误的时候,添加这个属性,消除水合警告。
(其实我记得,但这么小众的属性,我如果记得那么牢,面试官可能怀疑我背过答案了……)