BLOG
文章列表懒加载为什么不能把SEO一起丢掉
懒加载可以改善体验,但公开文章仍应在服务端输出可抓取链接。
文章列表页为了浏览体验引入懒加载,已经是很常见的做法。但有一个前提常常被忽略:如果列表里的文章是需要被公开访问的,那么列表页本身就不能只依赖懒加载来输出内容。懒加载解决的是“用户滚动时如何加载更多”的问题,而搜索引擎看到的是“页面初始返回的 HTML 里有什么”。这两件事的服务对象不同,不能互相替代。
一个文章列表,尤其是知识库或博客的列表页,承担着两个任务:一是让读者能顺畅地浏览大量条目,二是让搜索引擎能发现并抓取每一篇详情页的 URL。如果列表页的链接全部由 JavaScript 在滚动时才生成,那么搜索引擎的爬虫在抓取列表页时,得到的可能只是一个空壳,详情页的入口就丢失了。这并不意味着懒加载不能用,而是要求页面在服务端渲染或构建时,就把真实可访问的链接放在 HTML 里,懒加载只负责后续的增量补充。
先分清哪些内容必须“可见”
动手改代码之前,先确认列表页的使用对象和内容边界。公开的文章列表,首屏至少应该包含若干条真实文章的标题和链接,这些链接必须是用户可以直接复制、搜索引擎可以直接抓取的普通 <a> 标签。如果列表里混有需要登录才能查看的内容,那么这些条目在未登录状态下就不应该出现在 HTML 里,否则爬虫会抓到一堆需要权限的地址,反而造成索引质量问题。
另一个容易忽略的点是翻页方式。懒加载通常有两种实现:一种是滚动到底部自动加载下一页,另一种是点击“加载更多”按钮。无论采用哪一种,页面底部或 URL 结构里都应保留传统的分页入口。原因很简单:如果用户通过键盘操作,或者使用屏幕阅读器,滚动触发的方式可能根本无法被感知;而如果 JavaScript 加载失败,用户至少还能通过分页链接手动翻页。分页 URL 本身也是搜索引擎理解列表结构的重要信号,比如 /page/2 这样的路径,比一个永远不变的列表 URL 配合无限滚动更利于爬虫遍历。
处理顺序:先保证基础可用
实际操作时,建议按下面的顺序处理,而不是先优化动画效果或加载速度。
第一步,确认服务端输出的 HTML 里包含哪些内容。打开浏览器的“查看源代码”功能,而不是开发者工具里的 Elements 面板,因为后者显示的是经过 JavaScript 修改后的 DOM。查看源代码能看到爬虫和禁用脚本的用户真正拿到的内容。如果源代码里只有一两个链接,或者根本没有链接,那问题就出在服务端渲染或构建配置上。
第二步,实现懒加载时,优先选择 Intersection Observer 来监听列表底部的占位元素,而不是在滚动事件里频繁计算位置。Intersection Observer 是浏览器原生提供的接口,性能开销小,而且不需要依赖第三方库。核心逻辑是:当占位元素进入视口时,向服务器请求下一页数据,然后把返回的 HTML 片段或 JSON 数据渲染到列表末尾。这里要注意,请求下一页的 URL 应该是普通的分页地址,而不是一个只返回 JSON 的接口——前者在 JavaScript 失败时可以直接用浏览器打开,后者则完全依赖脚本。
第三步,处理加载失败和边界状态。如果请求下一页失败,界面上要显示明确的错误提示,并且提供“重试”按钮。更重要的是,失败后不能把分页入口隐藏起来,否则用户会被困在当前页面,无法继续浏览。空数据状态也要考虑:如果列表本身没有内容,或者某一页之后没有更多数据了,需要明确告诉用户“没有更多了”,而不是让占位元素一直存在,导致重复请求。
下面是一个简化的判断逻辑示例,展示如何区分“还有更多”和“已经到底”:
```javascript
// 假设服务端在返回 HTML 时,在列表末尾放置了一个 <div id="sentinel">
const sentinel = document.getElementById('sentinel');
const list = document.getElementById('article-list');
let currentPage = 1;
let isLoading = false;
let hasMore = true;
async function loadNextPage() {
if (isLoading || !hasMore) return;
isLoading = true;
try {
const response = await fetch(/articles?page=${currentPage + 1});
if (!response.ok) throw new Error('加载失败');
const html = await response.text();
if (html.trim() === '') {
hasMore = false;
sentinel.textContent = '已显示全部文章';
return;
}
list.insertAdjacentHTML('beforeend', html);
currentPage++;
} catch (error) {
sentinel.textContent = '加载失败,请重试';
sentinel.onclick = loadNextPage; // 点击重试
} finally {
isLoading = false;
}
}
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) loadNextPage();
});
observer.observe(sentinel);
}
```
这个例子里有一个关键点:如果服务端返回空字符串,就说明没有更多内容了,此时要把 hasMore 设为 false,并停止监听。否则,占位元素始终在视口内,会不断触发请求。另外,错误处理时把重试动作绑定在占位元素上,比弹出一个难以关闭的提示框更实用,尤其对键盘用户来说,一个可聚焦的按钮比一段提示文字更容易操作。
容易出错的位置
第一处是链接的 href 属性。懒加载生成的条目,如果 href 是空的,或者用了 href="#" 配合 JavaScript 跳转,那么即使用户能看到这条内容,也无法复制链接或在新标签页打开。正确的做法是,每条数据都带有完整的详情页 URL,渲染时直接写入 href。
第二处是键盘操作。滚动触发懒加载对鼠标用户很自然,但键盘用户通过 Tab 键移动焦点时,焦点到达列表底部后,如果没有可聚焦的“加载更多”按钮,就无法继续浏览。所以,即使实现了自动滚动加载,也建议在列表底部保留一个按钮,让键盘用户和屏幕阅读器用户能主动请求下一页。
第三处是窄屏或低性能设备。在手机上看长列表,滚动事件触发频率很高,如果懒加载逻辑里做了复杂的 DOM 操作或图片解码,页面会明显卡顿。Intersection Observer 的回调是异步的,不会阻塞主线程,但渲染大量新条目本身仍然有成本。如果一次加载的条目过多,可以考虑分批渲染,或者先加载文字,图片用 loading="lazy" 属性让浏览器自行决定何时加载。
如何检查结果
完成修改后,不要只看自己电脑上的效果。用浏览器的隐身窗口打开列表页,查看源代码,确认首屏链接存在。然后禁用 JavaScript,刷新页面,确认分页链接仍然可用,列表内容虽然不会自动加载,但用户可以通过传统翻页浏览。再用键盘操作一遍:Tab 键能否到达“加载更多”按钮,按钮按下后能否正常加载下一页。最后,在开发者工具里把网络状态切换为离线,触发一次加载,确认错误提示和重试入口是否清晰。
如果列表页是服务端渲染的,还要检查服务器返回的状态码。正常列表页返回 200,如果某一页超出范围,应该返回 404 而不是 200 加空内容,否则搜索引擎会收录大量无意义的空页面。分页 URL 的规则也要统一,比如第一页是 /articles,第二页是 /articles?page=2,不要出现 /articles/page/1 和 /articles?page=1 同时存在的情况,这会分散页面权重。
懒加载本身不是问题,问题在于把懒加载当成了唯一的内容输出方式。公开内容保持服务端可访问,增强体验的部分交给 JavaScript,两者各司其职,列表页才能在用户体验和搜索引擎可见性之间取得平衡。
评论
登录后可发表评论。