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

Laravel 一次请求经历了什么:路由、中间件与响应的完整链路 示意图

第一段:入口文件与容器引导

所有请求都从 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 问题,那是列表页最常见的性能来源。