很多人写 Laravel 是从控制器开始的:路由配好、方法写好,能跑通就算完成。但线上出现「接口 800ms」时,如果不知道框架在这一路上做了什么,就很难判断时间花在了哪里。这篇文章按实际执行顺序,把一次请求拆成四段来看。

第一段:入口文件与容器引导
所有请求都从 public/index.php 进入。这个文件只做三件事:加载 Composer、创建应用实例、把请求交给 HTTP Kernel。
- 创建应用实例时会注册基础绑定(容器、配置、异常处理),但不会立即连数据库;
- Kernel 的
bootstrap()依次执行环境变量、配置、异常、Facade、服务提供者与路由的引导; - 服务提供者的
register()只做绑定,boot()才会真正执行逻辑,所以耗时的初始化不要塞进boot()。
如果发现「一个空接口也要 300ms」,先用 php artisan optimize 把配置、路由、视图缓存起来,再看耗时是否回落。多数情况是每次请求都在重复解析配置与服务提供者。
第二段:路由匹配
路由文件在引导阶段被加载,请求进入后由 Router 拿 URL 与请求方法去匹配。匹配结果不只是「哪个控制器」,还包含路由参数、中间件列表与参数绑定规则。
Route::middleware(['auth'])->prefix('admin')->group(function () {
Route::get('/posts/{post}', [PostController::class, 'show'])
->name('admin.posts.show');
});
路由参数默认是字符串,隐式模型绑定会在控制器执行前查库:{post} 加上类型提示 Post $post 就会自动 findOrFail。这一步查不到会直接抛 404,所以它发生在中间件之后、控制器之前。
路由缓存(
route:cache)能明显减少匹配开销,但闭包路由无法被缓存,生产环境尽量用控制器方法。
第三段:中间件管道
中间件是洋葱模型:先进后出。全局中间件在最外层,路由中间件按声明顺序排列,越靠前的越早进入、越晚退出。
public function handle(Request $request, Closure $next): Response
{
$start = microtime(true);
$response = $next($request);
$cost = round((microtime(true) - $start) * 1000, 1);
$response->headers->set('X-Cost', $cost.'ms');
return $response;
}
这段代码放在路由中间件里,就能在响应的 X-Cost 头里看到「从进入该中间件到响应生成」的耗时。想对比「框架开销」与「业务耗时」,可以再在全局中间件里放一份,两个数字一减就清楚了。
顺序带来的坑
- 依赖 Session 的中间件必须排在
StartSession之后,否则读不到登录态; auth放在throttle之前更省资源:未登录请求不必计入限流;- 想让响应头在异常时也带上,需要在中间件里用
try / finally而不是只在成功分支处理。
第四段:控制器与响应
控制器返回的内容会被统一转换:数组或模型变成 JSON,字符串变成 HTML 响应,View 则交给 Blade 渲染。终止中间件(terminable middleware)与 app()->terminate() 在响应发送给客户端之后才执行,适合写日志、上报监控.
需要抓取真实响应头排查问题时,除了 curl -I,也可以直接用站内的网页源码查看工具看状态码、重定向链与响应头,省去本地配置环境。
把链路用起来
理解这四段之后,排查性能问题就有了顺序:
- 空接口也慢 → 看引导与路由缓存;
- 只有特定接口慢 → 看中间件与数据库查询,配合
DB::listen统计 SQL 条数; - 响应生成快但客户端慢 → 看响应体大小、压缩与静态资源,而不是继续优化 PHP。
下一篇可以接着看Eloquent 的 N+1 问题,那是列表页最常见的性能来源。