ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Next.js 性能优化实战:图像、字体与媒体资源全攻略

Next.js 性能优化实战:图像、字体与媒体资源全攻略 做前端这些年我真正开始认真对待 Next.js 的优化能力是因为一次很现实的线上事故页面体积 1.2MB首屏加载慢到 3 秒开外被客户当场截图发到了项目群里。查下来发现罪魁祸首不是什么复杂的业务逻辑而是几张完全没有压缩的轮播图、十几 MB 的字体文件以及一个进页面就自动播放的视频。那之后我意识到React 框架本身解决的是组件化和数据流的问题但一个真实可上线的 Web 应用能不能跑得快往往取决于图像、字体、媒体这些“边缘”资源有没有被认真对待。这篇教程我把 Next.js 里最常用的图像、字体、媒体优化三块内容彻底拆开讲透从原理到代码再到踩坑记录都会覆盖到。无论你是刚接触 Next.js还是正从 Vite React 迁移过来又或者已经在生产环境里被性能指标折磨过这篇内容都值得你收藏下来慢慢对照。每个部分我都会给出可直接复制的代码片段和配置项尽量做到看完就能在自己的项目里落地。1. 为什么 Next.js 的优化能力值得单独学很多从 Vite React 转过来的人第一反应是优化不就是压缩图片、拆分代码、加缓存吗在 Next.js 里确实也是这个思路但它多了一层“构建时 运行时”的双重设计。理解这一层后面所有配置才能不靠背而是靠推导。1.1 三个瓶颈的真实场景先说图像。电商项目里商品图动辄几百 KB如果按 2x 屏幕计算一张 800px 宽的主图实际上需要 1600px 物理像素。传统 React 项目里你得自己写一套picture判断、手动算 srcset、再处理懒加载。写得再好换个团队成员接手大概率会有人把loadinglazy忘在某张首屏图上。再说字体。国内团队做中后台系统经常会直接放一个方正或者思源的黑体一个 woff2 文件下来 8-12MB 是常有的事。第一次用 Chrome DevTools 看网络面板的时候你会发现整整几秒钟浏览器都在阻塞渲染就为了等那个字体。这种问题在纯前端项目里往往要手动扣而且很容易被忽略。最后说媒体。视频背景、音频播放器、大尺寸的 PNG 图片很多开发者默认丢到video src或者img src里就完事。但在 Next.js 里不管你是不是用了它的优化组件服务端渲染返回的 HTML 本身就带着渲染顺序的问题浏览器解析到一个 30MB 的 MP4 路径时会立刻发起请求哪怕这个视频在第三屏以外。1.2 核心原理构建时优化还是运行时优化Next.js 本质上是一个“编译器 运行时”的组合。你用next build构建出来的产物是静态页面和 Node.js/Edge 函数Next.js 会在构建期自动完成代码分割、静态资源指纹、预渲染解析并在运行时通过内置的 Image 和 Font 组件继续做资源级别的优化。举个例子next/image组件不会在构建时把所有图片全部压缩好而是按需处理浏览器请求一张 800px 宽的图时Next.js 的 image optimizer 才根据查询参数动态生成对应尺寸和格式的产物并写入缓存。这样做的好处是你不需要为每一个尺寸预生成文件坏处是如果线上环境没有持久化缓存第一次访问时会有一点冷启动延迟。理解了这个逻辑后面优化配置才能有的放矢。2. 图像优化从 next/image 到生产级配置next/image是 Next.js 里最值得先学会的组件。它把响应式图片、格式转换、懒加载、占位图这些能力内建好了你只需要关注业务属性而不是反复造轮子。2.1 next/image 的基本用法先看一下最常见的两种写法。第一种是使用本地导图资源import Image from next/image; import profilePic from ../../public/images/avatar.png; export default function Page() { return ( Image src{profilePic} alt用户头像 width{400} height{400} classNamerounded-full / ); }第二种是远程图片。这里有一个容易踩的坑直接写一个https://cdn.example.com/a.png给next/image默认是跑不起来的必须在next.config.js里配置允许加载的域名// next.config.js /** type {import(next).NextConfig} */ const nextConfig { images: { remotePatterns: [ { protocol: https, hostname: cdn.example.com, port: , pathname: /**, }, ], }, }; module.exports nextConfig;为什么 Next.js 要强制你配置 remotePatterns因为 image optimizer 是在服务端代为请求图片的。如果不限制域名任何客户端都可以通过/_next/image?url任意外链来让你的服务器去下载外部资源这会变成 SSRF 攻击的入口。这个安全设计不是多此一举而是必须保留的底线。2.2 本地图片与远程图片的差异本地图片比如直接从src目录导入或者放在public下可以让 Next.js 在构建时直接读取文件信息所以可以自动生成srcset和模糊占位图blurDataURL。远程图片则必须在配置了 remotePatterns 后组件内还必须显式提供宽度和高度否则它会报错。注意在 Next.js 14 及以后版本远程图片如果不给width和height必须使用fill模式。这不是 bug而是为了避免布局偏移CLS——它需要知道图片的渲染尺寸否则页面在图片加载完成前后会发生跳动。那如果没有固定尺寸、图片又是自己服务器上的怎么处理用fill模式配合sizes属性就可以import Image from next/image; export default function Banner() { return ( div classNamerelative h-[300px] w-full Image srchttps://cdn.example.com/banner.png alt横幅 fill sizes(max-width: 768px) 100vw, 1200px classNameobject-cover priority / /div ); }fill会让图片填满父容器的全部宽度和高度配合object-cover实现居中裁剪。sizes是告诉浏览器这张图在不同视口下需要多宽的物理像素它直接影响生成的srcset。2.3 常用参数的实战经验从项目里总结了一份参数选择建议参数默认值适用场景我的建议width/height无已知展示尺寸的图片必须给避免 CLSfillfalse图片尺寸依赖父容器配合sizes和object-cover使用quality75常规照片人像或商品图建议 80-85不要低于 70priorityfalse首屏 LCP 图片只给第一屏最重要的图片加sizes无响应式布局尽量写准确写错了反而多加载图placeholderempty提升观感本地图建议blurpriority这个属性是我踩过最多坑的地方。很多人误以为它是“图片优先上传”的意思其实它是把该图片改成预加载也就是会在head里插入link relpreload asimage同时强制取消懒加载。所以它只能用于首屏的 LCP 元素用了第二屏以后的图反而会造成带宽浪费。再补充一个细节明明设置了sizes为什么图片加载出来还是那么大这是因为它只影响生成的srcset如果你父容器宽度是不确定的浏览器会默认选择更大的图。这是有意为之因为浏览器要把图片当作 CSS 像素渲染拿不准的时候当然选高清图。所以做响应式页面时sizes一定要覆盖移动端、平板、桌面三种常见宽度。3. 字体优化next/font 与中文网站的现实解法字体是页面性能里最容易被忽视但也最容易翻车的一环。Next.js 从 13.2 版本开始整体推进了next/font它可以让你在导入字体时Next.js 自动在构建期把字体文件下载并加上指纹同时生成合适的font-display策略。3.1 next/font 的基本使用先在 App Router 的任意页面或布局里写import { Inter } from next/font/google; const inter Inter({ subsets: [latin], display: swap, }); export default function Layout({ children }: { children: React.ReactNode }) { return ( html langzh-CN className{inter.className} body{children}/body /html ); }这段代码做的事情包括构建时从 Google Fonts 拉取 Inter 字体文件并本地化托管、生成带 hash 的文件名、自动加上font-display: swap、通过 CSS 变量注入到className上。不要小看“本地化托管”这一点它避免了你页面运行时直接向 Google Fonts 发起跨域请求也减少了渲染阻塞和第三方 DNS 时间。如果字体文件已经放在项目里可以用next/font/localimport localFont from next/font/local; const myFont localFont({ src: ./fonts/my-font.woff2, display: swap, });这种方式适合公司内部设计系统字体、开源字体的定制版本等场景完全不走外网构建之后就是纯静态资源。3.2 变量字体与 font-display 的底层逻辑为什么要用变量字体常规字体文件要为每一套字重单独准备一个文件比如 400、500、700、900每个文件都能再吃几百 KB。变量字体用一个文件把整套字重都打包在保证质量的同时把体积大幅度降下来Next.js 官方字体接入时也在引导这种写法。display: swap的意思是文本先用 fallback 字体渲染等自定义字体加载完后再切换。这解决了字体阻塞文本渲染的问题但也有一个副作用——可能产生 FOUT字体闪烁。在 Next.js 里你还能看到display: optional之类的可选值但官方默认推荐swap因为它对页面的第一张画First Paint最友好。注意next/font/google可以用来加载英文字体和部分支持拉丁字符的字形但对于中文网站情况完全不同。中文字体动辄几 MBGoogle Fonts 上也几乎没有覆盖全量中文的字体子集直接用会导致首屏加载一个超大文件效果比不用还差。3.3 中文网站的自托管方案我做中文站点时很少直接用next/font/google里面的中文路径而是自己准备一个精简版字体。做法是找一个开源的中文字体比如思源黑体提取常用 3500 字或 6763 个 GB 2312 汉字用工具切出子集生成 woff2 格式放进项目里。切字体的工具有很多我用得比较多的是fontmin和fonttools后者偏 Python 环境。把生成的字体文件放到/fonts目录然后用next/font/local引入import localFont from next/font/local; const sourceHanSans localFont({ src: ./fonts/SourceHanSansCN-Regular.subset.woff2, display: swap, preload: true, fallback: [PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif], });这里做两个关键动作。第一preload: true只对首屏需要的字体文件设置如果你有多个字重不要全部 preload否则首屏仍然会发起多个字体请求。第二一定要写fallback这样字体还没加载完时用户看到的不是一片空白而是系统默认的中文字体渲染效果。为了进一步减少体积你还可以只保留数字和标点配上中文字体变体。比如一个电商页面价格、数量这些数字用西文字体渲染中文标题用另一套字重这样整体体积能压到 200KB 以内。4. 媒体优化视频、音频与 OG 图的工程实践图像和字体搞定了接下来就是视频、音频和其他多媒体内容。很多 Next.js 项目在这些资源的处理上非常简单粗暴直接往 JSX 里塞一个video标签就结束了结果首屏请求里出现了一个上百 MB 的 MP4。4.1 视频的懒加载与封面策略视频优化的第一条原则能不自动播放就不自动播放。首屏放一个自动播放的视频会让用户的流量在打开页面的瞬间就被耗尽。如果业务必须要自动播放要保证三点视频本身是无声的或者设置了muted、使用了playsInline、并且文件经过高度压缩。在 Next.js 里我通常不会对首屏视频做懒加载因为用户即点即开延迟加载会造成播放器卡顿。但对于非首屏的视频强烈建议用懒加载方式。可以用原生loading配合 IntersectionObserver 实现也可以直接用浏览器的preload属性来控制video controls preloadnone poster/images/video-poster.jpg classNameaspect-video w-full source src/media/banner.mp4 typevideo/mp4 / /videopreloadnone的意思是只有用户点播放时才加载视频本体但封面图还是会显示。这个封面图建议用next/image生成并严格设定尺寸因为视频容器本身会占据布局空间封面图尺寸不稳定很容易造成 CLS。如果你确实需要视频自动播放需要把视频从首屏移除或者尽量压缩。实践下来一个 1080p、5 秒内、H.264 编码的无声背景视频压到 500KB 以内是完全可行的如果目标用户主要在 WiFi 环境下可以放宽到 1MB。4.2 音频与其它媒体的处理要点音频文件和视频类似优先使用 HTTP 范围请求preloadnone建议作为默认值尤其是当页面里有多个音频文件时。可以在audio上加上preloadmetadata让浏览器只读取文件的时长等信息不下载整个音频文件。在处理字幕、歌词等文本文件时尽量放在和页面同一域或同一 CDN 下避免跨域请求。如果你正在迁移一个旧站音频文件放在第三方对象存储里路径中带特殊字符记得在配置里加上响应头Accept-Ranges: bytes否则一些浏览器可能无法拖动播放进度条。至于其他二进制媒体文件比如 PDF、PPT、压缩包原则是不要放在public目录之外。Next.js 服务器的职责是渲染页面和处理 API不应该承担大文件下载的带宽。所有大文件都建议走 CDN你在页面里只留一个链接即可。4.3 用 next/og 动态生成社交分享图再分享一个很实用的媒体优化技巧用next/og动态生成 Open Graph 分享图。以前每篇文章都需要设计一张 1200x630 的分享图费时且不好维护。现在可以直接在 Edge Runtime 环境里通过代码渲染// app/og/route.tsx import { ImageResponse } from next/og; export const runtime edge; export async function GET() { return new ImageResponse( ( div style{{ display: flex, width: 100%, height: 100%, background: #1a1a1a, color: #fff, fontSize: 64, padding: 48, }} 每一篇文章的专属分享图 /div ), { width: 1200, height: 630 } ); }这样可以在不预先生成任何图片文件的情况下为每一篇文章生成一张独特的分享图并且完全享受 CDN 的缓存。用的时候只需要在页面的 metadata 里指向这个路由生成一个带?title参数的 URL 就好。提示next/og只能在 Edge Runtime 或 Serverless 函数里运行不要试图在 Node.js 纯后端环境里执行它。部署到 Vercel 时它是默认支持的如果自建服务器需要确认你的 Next.js standalone 模式配置正确。5. 性能检测与常见问题排查配置写完了怎么确定效果直接看数据不要靠感觉。Next.js 提供了非常方便的内置性能监控方式同时也会在控制台直接输出一些关键错误帮你定位问题。5.1 用 Core Web Vitals 数据说话在 App Router 或 Pages Router 中都可以通过useReportWebVitals把性能指标上报出来。如果是中后台项目不想引入额外的监控 SDK也可以先直接用 Lighthouse 粗略看一遍// app/layout.tsx import { useReportWebVitals } from next/web-vitals; export function ReportVitals() { useReportWebVitals((metric) { if (metric.name LCP) { console.log(LCP:, metric.value); } }); return null; }如果是生产环境建议把指标透传到监控平台。重点盯三个指标LCP最大内容绘制2.5s 以内、CLS累计布局偏移0.1 以内、INP交互到下一次绘制200ms 以内。你会发现大部分 LCP 问题都出在图片和字体上CLS 问题几乎都来自没写宽高或媒体容器。Next.js 还有一个很有用的构建期输出。执行next build后Terminal 会打印每个路由下页面产物体积、JS 包首屏大小等信息。如果某个页面的 JS 包异常大基本可以判断是某个组件没有做动态 import。5.2 常见问题速查表我把平时群里问得最多的问题整理成一张速查表问题原因解决方案远程图片不显示报 hostname 错误未配置 remotePatterns在next.config.js里添加对应域名图片加载完成后页面跳动缺少 width/height 或 fill 模式下的容器高度不稳定给图片明确尺寸或给父容器固定宽高比自定义字体加载后页面闪烁字体文件太大或未设置 fallback 字族缩字体子集、开启display: swap、设置合理的 fallback 字体栈首屏请求数太多LCP 偏慢多张图片都添加了 priority只保留 LCP 元素上的 priority视频自动播放时首屏卡顿视频文件过大且 preload 默认为 auto压缩视频、preloadnone、使用 poster 封面使用 next/og 时控制台报 Runtime 错误在 Node.js 运行时环境中调用了 ImageResponse将路由设置export const runtime edge这中间最隐蔽的是字体闪烁问题。很多人把它归咎于网络慢其实很多情况下是font-display: swap在字体加载完后切换导致的。如果想完全避免闪烁可以把display改为optional但代价是如果字体加载超过一定时限浏览器就不会再应用自定义字体页面会一直使用 fallback。中英文混合的场景下我通常对中文使用swap对西文字体使用optional因为西文加载快而且英文字体对 fallback 的观感影响不大。写在最后的一点体会图像、字体、媒体优化这三件事单拆开都不算难但它非常考验你对“构建时”和“运行时”的理解。我在多个生产项目里的做法是图像全部走next/image本地图片尽量用blur占位字体准备两套方案西文直接next/font/google本地化中文用裁剪过的 woff2 子集视频默认preloadnone封面图交给next/image生成。每次配置完资源用 Lighthouse 跑一遍重点看 LCP 和 CLS再检查 Network 面板里有没有多余的大文件请求基本就能保证一个不错的性能底子。最后再分享一个小技巧开发环境里next/image会默认关闭缓存和压缩所以你感觉性能数据可能不如预期想要验证线上效果用next build next start起生产模式再测那一份数据才是用户真正会体验到的数据。先把资源体积压下来再把数据报上去一个看上去“普普通通”的 Next.js 项目就能跑出让人意外的性能成绩。
返回列表