如何测量Next.js SSR服务端HTML响应耗时及排查性能瓶颈
Next.js SSR服务端全链路耗时排查方案
一、测量服务端从接收请求到发送HTML的总耗时
方法1:自定义服务器拦截请求
创建自定义服务器文件(如server.js),在请求进入时记录起始时间,响应完成时计算总耗时:
const { createServer } = require('http'); const { parse } = require('url'); const next = require('next'); const dev = process.env.NODE_ENV !== 'production'; const app = next({ dev }); const handle = app.getRequestHandler(); app.prepare().then(() => { createServer((req, res) => { const start = Date.now(); // 监听响应结束事件 res.on('finish', () => { const totalTime = Date.now() - start; console.log(`[${req.method}] ${req.url} - 总耗时: ${totalTime}ms`); }); const parsedUrl = parse(req.url, true); handle(req, res, parsedUrl); }).listen(3000, (err) => { if (err) throw err; console.log('> Ready on http://localhost:3000'); }); });
启动命令改为node server.js(开发环境可加NODE_ENV=development),控制台会输出每个请求的总耗时。
方法2:Pages Router中间件记录
在pages/_middleware.js中注入请求时间追踪:
import { NextResponse } from 'next/server'; export function middleware(req) { const start = Date.now(); // 绑定起始时间到请求上下文,后续环节可复用 req.startTime = start; // 监听响应完成事件打印总耗时 req.res.on('finish', () => { const totalTime = Date.now() - start; console.log(`请求 ${req.nextUrl.pathname} 总耗时: ${totalTime}ms`); }); return NextResponse.next(); }
二、拆解SSR各环节的耗时
1. 精准测量getInitialProps执行细节
在页面组件的getInitialProps中拆分各步骤耗时:
Page.getInitialProps = async (ctx) => { const propsStart = Date.now(); // 记录API请求耗时 const apiStart = Date.now(); const data = await fetch('https://api.example.com/data'); const apiTime = Date.now() - apiStart; // 记录业务逻辑耗时 const bizStart = Date.now(); // 你的业务处理代码 const bizTime = Date.now() - bizStart; const propsTotal = Date.now() - propsStart; console.log(`[${ctx.pathname}] getInitialProps总耗时: ${propsTotal}ms | API请求: ${apiTime}ms | 业务逻辑: ${bizTime}ms`); return { data }; };
2. 覆盖React渲染及Next.js内部流程
通过自定义_document.js扩展Server Timing API,添加渲染阶段的时间标记:
import Document, { Html, Head, Main, NextScript } from 'next/document'; class MyDocument extends Document { static async getInitialProps(ctx) { const renderStart = Date.now(); const initialProps = await Document.getInitialProps(ctx); const renderTime = Date.now() - renderStart; // 向响应头添加渲染耗时信息,可在Chrome网络面板查看 ctx.res.setHeader('Server-Timing', `render;dur=${renderTime}`); return initialProps; } } export default MyDocument;
同时在自定义服务器中添加路由匹配环节的耗时记录:
createServer((req, res) => { const start = Date.now(); const routeMatchStart = Date.now(); const parsedUrl = parse(req.url, true); const routeMatchTime = Date.now() - routeMatchStart; res.on('finish', () => { const totalTime = Date.now() - start; console.log(`[${req.method}] ${req.url} - 总耗时: ${totalTime}ms | 路由匹配: ${routeMatchTime}ms`); }); handle(req, res, parsedUrl); });
3. 用Node.js性能分析工具定位黑盒环节
手动在关键节点添加性能标记,配合Chrome DevTools分析:
// 自定义服务器请求开始时 performance.mark('server_request_start'); // getInitialProps内部 performance.mark('getInitialProps_start'); // ...你的getInitialProps代码 performance.mark('getInitialProps_end'); performance.measure('getInitialProps总耗时', 'getInitialProps_start', 'getInitialProps_end'); // _document.js渲染阶段 performance.mark('render_start'); const initialProps = await Document.getInitialProps(ctx); performance.mark('render_end'); performance.measure('React渲染耗时', 'render_start', 'render_end'); // 响应完成时 performance.mark('server_response_end'); performance.measure('服务端总耗时', 'server_request_start', 'server_response_end'); // 打印所有测量结果 const measures = performance.getEntriesByType('measure'); measures.forEach(measure => { console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`); });
启动时使用NODE_OPTIONS='--inspect' next dev,打开Chrome DevTools的Performance面板录制请求,即可看到自定义标记的耗时区间。
4. 排查全局瓶颈
- 标记
_app.js中getInitialProps的执行时间,检查是否存在全局阻塞代码 - 验证
getInitialProps中是否有重复请求或未缓存的计算逻辑,添加缓存减少重复开销
内容的提问来源于stack exchange,提问作者Guido Ferraro
相关产品推荐
相关产品推荐

