Nuxt 3 文章列表越翻越慢:我把 skip/limit 换成了游标分页
个人站文章列表翻到后面接口变慢,我用 MongoDB 执行计划定位深分页问题,再用复合索引与游标分页替换 skip/limit,记录一次小站性能修复过程。

作者
ZHOUYI
最近给个人站的文章列表加了几百篇内容后,发现翻到第 20 页开始明显变慢。接口不是首屏,但用户点“更多”时会卡一下。考虑到后面还会接小程序和 RSS,我决定把分页方式改掉。
先用 explain 看慢在哪
最开始接口是 Article.find().sort({ createdAt: -1 }).skip(page * size).limit(size)。数据不多时没问题,但页码越大,skip 越需要跳过前面的文档。用 explain 看执行计划,能看到扫描数量和排序成本都随页码上升。对内容站来说,这种深分页很常见,问题不在 Nuxt,而在查询方式。
换成复合索引加游标分页
我把排序稳定成发布时间加 _id,并建了复合索引。前端不再传 page,而是传上一页最后一条的 cursor。下一页只查询比它更早的数据,limit 固定。这样无论翻到第几页,查询都只取当前页需要的范围,避免大范围跳过。
const cursor = req.query.cursor;
const query = cursor
? { $or: [
{ createdAt: { $lt: cursor.createdAt } },
{ createdAt: cursor.createdAt, _id: { $lt: cursor.id } }
] }
: {};
const items = await Article.find(query)
.sort({ createdAt: -1, _id: -1 })
.limit(20)
.lean();接口返回 nextCursor 时,我只取列表最后一条的 createdAt 和 _id。前端把它拼到下一页请求里,服务端不保存页码状态。这样对 Nuxt 的 useFetch 也比较友好,参数变化就能重新请求。
几个容易忽略的点
- 排序字段要稳定,最好用时间加 _id 做复合排序,避免同秒文章跳动。
- 复合索引建议覆盖排序字段,具体字段顺序和查询条件有关,以官方文档为准。
- 如果列表带筛选,游标条件也要跟着变,否则可能漏数据或重复。
- Redis 可以缓存第一页和热门分类,深页缓存收益不高,还容易脏。
- SEO 上保留可抓取的列表入口,分页链接别只依赖 JS。
这次改动没有引入复杂架构,只是把查询路径从“数到第几页”改成“从某个位置继续”。小站的性能问题很多时候不是机器不够,而是查询模式不适合数据增长。先定位执行计划,再做最小改造,比盲目加缓存更稳。