直接在循环内用 microtime(true) 记录起止时间是最轻量可控的方式;debugbar 和 telescope 无法监控循环内部耗时,因其仅捕获 sql 或整请求周期;需单独测量单次迭代与整体循环的时间及内存增量,并警惕 n+1 查询和重复计算等隐藏瓶颈。

直接在循环内部用 microtime(true) 记录起止时间,是最轻量、最可控的方式;任何依赖中间件或全局监听的方案,在循环场景下都会漏掉细节或引入干扰。
为什么不能靠 Debugbar 或 Telescope 查循环耗时
Debugbar 的 Queries 面板只捕获数据库查询,Telescope 的 Requests 面板统计的是整个 HTTP 请求生命周期——它们都把循环当作“黑盒”里的普通 PHP 执行,不会拆解、也不会标记循环体内的耗时分布。如果你发现某次请求总耗时 800ms,但 Debugbar 显示 SQL 总共才 120ms,那剩下近 700ms 很可能就卡在某个未被监控的 foreach 里。
常见错误现象:
- 在控制器中写
foreach ($items as $item) { $item->process(); },页面变慢,但 Debugbar 没报慢查询 - 用
DB::listen()监听,日志里一条慢 SQL 都没有,Log::info('done')却隔了 3 秒才输出 - Telescope 的 Timeline 显示 “PHP Script” 耗时突增,但点进去看不到具体哪行代码干的
怎么精准测单次循环迭代的耗时
别用平均值,先看单次——尤其当数据量小但某次迭代明显拖慢时(比如第 47 次遇到一个超大附件解析)。
实操建议:
- 在循环开始前加
$start = microtime(true); - 每次迭代末尾计算差值:
$iterTime = round((microtime(true) - $start) * 1000, 2); $start = microtime(true); - 用
Log::debug()记录超过阈值的迭代,例如:if ($iterTime > 50) { Log::warning('slow iteration', ['index' => $i, 'ms' => $iterTime]); } - 避免在循环内调用
dd()或dump(),它们会中断后续迭代,掩盖真实瓶颈 - 注意:CLI 环境下
microtime(true)受 I/O 影响较大,如循环中含file_get_contents()或curl_exec(),需单独剥离测试
怎么测整个循环块的总耗时与内存增量
这是判断循环是否该拆分、缓存或异步化的关键依据。只看时间不够,有些循环不慢但吃内存(比如不断 array_merge() 构造大数组)。
实操建议:
- 循环前记录两个基准:
$start = microtime(true); $memBefore = memory_get_usage(true); - 循环结束后立即采集:
$duration = round((microtime(true) - $start) * 1000, 2); $memAfter = memory_get_usage(true); - 计算净增量:
$deltaMem = $memAfter - $memBefore;,单位字节,除以 1024 得 KB - 日志统一打点:
Log::info('loop-profile', ['ms' => $duration, 'mem_kb' => (int)round($deltaMem / 1024), 'count' => count($items)]); - 不要用
memory_get_peak_usage()替代memory_get_usage(true)——前者是进程峰值,不是本次循环引起的增量
容易被忽略的陷阱:循环中隐藏的 N+1 和重复计算
很多“慢循环”其实不是循环本身慢,而是每次迭代都在触发一次数据库查询或重复解析同一份 JSON。
典型场景:
-
foreach ($users as $user) { echo $user->profile->avatar; }—— 缺少with('profile'),实际执行了n次查询 -
foreach ($logs as $log) { $data = json_decode($log->payload); process($data); }—— 每次都重新 decode 同一串大 JSON -
foreach ($reports as $r) { $r->generatePdf(); }——generatePdf()内部反复加载相同字体文件,没走缓存
这类问题不会在循环计时里直接暴露为“某次迭代耗时高”,而是表现为总耗时随数据量线性增长、内存占用阶梯式上升。必须结合 DB::getQueryLog() 或 Telescope 的 Queries 标签页交叉验证,确认循环体内是否意外触发了 DB/IO 操作。











