JackyLove 的技术人生

人生艰难,唯有一技傍身才能慢慢走向通途。个人博客,记录生活,分享技术,记录成长。专注全栈技术,next技术。

第79章—面试篇-常见面试题及解析(2)

首次发表于 2024-07-29, 更新于 2024-07-29

1. 服务端组件和客户端组件

1.1. 常见问题

服务端组件和客户端组件的区别有哪些?什么时候用服务端组件,什么时候用客户端组件?

1.2. 解题思路

Next.js 的基础问题,正常回答即可

1.3. 八股答案

在 Next.js 中,组件默认就是服务端组件。如果在文件顶部添加一个 "use client" 声明,则是声明为客户端组件。

使用服务端组件的好处:

  1. 数据获取:通常服务端环境(网络、性能等)更好,离数据源更近,在服务端获取数据会更快。通过减少数据加载时间以及客户端发出的请求数量来提高性能
  2. 安全:在服务端保留敏感数据和逻辑,不用担心暴露给客户端
  3. 缓存:服务端渲染的结果可以在后续的请求中复用,提高性能
  4. bundle 大小:服务端组件的代码不会打包到 bundle 中,减少了 bundle 包的大小
  5. 初始页面加载和 FCP:服务端渲染生成 HTML,快速展示 UI
  6. Streaming:服务端组件可以将渲染工作拆分为 chunks,并在准备就绪时将它们流式传输到客户端。用户可以更早看到页面的部分内容,而不必等待整个页面渲染完毕

使用客户端组件的好处:

  1. 交互性:客户端组件可以使用 state、effects 和事件监听器,意味着用户可以与之交互
  2. 浏览器 API:客户端组件可以使用浏览器 API 如地理位置、localStorage 等

组件如何选择:

如果你需要…… 服务端组件 客户端组件
获取数据
访问后端资源(直接)
在服务端上保留敏感信息(访问令牌、API 密钥等)
在服务端使用依赖包,从而减少客户端 JavaScript 大小
添加交互和事件侦听器(onClick(), onChange() 等)
使用状态和生命周期(useState(), useReducer(), useEffect()等)
使用仅限浏览器的 API
使用依赖于状态、效果或仅限浏览器的 API 的自定义 hook
使用 React 类组件

1.4. 回答参考

简单的来说,如果需要添加事件、使用状态和生命周期以及使用浏览器的 API,都需要声明为客户端组件,除此之外,都应该尽可能使用服务端组件。

为了尽可能多的使用服务端组件,常用的技巧有两个,一个是客户端组件下移,尽可能减少客户端组件的范围,降低客户端组件在组件树的位置。另一个是将服务端组件作为 props 传给客户端组件。

服务端组件和客户端组件的最大区别,表明上看是各种用法上的差别,但我理解根本上还是渲染原理上的差别。像服务端组件在服务端进行渲染,渲染为 HTML 传给客户端,流程就结束了。但是客户端组件它会先在服务端进行一次预渲染,传给客户端后还要进行一次水合,添加事件处理程序,最后根据客户端事件进行更新。所以客户端组件我觉得可以简单粗暴的理解为“SSR + 水合 + CSR”。

2. 性能优化

2.1. 常见问题

如何优化 Next.js 应用程序的性能?Next.js 有哪些常用的性能优化手段?

2.2. 解题思路

考察对 Next.js 常用性能优化方法的理解,性能优化更多是开发者完成工作之外做的事情,考察这些事情是为了发现面试者的做项目的能力和态度,所以在回答问题本身之外,可以引申到自己在实际项目中做的一些性能优化相关的工作经历。

2.3. 八股答案

Next.js 有哪些常用的性能优化手段?

  1. 渲染策略

Next.js 存在三种不同的服务端渲染策略,1. 静态渲染 2. 动态渲染 3. Streaming,选择合适的渲染策略可以大大提高加载速度

  1. 缓存策略

Next.js 中有四种缓存策略,基于 React 的函数记忆化、跨请求和部署的数据缓存、完整路由缓存和客户端路由缓存。默认情况下,Next.js 会尽可能多的使用缓存以提高性能和降低成本。

  1. 减少 JavaScript 大小

尽可能多的使用服务端组件,将客户端组件树的位置尽可能下移,减少客户端组件 bundle 的大小。Next.js 也提供了 next/dynamic 实现组件懒加载,进一步拆分代码。

  1. 优先使用内置组件

Next.js 内置的 Image 组件内置了懒加载、图片优化等功能,内置的字体组件可以在构建的时候将谷歌字体下载到本地,避免重复请求。内置的 Link 组件默认带预加载功能等。由于自带多种优化,在使用 Next.js 的时候,优先使用内置组件。

  1. 使用性能测量工具

Next.js 提供了 @next/bundle-analyzer 插件用于分析管理 JavaScript 模块大小。可以根据分析图,删除不必要的依赖项或者拆分代码。同时 Next.js 内置了性能测量和上报。可以使用 useReportWebVitals hook 进行上报或者依托于 Verecl 自动进行收集。

2.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

3. 使用中的问题

3.1. 常见问题

Next.js 服务端和客户端渲染不一致导致的水合报错该怎么解决?

3.2. 解题思路

这是使用 Next.js 的常见问题,通过考察这类常见问题的解决方法,判断面试者是否对日常开发中遇到的问题进行过系统总结和思考。

3.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,导致水合报错。

导致水合错误的原因有很多:

  1. 渲染的时候,使用了诸如 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>,前后内容不一致,就会导致水合错误

  1. 渲染的时候,使用了仅限浏览器的 API,比如 window 或 localStorage
  2. 渲染的时候,使用了时间相关的 API,比如 Date()
  3. 浏览器扩展修改了 HTML

常用的解决方法有:

  1. 使用 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>
}
  1. 禁用特定组件的 SSR 渲染
import dynamic from 'next/dynamic'
 
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })
 
export default function Page() {
  return (
    <div>
      <NoSSR />
    </div>
  )
}
  1. 使用 suppressHydrationWarning 消除警告

如果实在无法避免,可以添加 suppressHydrationWarning 属性消除警告

<time datetime="2016-10-25" suppressHydrationWarning />

3.4. 回答参考

这个问题很常见,本质上是服务端预渲染的 React 树和水合时的 React 树不一致导致,导致这个错误的原因有很多,我专门研究过还写过文章分享,大致有几种原因:

第一种是使用了 window、document 等客户端 API 判断,使用了浏览器 API,如 localstorage 等

第二种是使用了时间这类 API

我遇到的就这两种,但可能原因还有很多,据我所知,还有标签嵌套错误,比如 button 嵌套 button,浏览器插件导致渲染不一致,错误配置 CSS-In-JS 导致等等

解决方案有几种:

第一种是使用 useEffect,将在客户端运行的内容放在 useEffect 中执行

第二种是禁用组件的 SSR 渲染,据我所知,目前 Next.js 只提供了 dynamic 方法可以禁用组件的预渲染

第三种是添加一个属性,具体我忘了,这是在无法避免水合错误的时候,添加这个属性,消除水合警告

(其实我记得,但这么小众的属性,我如果记得那么牢,面试官可能怀疑我背过答案了……)

总结

我的答案也不一定对,也不一定符合你对“面试答案”的期望,欢迎在评论区补充自己的回答!

© Copyright 2025 JackyLove 的技术人生. Powered with by CreativeDesignsGuru