把首页 TTFB 从 2.8s 压到 180ms 那晚,我拆掉了三个"看起来没问题"的定时炸弹

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
站长杂谈 77 浏览 0 回复

上周用 Lighthouse 跑分,首页 Performance 只有 23,TTFB 2.8s,我人都麻了。服务器是 4H8G,带宽 5M,按说不至于这么惨。逐层剥洋葱剥了三个晚上,最后发现全是"祖传代码"里看着人畜无害、实际吃性能不吐骨头的小毛病。

第一颗雷:N+1 藏在 foreach 的温柔乡里

首页要展示 12 条文章,每条带作者昵称和分类名。模型里写了关联,但控制器里顺手就:

$list = Article::limit(12)->select();
foreach ($list as $item) {
    $item['author_name'] = $item->author->nickname;
    $item['cate_name'] = $item->category->name;
}

12 条数据,1 次主查询 + 12 次作者查询 + 12 次分类查询 = 25 次数据库往返。Wireshark 抓包一看,MySQL 那边排队等连接池,光这堆查询就耗了 1.9s。

改法简单粗暴:with 预加载 + 字段限定。

Article::with(['author' => function($q) {
    $q->field('id,nickname');
}])->with(['category' => function($q) {
    $q->field('id,name');
}])->field('id,title,author_id,cate_id,cover,create_time')
  ->order('id', 'desc')
  ->limit(12)
  ->select();

25 次变 3 次,TTFB 直接掉 1.5s。这教训我记本子上了:关联模型不用 with,等于慢性自杀。

第二颗雷:缓存穿透比缓存失效更阴险

之前搞了个"智能缓存",文章详情页用 ID 做 key,Redis 存序列化数组。但有个坑没填:ID 不存在时,代码直接透传到数据库,而且每次返回空结果都不写缓存。

爬虫扫了一晚上无效 ID,MySQL `SELECT * FROM article WHERE id = xxxxx` 跑了四万多次,CPU 飙到 90%。监控告警响的时候我还以为是 CC 攻击。

现在我的缓存规则是"三件套":有效数据 TTL 6 小时,空结果 TTL 5 分钟,非法 ID 直接防火墙层面 444 断开。另外加了个布隆过滤器挡在最前面,虽然占点内存,但省下的数据库连接数值回票价。

第三颗雷:静态资源"散装"拖垮首屏

这个最冤。首页引了 7 个 CSS、11 个 JS,还有 6 张没压缩的 banner,总请求 34 个。5M 带宽下,TCP 握手都要排队,Chrome 瀑布图红成一片。

我的处理清单:

1. 小图全部转 WebP,fallback 用 `` 标签兜底,平均体积从 180KB 压到 35KB
2. CSS/JS 用 Vite 打包成两个 chunk,加 hash 文件名,CDN 缓存一年
3. 字体文件用 `font-display: swap`,不让自定义字体阻塞渲染
4. 非首屏图片加 `loading="lazy"`,首屏只保留一张 cover

OSS + CDN 组合拳打完,静态资源加载从 4.2s 降到 380ms。Lighthouse 里那个"Reduce unused CSS"的警告还在,但已经不影响用户体感了。

最后说个反直觉的

我之前迷信"查询越少越好",把首页数据全塞一个 SQL 用 JOIN 硬怼,结果产生临时表,filesort 全表扫描,比 N+1 还慢。现在我的原则是:该拆就拆,该预加载就预加载,该缓存就缓存,别为了 SQL 数量好看牺牲执行计划。

现在首页 TTFB 稳定在 180ms 左右,Lighthouse Performance 跑到 91。优化这事没有银弹,就是拿工具一层层剖,找到真正的瓶颈,而不是凭感觉拍脑袋。

你们首页 TTFB 多少?有没有被什么"看起来没问题"的代码坑过?

评论0
回复 · 0
还没有回复
微信客服 微信客服