前文回顾:# 🔥🔥🔥念头通达:手把手带你学next+tailwind(一)
缓存配置
next缓存分为请求记忆、数据缓存、完整路由缓存、路由缓存。其中前三种都是针对服务端组件的,只有路由缓存是针对客户端组件的
机制 | 缓存内容 | 缓存位置 | 缓存目的 | 缓存持续时间 |
---|---|---|---|---|
请求记忆 | 函数返回值 | 服务端 | 在react组件树中复用数据 | 每个请求生命周期 |
数据缓存 | 数据 | 服务端 | 在用户请求和部署期间存储数据 | 永久 (可以被重新验证) |
完整路由缓存 | HTML和 RSC payload | 服务端 | 减少渲染开销,提高性能 | 永久 (可以被重新验证) |
路由缓存 | RSC Payload | 客户端 | 在导航期间减少服务端请求 | 用户 session 或基于时间 |
next.js会尽量缓存改善性能,默认使用静态渲染和数据缓存,下面是官网给的首次访问静态路由的流程
- 打包构建/a路由,由于是第一次访问,路由缓存、完整路由缓存、请求记忆、数据缓存都会miss;从数据源获取数据后,依次存入数据缓存、请求记忆,生成的RSC payload和HTML存入完整路由缓存
- 客户端访问/a路由,命中缓存的RSC payload和HTML,并且把RSC payload存在路由缓存中
目前最新的next15,数据默认不缓存
请求记忆
概念
react扩展了fetch api,使用fetch默认会进行缓存(针对URL和请求参数相同的请求)。所以我们没有必要把请求得到的数据通过props等方式传参,应该直接在每个组件发起请求
csharp
async function getItem() {
// fetch自动请求记忆缓存结果
const res = await fetch('https://.../item/1')
return res.json()
}
// getItem调用了2次, 只有第一次有请求花销
const item = await getItem() // 第一次请求,缓存未命中
// 第二次请求在路由任何地方触发缓存都会生效
const item = await getItem() // 第二次请求,缓存击中
请求记忆只适用于fetch的get请求,而且只适用于react组件树
它可以适用于generateMetadata
, generateStaticParams
,layout(布局)
,page(页面)
和其他服务端组件
不用fetch请求如何实现请求记忆,直接用react内置的cache函数
最佳实践
- 不重新验证
- 默认不要退出请求记忆,如果实在要退出可以使用AbortController
scss
const { signal } = new AbortController()
fetch(url, { signal })
数据缓存
工作流程
- 首次访问/a路由,请求记忆和数据缓存都会miss,从数据源中获取数据后会存入数据缓存和请求记忆
- 再次访问/a路由,优先从请求记忆和数据缓存中取数据,缓存没命中才会重新请求
如果配置了cache:'no-store',不管请求多少次都是数据缓存都会miss,直接从数据源中获取,但是请求记忆还是会生效
数据缓存和请求记忆的区别
两者都是用于缓存,提升性能的
-
持续时间不同;数据缓存在请求和部署期间都是永久性的 ,请求记忆只在每个请求的生命周期有效
-
作用不同,数据缓存用于减少原始数据源的请求数量,请求记忆用于减少重复请求数量
重新验证缓存方法
- 基于时间的重新验证
使用next.revalidate
,适用于不经常更改而且新鲜度不重要的数据
php
fetch('https://...', { next: { revalidate: 60 } }) // 60秒后重新验证
配置了next.revalidate后如下图 在60秒以内都会使用数据缓存,在60秒以后再有新的请求会触发重新验证,第一次请求还用之前的缓存值,将新的数据加入到缓存。60秒后的第2次请求使用新的缓存
- 按需重新验证
分为revalidatePath
和revalidateTag
两种方式
php
// actions.tsx
revalidatePath('/')
// app/form/page.tsx
fetch('...',{next:{tags:['a']}})
// actions.tsx
revalidateTag('a')
配置了revalidateTag后,第一次调用请求正常缓存,触发revalidateTag后会直接从数据缓存中清除对应tag的数据,再次请求时还是会把tag的数据缓存起来,revalidatePath同理
退出缓存方法
- 直接配置cache:'no-store',这种方式是针对配置的请求有效
php
fetch('...',{cache:'no-store'})
- 路由段配置,这种方式会影响这个路由段的所有请求
dart
export const dynamic = 'force-dynamic'
完整路由缓存
概念
next.js自动渲染路由和缓存路由,这是内置的优化,可以让页面加载速度变快
在服务端,next.js使用react编排渲染,渲染会通过路由段和Suspense被分成chunks块,每一个chunks的渲染都会经过两个步骤:
- react把服务端组件渲染为特殊的数据格式,优化为流式渲染(Streaming),即RSC payload
- Next.js使用RSC payload和客户端组件渲染指令,在服务端上渲染HTML
我们不需要等所有工作完成才渲染,这就是next.js的流式渲染
RSC payload是什么
RSC payload全称为服务端组件payload(React Server Component Payload),它是react服务端组件树的渲染结果,用于react客户端更新浏览器的DOM结构,包括:
- 服务端组件的渲染结果
- 客户端组件占位符和引用结果
- 服务端组件传给客户端组件的属性 RSC payload不会打包到客户端代码中,它属于服务端组件代码,因此可以减少包体积大小
静态路由默认开启完整路由缓存,由于动态路由在请求的时候才渲染,因此不会被完整路由缓存
下面这个图片清晰的展示了静态路由和动态路由的区别
作用
- 性能提升,通过缓存整个路由页面,用户重复访问同一页面时,可以直接从缓存中加载而不是服务器生成,大大减少了页面加载时间,提升了用户体验
- 降低成本,减少了服务器的计算资源和带宽需求,在高流量的情况下效果显著,可以降低运营成本
- SEO优化,对于SSG页面而言有利于SEO优化,可以帮助爬虫直接抓取完整的HTML内容
优缺点
优点
- 有利于加快页面加载速度,尤其是内容不怎么需要更新的页面
- 减轻服务端压力;服务器不需要频繁地处理相同的页面请求,特别是在高并发场景下,可以有效分配资源
- 更好的用户体验;快速响应和即时交互提升了用户体验
缺点
- 内容更新问题,如果内容需要经常更新,缓存可能导致用户看到的是过时的信息,除非缓存被更新或清除
- 缓存管理增加了一定的维护成本和复杂性,需要有效的缓存管理策略来保证内容的新鲜度,同时避免缓存未命中率过高
- 存储限制;大量的缓存数据可能会占用客户端或边缘服务器的存储空间,尤其在资源丰富的应用中
- 初次部署成本,对于ISR和SSG策略,首次构建部署可能需要比较长的时间,因为所有页面都要预先生成
失效方式
完整路由缓存有两种方式失效:
- 重新验证
- 重新部署
退出方式
- 将静态渲染改为动态渲染
路由缓存
概念
路由缓存也叫客户端缓存或者预取缓存,Next.js的客户端缓存是基于内存缓存,存储RSC Component,缓存持续时间在整个用户session中
路由缓存的工作流程如下图
- 首次访问路由/a,路由缓存miss,它会根据路由段顺序从根路由开始,从上到下依次到目标路由存入路由缓存,顺序依次为/layout,/a(page)
- 导航到/b会触发部分命中,因为这个路由和/a路由共享根路由/layout命中,/b miss,将/b加入路由缓存
- 再次导航/a,路由缓存生效
路由缓存可以改善用户导航体验,前进/后退导航速度会加快,因为有路由缓存,没有访问过的路由导航速度也会加快,因为有prefetch和部分渲染
路由导航不会触发full page reload(完整路由重新加载)
完整路由缓存和路由缓存有什么区别
- 存储位置不同,完整路由缓存是在服务端上的,路由缓存是在客户端的
- 存储期限不同,完整路由缓存永久性存储RSC payload和HTML,在多个用户请求期间有效;路由缓存临时存储RSC Payload,它只在用户会话有效
- 完整路由缓存只存储静态路由,路由缓存静态路由和动态路由都存储
- 重新验证或退出路由缓存会让完整路由缓存失效,因为渲染的输出依赖数据;完整路由缓存的重新验证或退出不会影响路由缓存
持续时间
路由缓存的持续时间取决于两个因素:会话和自动失效期间
- 会话,缓存在导航期间都有效,直到页面刷新
- 自动失效期间,路由段在特定时间内自动失效,取决于资源怎么定义prefetch
- Default Prefetching (
prefetch={null}
或者 未指定): 30秒 - Full Prefetching : (
prefetch={true}
或者router.prefetch
): 5分钟
从v14.2.0-canary.53版本开启了实验性支持配置自动失效时长
重新验证方式
- 在server action中,使用revalidatePath或revalidateTag;使用cookies.set或者cookies.delete
- 调用router.refresh会让路由缓存失效,向服务端发起新的请求获取当前路由
退出方式
没有办法退出路由缓存,但是你可以退出prefetch,通过在<Link>标签加prefetch属性为false实现
ini
<Link href='/a' prefetch={false}></Link>
server action
概念
server action是指在服务端执行的异步函数,它们可以在客户端组件和服务端组件中使用,用于处理表单提交和数据突变
可以通过在函数顶部定义use server
实现
javascript
//app/page.tsx
// Server Component
export default function Page() {
async function create() {
'use server'
// ...
}
return (
// ...
)}
也可以在actions文件夹中定义
javascript
//app/action.ts
'use server'
export async function create() {
// ...
}
// app/page.tsx
import create from "./action.ts"
create()
server action既可以在客户端组件使用又可以在服务端组件使用,但是客户端组件只支持第二种方式使用
使用示例
- 新建next项目,使用typescript,项目名next-demo
lua
npx create-next-app@latest --typescript next-demo
cd next-demo
pnpm install
项目目录如下
2.首先进入global.css,把默认的难看背景色去掉
3. 在app目录下新建shoplist文件夹page.tsx
javascript
// app/shoplist/page.tsx
import { getShopList, addShopItem } from "./actions";
export default async function Page() {
const list = await getShopList();
return (
<div className="p-2">
<form action={addShopItem}>
<input
className="border border-gray-300 border-solid rounded-sm"
name="item"
/>
<button
className="mx-1 px-2 py-1 text-white bg-blue-300 hover:bg-blue-500 rounded-md"
type="submit"
>
添加购物项
</button>
</form>
<ol>
{list.map((item, i) => (
<li key={i}>
{i + 1}.{item}
</li>
))}
</ol>
</div>
);
}
- 在shoplist文件夹下增加actions.tsx
javascript
// shoplist/actions.tsx
"use server";
import { revalidatePath } from "next/cache";
const data = ["毛巾", "牙刷", "餐具"];
export async function getShopList() {
return data;
}
export async function addShopItem(formData: FormData) {
const item = formData.get("item");
if (item) {
data.push(item + "");
}
revalidatePath("/shoplist");
return data;
}
-
运行项目访问/shoplist
pnpm dev
点击添加购物项,调用了serverAction的addShopItem方法,可以看到发起了一个POST请求,查看请求体可以看到提交项包括
ACTION_ID
和数据项
ACTION_ID
是什么东西?
查看页面结构,可以看到表单元素中自动添加了一个带ACTION_ID的input标签,可以理解为它是这个server action对应的唯一标志
数据提交后返回的是RSC payload
在实际项目中,每个表单手动控制请求的error,status,pending效率太低了,next.js提供了乐观更新和useFormStatus/useActionState来控制表单
性能优化
内置组件优化
<Image>
Next.js的内置图片组件<Image>
对<img>标签做了优化,实现了懒加载,还可以根据设备尺寸调整图片大小;它还实现了视觉稳定性,为了防止图片在加载时出现布局偏移
图片尺寸对于LCP来说是个很重要的优化点
推荐将图片资源放在根目录的public文件夹下,新建images目录存放所有静态图片资源,支持的图片格式:
.png
.jpg
.jpeg
.webp
Next.js会根据你导入的图片自动决定图片的width
和height
属性,可以有效减少累计布局偏移(CLS)
javascript
import Image from "next/image"
export default function Page(){
return <>
// 访问路径为 public/images/logo.png
// 必填属性 src/width/height/alt
<Image src={`/images/logo.png`} width={270} height={90} alt="logo"></Image>
</>
}
远程图片需要在next.config.js中配置才能访问
css
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 's3.amazonaws.com',
port: '',
pathname: '/my-bucket/**',
},
],
}
}
上面的配置指的是允许访问s3.amzaonaws.com/my-bucket/** 下的的所有图片
不确定远程图片宽高,可以使用layout配合objectFit属性
javascript
import Image from 'next/image'
export default function Page(){
return <div className="relative w-full h-full">
<Image
src={`https://xxx.xxx`}
alt=""
fill
objectFit="contain"></Image>
</div>
}
<Link>
用于后台预获取资源
客户端组件导航的重要方式之一,除此之外客户端组件还能使用useRouter
的router.push /router.back /router.forward /router.replace 等方式导航
javascript
//app/page.tsx
import Link from "next/link"
export default function Page(){
return <>
{/* 等价于跳转/order */}
<Link href={'/order'}></Link>
{/* 等价于跳转/order?id=1 */}
<Link href={{
pathname:'/order',
search:{
id:1
}
}}></Link>
</>
}
href 属性必填,支持传入字符串或者对象,文档
<Script>
用于加载和控制第三方脚本
加载策略strategy可选值:
beforeInteractive
在水合(页面可交互)之前加载,一般用于加载通用脚本afterInteractive
页面可交互后加载,一般用于加载统计脚本lazyOnload
在浏览器空闲时加载,一般用于加载优先级不高的脚本worker
在web worker中加载(实验特性)
<Script>可以在layout(布局)或者Page页面使用,不管被多少页面引用,都只会加载一次。也就是页面导航<Script>不会重复加载
<Script>
还支持三个事件:
onLoad
脚本加载完成后执行onReady
脚本加载完,组件挂载后执行逻辑onError
脚本加载失败后执行的逻辑 这三个方法都只能在客户端组件使用
javascript
// app/layout.tsx
import Script from 'next/script'
export default function Page() {
return (
<>
<Script src="https://example.com/script.js"/>
</>
)}
React的很多性能优化方法都适用于Next.js
性能监测
可以通过自定义组件实现
javascript
//app/components/web-vitals.ts
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric)
})}
在根布局中引入
javascript
//app/layout.tsx
import { WebVitals } from "./components/web-vitals.ts"
export default function Layout(){
return (
<html>
<body>
{children}
<WebVitals/>
</body>
</html>
)
}
懒加载
Next.js支持懒加载:dynamic函数和React.lazy
javascript
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
// 引入客户端组件
const ComponentA = dynamic(() => import('../components/A'))
const ComponentB = dynamic(() => import('../components/B'))
const ComponentC = dynamic(() => import('../components/C'), { ssr: false }) // 设置ssr为false跳过SSR渲染
export default function ClientComponentExample() {
const [showMore, setShowMore] = useState(false)
return (
<div>
<ComponentA /> {/* 立即加载,单独的客户端组件*/}
{showMore && <ComponentB />} {/* showMore为真的时候按需加载 */}
<button onClick={() => setShowMore(!showMore)}>Toggle</button>
<ComponentC /> {/* 只有客户端加载 */}
</div> )}
在服务端组件中使用dynamic加载客户端组件,只有客户端组件的部分会被懒加载
打包分析
Next.js也内置了类似于Bundle-anlayzer的功能
- 安装
sql
pnpm add @next/bundle-analyzer // 如果使用别的包管理工具用对应语法安装@next/bundle-analyzer
- 配置next.config.js文件
ini
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true',})
const nextConfig = {}
module.exports = withBundleAnalyzer(nextConfig)
- 在package.json增加scripts脚本
json
// package.json
{
"scripts":{
"analyze":"ANALYZE=true pnpm build"
}
}
-
运行脚本
pnpm analyze
如图,浏览器会打开多个页面展示包的体积
SEO
Next.js内置的Metadata是专用于SEO的,静态使用metadata
,动态使用generateMetadata
函数。它可以帮助搜索引擎更好的抓取网页内容因此对于SEO优化很有帮助
使用示例
javascript
// app/demo/page.tsx
import type {MetaData} from "next"
export const metadata:MetaData = {
title:'lyllovelemon', // 设置文档标题
description:'next.js测试应用' // 设置文档描述
}
export default function Page(){
...
}
metadata字段参考文档
注意:metadata和generateMetadata函数只能在服务端组件使用;
在同一个路由段不要同时使用metadata和generateMetadata函数
推荐在不需要取数据的场景使用静态metadata,元数据需要数据请求获取的场景使用generateMetadata函数
typescript
// app/demo2/[id]/page.tsx
import {Metadata,ResolvingMetadata} from "next"
type Props = {
params:{id:string}
searchParams:any
}
export async function generateMetadata({
params,
searchParams
}:Props,
parent:ResolvingMetadata):Promise<Metadata>{
const id = params.id
const demo = await fetch(`https://.../${id}`).then((res) => res.json()) // 假设数据为{title:"柠檬酱",id:1}
return {
title:demo.title,
description:`demo详情-${id}`
}
}
export default function Page(){
...
}
Next.js会等待generateMetadata数据请求完成再开始客户端渲染UI,保证<head>中有<meta>标签
generateMetadata
接收params和searchParams作为参数,可以获取路由段的params和searchParams
路由段 | 访问路由 | searchParams | params |
---|---|---|---|
demo2/[id]/page.tsx | demo2/1/page.tsx | null | {id:1} |
demo2/page.tsx | demo2?id=lyllovelemon | {id:'lyllovelemon'} | null |
demo2/[...id]/page.tsx | demo2/1/2/page.tsx | null | {id:[1,2]} |
上面示例代码等价于在head标签中注入meta标签
ini
<head>
<meta name="title" content="柠檬酱"/>
<meta name="description" content="demo详情-1"/>
...
</head>
最佳实践
首先需要了解SEO的<html>标签权重
关键词优化
metadata的keywords字段非常重要,让我们扒一下掘金的keywords字段
ini
<meta data-n-head="ssr" vmid="keywords" name="keywords" content="掘金,稀土,Vue.js,前端面试题,Kotlin,ReactNative,Python">
对应Next.js的配置为
arduino
//app/page.tsx
export const metadata = {
keywords: ['掘金,', '稀土', 'Vue.js','前端面试题','Kotlin','ReactNative','Python']
}
是不是超级简单
内容优化
对应metadata的content字段,还是拿掘金举例
ini
<meta data-n-head="ssr" vmid="description" name="description" content="掘金是面向全球中文开发者的技术内容分享与交流平台。我们通过技术文章、沸点、课程、直播等产品和服务,打造一个激发开发者创作灵感,激励开发者沉淀分享,陪伴开发者成长的综合类技术社区。">
对应的Next.js配置为
arduino
//app/page.tsx
export const metadata = {
description: "掘金是面向全球中文开发者的技术内容分享与交流平台。我们通过技术文章、沸点、课程、直播等产品和服务,打造一个激发开发者创作灵感,激励开发者沉淀分享,陪伴开发者成长的综合类技术社区。"
}
当我们搜索掘金关键词时,搜索引擎会把description字段的内容展示出来
HTML语义化
HTML5标签都是语义化标签,推荐使用这些
<h1>
~<h5>
一个页面最好只用一个<h1>
标签,和title字段一一对应<nav>
导航标签<header>
<footer>
<section>
章节、页眉页脚<aside>
侧边栏和引述内容<article>
独立的文档、页面、应用
其次,页面性能也会影响网站排名,推荐使用lighthouse对页面进行优化
指定<rel="canonical">
如果你的网站有多个链接,推荐使用这个meta标签,用来给多个相似网站指定权威网址
ini
<link rel="canonical" href="目标网站绝对路径">
内外链优化
优先使用HTTPS
HTTPS网站比HTTP网站排名高
拿我线上做的项目搜索效果如下