🔥🔥🔥念头通达:手把手带你学next:缓存配置、性能优化和SEO(二)

前文回顾:# 🔥🔥🔥念头通达:手把手带你学next+tailwind(一)

缓存配置

next缓存分为请求记忆、数据缓存、完整路由缓存、路由缓存。其中前三种都是针对服务端组件的,只有路由缓存是针对客户端组件的

机制 缓存内容 缓存位置 缓存目的 缓存持续时间
请求记忆 函数返回值 服务端 在react组件树中复用数据 每个请求生命周期
数据缓存 数据 服务端 在用户请求和部署期间存储数据 永久 (可以被重新验证)
完整路由缓存 HTML和 RSC payload 服务端 减少渲染开销,提高性能 永久 (可以被重新验证)
路由缓存 RSC Payload 客户端 在导航期间减少服务端请求 用户 session 或基于时间

next.js会尽量缓存改善性能,默认使用静态渲染和数据缓存,下面是官网给的首次访问静态路由的流程

  1. 打包构建/a路由,由于是第一次访问,路由缓存、完整路由缓存、请求记忆、数据缓存都会miss;从数据源获取数据后,依次存入数据缓存、请求记忆,生成的RSC payload和HTML存入完整路由缓存
  2. 客户端访问/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, generateStaticParamslayout(布局)page(页面)和其他服务端组件

不用fetch请求如何实现请求记忆,直接用react内置的cache函数

最佳实践

  1. 不重新验证
  2. 默认不要退出请求记忆,如果实在要退出可以使用AbortController
scss 复制代码
const { signal } = new AbortController()
fetch(url, { signal })

数据缓存

工作流程

  1. 首次访问/a路由,请求记忆和数据缓存都会miss,从数据源中获取数据后会存入数据缓存和请求记忆
  2. 再次访问/a路由,优先从请求记忆和数据缓存中取数据,缓存没命中才会重新请求

如果配置了cache:'no-store',不管请求多少次都是数据缓存都会miss,直接从数据源中获取,但是请求记忆还是会生效

数据缓存和请求记忆的区别

两者都是用于缓存,提升性能的

  1. 持续时间不同;数据缓存在请求和部署期间都是永久性的 ,请求记忆只在每个请求的生命周期有效

  2. 作用不同,数据缓存用于减少原始数据源的请求数量,请求记忆用于减少重复请求数量

重新验证缓存方法

  1. 基于时间的重新验证

使用next.revalidate,适用于不经常更改而且新鲜度不重要的数据

php 复制代码
fetch('https://...', { next: { revalidate: 60 } }) // 60秒后重新验证

配置了next.revalidate后如下图 在60秒以内都会使用数据缓存,在60秒以后再有新的请求会触发重新验证,第一次请求还用之前的缓存值,将新的数据加入到缓存。60秒后的第2次请求使用新的缓存

  1. 按需重新验证

分为revalidatePathrevalidateTag两种方式

php 复制代码
// actions.tsx
revalidatePath('/')

// app/form/page.tsx
fetch('...',{next:{tags:['a']}})
// actions.tsx
revalidateTag('a')

配置了revalidateTag后,第一次调用请求正常缓存,触发revalidateTag后会直接从数据缓存中清除对应tag的数据,再次请求时还是会把tag的数据缓存起来,revalidatePath同理

退出缓存方法

  1. 直接配置cache:'no-store',这种方式是针对配置的请求有效
php 复制代码
fetch('...',{cache:'no-store'})
  1. 路由段配置,这种方式会影响这个路由段的所有请求
dart 复制代码
export const dynamic = 'force-dynamic'

完整路由缓存

概念

next.js自动渲染路由和缓存路由,这是内置的优化,可以让页面加载速度变快

在服务端,next.js使用react编排渲染,渲染会通过路由段和Suspense被分成chunks块,每一个chunks的渲染都会经过两个步骤:

  1. react把服务端组件渲染为特殊的数据格式,优化为流式渲染(Streaming),即RSC payload
  2. Next.js使用RSC payload和客户端组件渲染指令,在服务端上渲染HTML

我们不需要等所有工作完成才渲染,这就是next.js的流式渲染

RSC payload是什么

RSC payload全称为服务端组件payload(React Server Component Payload),它是react服务端组件树的渲染结果,用于react客户端更新浏览器的DOM结构,包括:

  • 服务端组件的渲染结果
  • 客户端组件占位符和引用结果
  • 服务端组件传给客户端组件的属性 RSC payload不会打包到客户端代码中,它属于服务端组件代码,因此可以减少包体积大小

静态路由默认开启完整路由缓存,由于动态路由在请求的时候才渲染,因此不会被完整路由缓存

下面这个图片清晰的展示了静态路由和动态路由的区别

作用

  1. 性能提升,通过缓存整个路由页面,用户重复访问同一页面时,可以直接从缓存中加载而不是服务器生成,大大减少了页面加载时间,提升了用户体验
  2. 降低成本,减少了服务器的计算资源和带宽需求,在高流量的情况下效果显著,可以降低运营成本
  3. SEO优化,对于SSG页面而言有利于SEO优化,可以帮助爬虫直接抓取完整的HTML内容

优缺点

优点

  1. 有利于加快页面加载速度,尤其是内容不怎么需要更新的页面
  2. 减轻服务端压力;服务器不需要频繁地处理相同的页面请求,特别是在高并发场景下,可以有效分配资源
  3. 更好的用户体验;快速响应和即时交互提升了用户体验

缺点

  1. 内容更新问题,如果内容需要经常更新,缓存可能导致用户看到的是过时的信息,除非缓存被更新或清除
  2. 缓存管理增加了一定的维护成本和复杂性,需要有效的缓存管理策略来保证内容的新鲜度,同时避免缓存未命中率过高
  3. 存储限制;大量的缓存数据可能会占用客户端或边缘服务器的存储空间,尤其在资源丰富的应用中
  4. 初次部署成本,对于ISR和SSG策略,首次构建部署可能需要比较长的时间,因为所有页面都要预先生成

失效方式

完整路由缓存有两种方式失效:

  1. 重新验证
  2. 重新部署

退出方式

  1. 将静态渲染改为动态渲染

路由缓存

概念

路由缓存也叫客户端缓存或者预取缓存,Next.js的客户端缓存是基于内存缓存,存储RSC Component,缓存持续时间在整个用户session中

路由缓存的工作流程如下图

  1. 首次访问路由/a,路由缓存miss,它会根据路由段顺序从根路由开始,从上到下依次到目标路由存入路由缓存,顺序依次为/layout,/a(page)
  2. 导航到/b会触发部分命中,因为这个路由和/a路由共享根路由/layout命中,/b miss,将/b加入路由缓存
  3. 再次导航/a,路由缓存生效

路由缓存可以改善用户导航体验,前进/后退导航速度会加快,因为有路由缓存,没有访问过的路由导航速度也会加快,因为有prefetch和部分渲染

路由导航不会触发full page reload(完整路由重新加载)

完整路由缓存和路由缓存有什么区别

  1. 存储位置不同,完整路由缓存是在服务端上的,路由缓存是在客户端的
  2. 存储期限不同,完整路由缓存永久性存储RSC payload和HTML,在多个用户请求期间有效;路由缓存临时存储RSC Payload,它只在用户会话有效
  3. 完整路由缓存只存储静态路由,路由缓存静态路由和动态路由都存储
  4. 重新验证或退出路由缓存会让完整路由缓存失效,因为渲染的输出依赖数据;完整路由缓存的重新验证或退出不会影响路由缓存

持续时间

路由缓存的持续时间取决于两个因素:会话和自动失效期间

  1. 会话,缓存在导航期间都有效,直到页面刷新
  2. 自动失效期间,路由段在特定时间内自动失效,取决于资源怎么定义prefetch
  • Default Prefetching (prefetch={null} 或者 未指定): 30秒
  • Full Prefetching : (prefetch={true} 或者 router.prefetch): 5分钟

从v14.2.0-canary.53版本开启了实验性支持配置自动失效时长

重新验证方式

  1. 在server action中,使用revalidatePath或revalidateTag;使用cookies.set或者cookies.delete
  2. 调用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既可以在客户端组件使用又可以在服务端组件使用,但是客户端组件只支持第二种方式使用

使用示例

  1. 新建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>
  );
}
  1. 在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;
}
  1. 运行项目访问/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会根据你导入的图片自动决定图片的widthheight属性,可以有效减少累计布局偏移(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>

用于后台预获取资源

客户端组件导航的重要方式之一,除此之外客户端组件还能使用useRouterrouter.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的功能

  1. 安装
sql 复制代码
pnpm add @next/bundle-analyzer // 如果使用别的包管理工具用对应语法安装@next/bundle-analyzer
  1. 配置next.config.js文件
ini 复制代码
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true',})
const nextConfig = {} 
module.exports = withBundleAnalyzer(nextConfig)
  1. 在package.json增加scripts脚本
json 复制代码
// package.json
{
  "scripts":{
   "analyze":"ANALYZE=true pnpm build"
  }
}
  1. 运行脚本

    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="目标网站绝对路径">

内外链优化

详见以掘金示例,利用内链/外链进行网站SEO优化

优先使用HTTPS

HTTPS网站比HTTP网站排名高

拿我线上做的项目搜索效果如下

相关推荐
邵泽明38 分钟前
面试知识储备-多线程
java·面试·职场和发展
夜流冰2 小时前
工具方法 - 面试中回答问题的技巧
面试·职场和发展
zqx_73 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
杰哥在此9 小时前
Python知识点:如何使用Multiprocessing进行并行任务管理
linux·开发语言·python·面试·编程
笑非不退15 小时前
前端框架对比和选择
前端框架
GISer_Jing15 小时前
【React】增量传输与渲染
前端·javascript·面试
Neituijunsir20 小时前
2024.09.22 校招 实习 内推 面经
大数据·人工智能·算法·面试·自动驾驶·汽车·求职招聘
TonyH200221 小时前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包
小飞猪Jay21 小时前
面试速通宝典——10
linux·服务器·c++·面试