本篇讲解如何对 Next.js 应用的性能进行监控。
首先是 bundle 包的管理。Next.js 提供了 @next/bundle-analyzer
插件,它会生成每个模块大小及其依赖的可视化报告。你可以据此删除较大的依赖项或者拆分代码从而减少客户端 bundle 的大小。
安装插件:
npm i @next/bundle-analyzer
修改 next.config.mjs
,代码如下:
import { withContentlayer } from 'next-contentlayer'
import bundleAnalyzer from '@next/bundle-analyzer'
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})
export default withBundleAnalyzer(withContentlayer({}))
修改 package.json
,添加脚本命令:
{
// ...
"scripts": {
"analyze": "ANALYZE=true next build"
},
// ...
}
命令行运行 npm run analyze
,它会陆续生成 3 个 HTML 文件,并同时为你打开浏览器展示文件:
一个是 nodejs.html
,展示 nodejs server bundle:
一个是 edge.html
,展示 edge server bundle:
一个是 client.html
,展示客户端浏览器 js bundle:
如果你只是要在上线前,测一下应用的性能,看看有哪些性能优化工作要做,履行一下前端的“职责”,那你可以:
安装 Chrome Lighthouse 插件,安装完成后,开发者工具会有一个 lighthouse 选型,用于测量页面性能。以掘金为例:
lighthouse 会列出需要具体改善的点,参照建议完善即可。
PageSpeed Insights 是一款由 Google 开发的网页性能评估工具,可以帮助开发者评估网页的性能,并提供优化建议。
打开 https://pagespeed.web.dev/,输入网址查看页面测试结果即可:
当然这种性能测试网站还是蛮多的,比如 https://www.webpagetest.org/、https://tools.pingdom.com/ 等等
但是这种上线前测试一下性能的做法问题也很明显,样本有限导致数据不够准确,而且无法准确反应真实用户的体验。
如果真的要解决这个问题的话,就需要在页面实现测量和上报性能相关的数据。可是要测量哪些性能相关的数据呢?性能相关的数据又该如何上报、搜集、分析、输出图标呢?
这里的解决方案有很多种,我提供一种 Prometheus + Grafana 的方式,这套技术选型也是常见的用于数据采集、分析的解决方案。我们且慢慢说来。
其实 Next.js 支持对性能进行测量和上报。Next.js 提供了 useReportWebVitals
hook 自行管理数据。
新建 components/WebVitals.js
,代码如下:
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric)
})
}
修改 next-blog/app/[lng]/layout.js
,添加代码如下:
// ...
import { WebVitals } from '@/components/WebVitals.js'
// ...
export default function RootLayout({ children, params: { lng } }) {
return (
<html lang={lng} dir={dir(lng)} suppressHydrationWarning>
<body>
<WebVitals />
<ThemeProviders>
<header className="flex justify-end">
<ThemeSwitch />
<LangSwitch />
</header>
{children}
</ThemeProviders>
</body>
</html>
);
}
访问页面如 http://localhost:3000/posts/first,打印效果如下:
这 6 个指标就是 Web Vitals(网页指标),它是 Google 的一项计划,旨在针对网页质量信号提供统一指南,这些信号对于提供出色的网页用户体验至关重要。它的目标是简化各种可用的性能测量工具,并帮助网站所有者专注于最重要的指标。
注:其实业界尝试过非常多用于衡量性能和体验的指标,但很多指标由于难以测量、逻辑复杂等原因逐渐消亡。Web Vitals 背靠 Google,算是大浪淘沙,经过时间的检验。但这些指标也会随着时间的推移而演变,目前是这 6 个指标。
为了更直观的展示这些指标,我为大家总结了一个表格:
指标 | 中文名 | 目的 | 标准 | |
---|---|---|---|---|
LCP(Largest Contentful Paint) | 最大内容绘制 | 衡量加载性能 | ||
Core Web Vitals 这组指标侧重于用户体验的三个方面:加载、互动和视觉稳定性 | INP(Interaction to Next Paint) | 交互到绘制延迟 | 衡量互动 | |
CLS(Cumulative Layout Shift) | 累积布局偏移 | 衡量视觉稳定性 | ||
FCP(First Contentful Paint) | 首次内容绘制 | 衡量加载体验,有助于诊断 LCP 问题(服务器响应时间过长或阻塞渲染的资源) | ||
Core Web Vitals 的代理或补充指标 这组指标有助于捕获更广泛的体验或诊断特定问题 | FID(First Input Delay) | 首次输入延迟 | 衡量互动,未来可能完全被 INP 替代 | |
TTFB(Time to First Byte) | 第一字节时间 | 衡量加载体验,有助于诊断 LCP 问题 |
那如何测量这些指标呢?
Chrome 团队提供了 web-vitals 开源工具库,它基于统一的浏览器 Performance API 获取标准化的用户体验数据。
Next.js 提供的 useReportWebVitals hook 背后也是用这个库来实现的。
采集到了数据,数据该如何上报并可视化呢?
我们的技术方案是用 Prometheus 和 Grafana。我们先来介绍一下这两个技术选型:
Prometheus(普罗米修斯)是一个用于监控和报警的开源系统,它自带一个基于时间序列的数据库。
它的架构设计如下:
看到这个架构图先不要害怕,其实很简单。简单来说,Prometheus 由这几部分组成:
关于本篇我们采用的方式:
我们会使用 Express 自建一个服务,用于处理 Next.js Web Vitals 数据的上报。然后由 Prometheus Server 定期拉取指标数据,最后结合 Grafana 做数据可视化。
Grafana 是一款开源的数据可视化工具,使用它是因为:
让我们运行:
# 创建文件夹
mkdir node-monitor && cd node-monitor
# 初始化
npm init
# 安装依赖项,其中 prom-client 是 Prometheus 的 Node 客户端
npm i express cors prom-client
新建 app.js
,代码如下:
import express from 'express';
import client, { collectDefaultMetrics } from 'prom-client';
import cors from 'cors';
const register = new client.Registry();
collectDefaultMetrics({ register });
const app = express();
app.use(express.text())
app.use(cors())
app.get('/metrics', async (_req, res) => {
try {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
} catch (err) {
res.status(500).end(err);
}
});
app.post('/report', function (req, res) {
const { name, rating} = JSON.parse(req.body);
let counter = register.getSingleMetric(name);
if (!counter) {
counter = new client.Counter({
name,
help: req.body,
registers: [register],
labelNames: ['rating'],
});
}
counter.inc({
rating
}, 1);
res.status(200).json({ success: true });
});
app.listen(4001, '0.0.0.0');
在这段代码中,我们提供了一个 /metrics
接口用于 Prometheus Server 拉取数据,一个 /report
接口用于 Next.js 应用上报数据。至于代码具体为什么这样写,参考 prom-client。
修改 package.json
,添加代码如下:
{
// ...
"type": "module",
"scripts": {
"start": "node app.js"
},
// ...
}
运行 npm start
,打开 http://localhost:4001/metrics,效果如下:
数据看不懂?没有关系,这些是 Node 应用相关的数据,可用于监控 Node 性能。我们真正要监控和记录的是 Web Vitals 相关的数据。
修改我们的 Next.js 应用 components/WebVitals.js
:完整代码如下:
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric)
const body = JSON.stringify(metric)
const url = 'http://localhost:4001/report'
// Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
if (navigator.sendBeacon) {
navigator.sendBeacon(url, body)
} else {
fetch(url, { body, method: 'POST', keepalive: true })
}
})
}
navigator.sendBeacon() 方法可用于通过 HTTP POST 将少量数据 异步 传输到 Web 服务器。 它主要用于将统计数据发送到 Web 服务器,同时避免了用传统技术(如:XMLHttpRequest)发送分析数据的一些问题。
我们刷新一下 Next.js 的页面,上报一些数据,再查看 http://localhost:4001/metrics,可以看到产生了一批 Web Vitals 相关的数据指标:
这说明我们的 Next.js 应用(3000 端口)和 Express 应用(4001 端口)已经打通了!
为了方便起见,我们使用 Docker 来开启 Prometheus 和 Grafana。
在 node-monitor 项目下新建 prometheus.yml
文件(当然放在其他地方也是可以的),用于 Prometheus 的配置文件,代码如下:
global:
scrape_interval: 5s
scrape_configs:
- job_name: "next-app"
static_configs:
- targets: ["docker.for.mac.host.internal:4001"]
配置文件告诉 Prometheus 每 5 秒抓取一次所有目标。目标在 scrape_configs 下定义。在 Mac 上,需要使用 docker.for.mac.host.internal
作为主机,以便 Prometheus Docker 容器可以抓取本地 Node.js HTTP 服务器的指标。在 Windows 上,使用 docker.for.win.localhost
;在 Linux 上,使用 localhost
。
然后运行:
docker run --rm -p 9090:9090 \
-v `pwd`/prometheus.yml:/etc/prometheus/prometheus.yml \
prom/prometheus
Windows 用户需要将 pwd 替换为当前工作目录的路径。
注:这里我们将端口开发了默认的 9090 端口,如果你开了 Clash,可能会产生端口冲突。关掉 Clash 或者修改 Clash 的配置文件,将其更改到其他端口。
如果正常开启,此时访问 http://localhost:9090/,可以看到:
对于 Prometheus 的初学者,可以通过访问 http://localhost:9090/api/v1/label/name/values 来查看有哪些数据指标:
我们输入 FCP
,点击 Execute
按钮,如果能查询到数据就说明 Prometheus 应用(开在 9090 端口)与我们的 Express 应用(开在 4001 端口)连接成功:
接下来开启 Grafana,运行:
docker run -d --name=grafana -p 3001:3000 grafana/grafana
正常 Grafana 开启在 3000 端口,但跟我们的 Next.js 应用冲突了,所以开在了 3001 端口。如果正常开启,访问 http://localhost:3001/,会跳转到登录页面:
账号和密码都是 admin,登录后:
选择 Prometheus
,然后点击 Add new data source
按钮,添加 Prometheus 作为数据源,进入设置页面:
Prometheus server URL 这里填写 http://host.docker.internal:9090,这是因为 Prometheus 和 Grafana 都是通过 Docker 开启,环境与本机有隔离,所以无法通过 localhost 直接访问。如果这个地址不行,填写 http://prometheus:9090
试试。
如果能够配置成功,此时我们的各个应用就算联调成功了。一共涉及 4 个应用,Next.js 应用是我们开发的博客项目,博客页面会上报 Web Vitals 数据,Express 应用会接收上报的 Web Vitals,并提供 /metrics 接口供 Prometheus 应用拉取采集数据。最后 Grafana 应用会连接 Prometheus,用可视化的图表代替 Prometheus 的 PromQL 语句查询。
接下来我们开始建立一个 Grafana 图表:
这样我们就建立了一个 FCP 评分占比的饼图:
可以看到评分为 poor 的有多少个,评分为 good 的有多少个。(数据量太少了,所以没有 need improvement)
接下来我们就可以根据自己的需要自定义需要的图表。
目前整体的打通非常简陋,比如 Prometheus 和 Grafana 都没有做数据持久化,一旦重启,数据就会丢失。只监控了自定义的 Web Vitals 指标,Node 性能相关的指标没有做自定义上报。Grafana 也只实现了简单的百分比,而没有实现各种高大上的图表。
Prometheus 和 Grafana 其实内容非常多,碍于作者能力有限,只能为大家简单介绍下这套技术方案,更多的内容还需要大家自己去探索。