Skip to content

第 14 章 渲染、移动端与页面体验:让内容确实可见、可用 ​

14.1 从“页面能打开”追问到“谁看见了什么” ​

假设你写了一个 JSON 格式化工具。自己打开时,编辑器、说明文档和示例一应俱全,SEO 检查工具却只提取到导航与页脚。这里最有价值的问题是:正文在什么时刻出现,依赖哪些请求,失败时剩下什么?本章沿着这些问题解释 JavaScript 渲染,再把渲染方案与移动端、性能、可访问性和安全连接起来。

学习目标是能画出一张页面加载依赖图,判断哪些内容应该放进初始 HTML,能设计有证据的渲染检查,并读懂核心网页指标而不迷信单次分数。对于会编程的读者,代码本身通常不难;难点是把搜索引擎、浏览器、网络和服务器的不同观察角度分开。

浏览器收到 HTML 后,会解析文档,获取样式、脚本和图片,并逐步形成页面。JavaScript 可以继续请求接口并改变文档对象模型 DOM。查看源代码通常接近服务器最初返回的文本;开发者工具的元素面板看到的则是某个时刻的 DOM。两者不相同并不一定有错,但决定了排查时不能只截图。

Google 可以执行 JavaScript,并在抓取、渲染和索引过程中处理内容;被禁止抓取的页面或资源无法按正常流程渲染。官方也提醒,初始 HTML 中已有 noindex 时,不能指望后续脚本可靠地把它移除来获得索引。[D07] 因而“爬虫支持 JavaScript”并不等于网站可以无视访问失败、脚本错误或错误状态。

14.2 CSR、SSR 与 SSG 分别把工作放在哪里 ​

客户端渲染 client-side rendering,简称 CSR,是让浏览器主要依靠 JavaScript 生成页面主体。它适合复杂交互,但正文可能需要等待 HTML、脚本下载、脚本执行、接口响应等多个步骤。任一步骤失败,都可能只留下加载动画。对于只在登录后使用的后台,公开搜索未必是目标;对于靠搜索获取新用户的工具说明页,这条依赖链就值得认真缩短。

服务端渲染 server-side rendering,简称 SSR,是请求到达服务器后,由服务器生成包含内容的 HTML。静态生成 static site generation,简称 SSG,则把生成过程提前到构建或发布阶段,访问时直接返回已经生成的页面。两者都能让关键文字更早出现在响应里,但成本位置不同:SSR 需要考虑请求时的计算、数据库与缓存;SSG 需要考虑构建时间、内容更新和失效机制。

选型最好按页面类型,而不是按全站口号。个人博客、更新不频繁的帮助文档适合评估静态生成;实时库存与价格页需要可靠的新鲜度策略;在线编辑器的工作区可以保留丰富客户端逻辑,而产品介绍、用法与例子采用预生成内容。一个站点完全可以同时使用多种模式,只要 URL、元数据与缓存规则一致。

还有按需重建、增量生成、局部渲染、流式输出等实现方式。名称随框架而变,判断标准可以保持稳定:首次响应是否含重要内容,错误时返回什么,什么时候更新数据,缓存多久,更新后谁负责失效,浏览器还要执行多少工作。不要仅因框架营销页写了“对 SEO 友好”,就跳过这些问题。

SSR 也可能慢。假设每次渲染都串行调用六个接口,其中一个推荐接口延迟很高,服务器便迟迟无法输出正文。将客户端等待搬到服务器,并没有消除等待。更合理的设计可能是让标题、主体说明和主要产品资料先可靠输出,把非关键推荐与登录状态延后处理。这里的“关键”来自用户任务,不等于所有可见元素都必须一起等待。

动态渲染 dynamic rendering 是根据爬虫等条件返回预渲染版本的变通方案。Google 当前文档把它定位为兼容问题的 workaround,并建议优先考虑更长期的渲染方案。[D35] 如果已有系统必须暂时保留它,至少要保证用户与爬虫看到的主要内容一致,并持续检查两套输出。维护两个容易漂移的版本,往往比选择一种稳定输出更费力。

14.3 水合与路由:静态外观之后还有一次接管 ​

水合 hydration 指客户端程序接管服务器已经生成的界面,使按钮、输入框等获得交互能力。以 React 为例,官方要求客户端初次生成结果应与服务器 HTML 一致,水合不匹配应作为缺陷处理。[D34] 对 SEO 而言,需要关注的不仅是日志中的警告,还包括接管后正文是否被替换为空、标题是否回退、canonical 是否被改成另一地址。

典型问题是服务器按公开默认状态渲染内容,客户端却先读取一个空的本地缓存,随后把整块页面清空,再等待接口。访客看到闪烁,爬虫观察到的文本也可能受时序影响。更稳妥的做法是把初始数据与 HTML 一起传递,让客户端从同一份状态开始;有个性化需求时,只更新相关组件,不要重建整篇公开正文。

单页应用 single-page application,简称 SPA,通常在客户端切换视图而不整页刷新。SPA 是交互方式,不代表所有地址都只能由一个空壳响应。每个需要搜索发现的视图都应有稳定、可直接打开的 URL。测试时必须新开窗口直达深层页面,再刷新,再后退前进,不能只从首页点过去一次就判定成功。

网站导航应使用带真实 href 的链接表达目的地,操作按钮则负责提交、展开或计算。下面是教学示范,未对真实应用运行:

html
<nav aria-label="工具导航">
  <a href="/tools/json-format">JSON 格式化</a>
  <a href="/guides/json-errors">JSON 错误排查</a>
</nav>
<button type="button" id="format">格式化输入</button>

链接可以被 JavaScript 增强以获得平滑切换,但关闭脚本或直接请求目标地址时,仍应有合理结果。用 div 加点击事件模拟导航,会让键盘、辅助技术和抓取行为都更难维护。只用 URL 片段表示完全不同内容的路由,也不适合作为公开搜索页面的基础。[D07]

14.4 为 Go 工具站设计一份渲染约定 ​

下面设计一个虚构工具页。它向陌生访客解释如何校验 JSON,并提供浏览器内操作。公开页面应有稳定标题、用途说明、一个输入示例、错误含义和相关帮助链接;用户粘贴的内容则不应成为公开 URL 的一部分。这样既能让页面独立说明价值,也避免为了索引临时结果而暴露输入数据。

将约定写成可以检查的条件:直接 GET /tools/json-validate 得到成功状态,初始 HTML 包含标题和说明;不存在的工具返回真实的不存在状态;脚本失败时说明仍可阅读,工具区域提示无法运行;客户端接管后标题与规范地址不变;登录状态只影响保存功能,不改变公开正文。

以下 Go 代码是简化教学片段,未编译、未运行,省略了完整路由、安全头、日志和生产错误处理。它展示的是“先构造正确响应,再发送”的思路,不是可直接上线的完整服务器:

代码块 · 50 行 · 点击收起 / 展开
go
package main

import (
    "bytes"
    "html/template"
    "net/http"
)

var toolPage = template.Must(template.New("tool").Parse(`
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{.Title}}</title>
  <link rel="canonical" href="{{.Canonical}}">
</head>
<body>
  <main>
    <h1>{{.Heading}}</h1>
    <p>{{.Description}}</p>
    <a href="/guides/json-errors">阅读 JSON 错误排查说明</a>
    <div id="editor">编辑器加载后可在浏览器中校验输入。</div>
  </main>
  <script defer src="/assets/editor.demo.js"></script>
</body>
</html>`))

func toolHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/tools/json-validate" {
        http.NotFound(w, r)
        return
    }
    data := struct {
        Title, Canonical, Heading, Description string
    }{
        "JSON 校验工具与错误解释",
        "https://example.com/tools/json-validate",
        "检查 JSON 格式错误",
        "了解缺少逗号、引号不配对等常见问题,并在本地校验输入。",
    }
    var out bytes.Buffer
    if err := toolPage.Execute(&out, data); err != nil {
        http.Error(w, "页面暂时不可用", http.StatusServiceUnavailable)
        return
    }
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.WriteHeader(http.StatusOK)
    _, _ = w.Write(out.Bytes())
}

先缓冲模板输出,是为了避免写出一半成功响应后才发现生成失败,导致状态与内容矛盾。正式项目还应处理请求方法、统一错误页、超时和缓存;代码中的示例域名必须从受控配置生成,不能直接信任用户提供的 Host 头来拼 canonical。

这个方案也有成本。静态说明与工具实现可能分别更新,若文档承诺了尚未上线的功能,虽然抓取正常,用户仍会失望。因此把工具能力、示例输入和说明文档纳入同一发布验收,比单纯检查 HTML 是否含关键词更重要。

14.5 怎样检查渲染,而不是猜测渲染 ​

先选一个失败页面和一个同模板正常页面,保存首次响应。把标题、主标题、正文中的一句独特文本、主要链接和结构化数据设为检查点,再比较浏览器渲染后是否仍然存在。文本存在于脚本变量中,并不代表已出现在可见内容里;截图有文字,也不代表链接具备正确目的地址。两类检查都要做。

随后检查网络与控制台。正文接口是否需要登录 Cookie,是否被跨域策略阻断,是否在慢网络下超时,是否因广告脚本异常而中断后续执行?不要看到第一条红色报错就认定它是根因。用依赖关系判断:若错误只影响统计请求,未必阻断正文;若错误发生在路由初始化之前,影响可能更广。

Google 的调试文档建议使用 URL 检查或富结果测试查看渲染后的 HTML、加载资源及错误信息,同时提醒客户端分析工具并不能完整代表 Googlebot 与渲染服务活动。[D08] 实际排查应把这些结果与服务器日志对照,明确哪个工具、什么时间、哪个版本看到了什么。一次测试没有失败,不足以排除间歇性的接口或缓存问题。

懒加载 lazy loading 是把暂时不需要的内容或资源延后加载。适合延后的是下方图片或非关键模块,不应把文章主体的出现绑定在用户点击、拖动或其他交互上。Google 的懒加载指南要求内容在进入视口等可处理条件下加载,并对无限滚动提供分页支持。[D27] 如果你的内容必须点“同意”“展开更多”后才发请求,至少要验证未交互的搜索测试能否取得需要索引的部分。

对虚拟列表更要小心。为了节省 DOM,界面可能只保留当前视口附近十条商品,滚动后移除前十条。用户体验可以很好,但不能据此假定爬虫知道所有商品。公开分类仍需要稳定的分页地址、真实链接或其他可靠发现入口。可访问的索引路径与交互列表可以并存,不必牺牲所有前端性能。

14.6 移动端优先,检查内容的一致性 ​

Google 的移动优先索引主要使用移动版内容进行索引与排名处理。对于同一内容的不同设备版本,应重点确保主内容、机器人指令、结构化数据等关键要素一致。[D09] 这不意味着桌面版不重要,而是不能只在大屏上保留真正有价值的说明,再把移动页削成购买按钮。

响应式设计通常让同一 URL 随屏幕宽度改变布局,维护成本相对容易控制。如果已经有独立移动域名或动态返回版本,应把对应关系和缓存条件写清。故障可能来自服务器把桌面 HTML 缓存给手机,或把移动版的 noindex 错误应用到全部设备。此时只调整 CSS 无法解决问题。

移动端验收应真实完成任务:能否找到规格、比较方案、关闭弹窗、填写表单、看到错误信息?把屏幕缩窄只是开始,还要检查触摸目标、软键盘遮挡、横向滚动和弱网络。如果一个本地服务页的电话按钮被固定广告盖住,即使正文可索引,搜索访客也无法联系商家。

折叠长说明可以改善移动阅读,但应保证内容确实可访问且语义合理。对用户来说,“点击展开已有内容”与“点击后才请求此前不存在的内容”并不明显;对技术排查而言,二者依赖不同。应通过 HTML 和网络检查确定实现,而不是仅凭外观判断。

14.7 核心网页指标:把分数变成可定位的问题 ​

截至本章核验日期,核心网页指标 Core Web Vitals 包括最大内容绘制 LCP、交互到下一次绘制 INP、累积布局偏移 CLS。良好目标分别为 LCP 不超过 2.5 秒、INP 不超过 200 毫秒、CLS 不超过 0.1,通常按移动与桌面分别观察第七十五百分位。[D10] 百分位的含义是关注较慢的一部分体验,不能只用平均值把差体验掩盖掉。

实验室数据是在规定环境下重复加载或执行任务,便于诊断;真实用户数据反映真实设备、网络和使用过程。二者回答的问题不同。一次 Lighthouse 高分不能代表所有访客都快;真实数据缺失也不能写成“指标全部通过”,可能只是样本不足。还应确认工具展示的是当前 URL 还是整个来源域的数据。

第一次实际检查时,可以在 PageSpeed Insights 输入一个公开 URL,分别查看手机与桌面结果。先记录真实用户区展示的是该 URL 还是整个来源域,再查看下方实验室诊断。真实用户区采用 CrUX 过去 28 天的数据;新页面可能只有来源域数据,也可能完全缺少样本。因此,今天修好问题,不意味着这段历史窗口马上变成全绿。[D49] 保存测试时间、页面版本和测试环境,用当前实验室结果定位修改,再用后续真实用户数据确认影响。

Google 对页面体验的说明明确反对把单个报告得分当成排名保证。[D12] 所以优化可以用“减少用户等待、降低任务失败、避免内容跳动”解释价值,但不能承诺“提升十分就上涨三名”。搜索相关性、内容质量和竞争变化不会因为性能工作而消失。

LCP:先找最大内容何时才开始加载 ​

LCP 问题可以沿着 HTML 响应、资源发现、资源传输和最终绘制来拆分。web.dev 的优化指南强调应观察完整链路,而非只压缩图片。[D31] 例如一张首屏示意图只有很小体积,但它必须等大型脚本下载并请求接口后才插入页面,继续压缩几千字节通常解决不了主要等待。

实际操作时,先定位本次测试的 LCP 元素,再看它在网络瀑布中何时被请求。如果 HTML 很晚到达,查服务器和重定向;如果 HTML 已到但资源迟迟未发现,查脚本依赖和懒加载;如果传输慢,查尺寸、格式与连接;如果已下载却晚显示,查样式、字体、隐藏逻辑和主线程任务。每次只改变一个明确瓶颈,留下前后测量条件。

首屏主图不宜机械设置为懒加载。给每张图片都设置最高优先级也会失去区分,多个资源仍然争用网络。更好的做法是识别真正关键的一两项,并在测试中确认浏览器采用了预期加载顺序。没有测量依据时,不要把所有 preload 建议一次性加入模板。

INP:页面显示出来以后是否响应 ​

INP 关注交互响应。性能问题可能发生在点击前主线程已被占满,也可能发生在事件处理或后续绘制阶段。web.dev 的指南以这些阶段拆解响应延迟。[D32] 因此,一个纯加载测试不能替代真实地点击菜单、切换筛选器和输入长文本。

以 JSON 工具为例,输入很长时同步解析、格式化、语法高亮和全量 DOM 更新一起执行,按钮可能长时间无响应。可考虑先给出明确忙碌反馈,再将独立计算放到适合的后台工作方式中,控制高亮规模并减少重复计算。但后台工作线程并不解决所有问题:最终插入巨大 DOM 仍有成本,序列化和传输数据也需要时间。

给性能工单写“优化 JS”很难验收。更可操作的表述是:在约定设备与输入规模下,点击格式化后界面应及时反馈,主线程长任务的来源能够解释,并在真实用户数据中观察对应交互。输入规模是业务边界;只有十个字符的演示无法证明工具能处理实际大文件。

CLS:不要让用户正在看的位置突然移动 ​

CLS 衡量非预期布局移动,常见问题包括图片未预留空间、广告晚插入、字体替换和动态组件改变高度。优化指南强调关注加载后也可能发生的偏移。[D33] 因此,首页刚出现时看起来稳定,不足以证明滚动、登录提示和异步推荐出现后仍稳定。

实践中可为图片设定合适尺寸或比例,为必要占位模块预留稳定空间,并避免把通知突然插入当前阅读位置上方。留白本身也有体验成本,不能为了指标在手机首屏永久放一大块空框。应在合理占位、内容密度与广告商业需求之间做明确取舍,而不是让工程师单独追分。

14.8 可访问性与安全怎样影响技术方案 ​

可访问性是网站设计与实现的整体责任。[D11] 第 10.7 节已介绍键盘、标签、焦点和错误提示的基本检查;这里把它们放进渲染回归测试:首次加载能操作,水合后是否仍能操作?筛选结果异步更新后,焦点是否丢失?错误模块被替换后,用户能否找到新提示?这些问题需要在真实交互之后再检查一次,不能只检查服务器输出的静态 HTML。

安全方面,HTTPS、依赖更新、输入处理、权限边界与备份都与网站可靠性有关。公开搜索页面不应把管理员接口或用户数据一起暴露。内容安全策略 Content Security Policy,简称 CSP,可限制允许加载与执行的内容来源,但需要结合实际资源依赖逐步设计;MDN 的指南说明了其配置与防护用途。[D36] 如果上线一条过严策略导致正文脚本被阻断,应该修正策略与资源加载方式,而不是长期关闭安全控制。

被入侵的网站还可能出现隐藏垃圾页面、恶意重定向和错误的索引内容。Google 提供防止站点被滥用的安全建议。[D29] 技术 SEO 在此处应与安全响应合作:保留日志、定位入侵入口、清理恶意内容、修补漏洞、检查后门,再处理搜索表现。只删搜索结果中的几个 URL,不代表服务器已经恢复安全。

14.9 从上线检查到持续监测 ​

渲染与体验问题常在发布后重新出现,因此要把检查放进发布流程。为首页、工具、文章、分类、商品、错误页各选代表样本,保存初始 HTML 的关键字段,并用浏览器测试验证交互后的字段没有被意外覆盖。新增第三方组件、调整缓存或换渲染框架时,这些样本尤其重要。

性能预算应围绕真实任务定义。可记录首屏关键资源大小、脚本执行开销、主要交互时间与模板级真实用户指标,但不要把某个统一字节上限变成所有页面的真理。文章页与大型编辑器承担不同工作,应该分别设定目标,并解释超出预算的功能价值。

练习与参考解答 ​

练习一:工具页源代码含完整说明,水合后说明消失三秒再出现。算不算已经完成 SSR?参考解答:实现了部分服务端输出,但交互接管破坏了稳定性。应检查初始状态传递与客户端空状态覆盖,验收渲染前后的内容一致性。

练习二:首屏图片从三百千字节压到一百千字节,LCP 几乎不变。下一步是什么?参考解答:查看请求开始时间和最终绘制时间,定位是否被接口发现链、CSS、字体或主线程阻塞限制;不能继续盲目降低画质。

练习三:手机页只保留一百字简介,完整教程只在桌面展示。你会先加 schema 还是修内容一致性?参考解答:先让移动访问取得重要正文及相应元数据。结构化数据无法补回实际缺失的页面内容。

执行清单 ​

  • 记录每类页面从请求到正文出现的依赖与失败表现。
  • 对公开重要内容设计初始 HTML、元数据及路由约定。
  • 分别验证直接访问、客户端切换、水合完成和接口失败。
  • 用真实移动任务与实验室诊断共同检查体验,保留样本口径。
  • 将可访问性、安全、内容一致性与性能一起验收,避免互相牺牲。

本章参考来源 ​

[D07] Understand JavaScript SEO basics | https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics | 访问 2026-10-13 | 渲染流程、路由与初始索引标记。 [D08] Fix Search-related JavaScript problems | https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript | 访问 2026-10-13 | 渲染测试与观察限制。 [D09] Mobile-first indexing best practices | https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing | 访问 2026-10-13 | 移动版主要内容与关键配置一致性。 [D10] Web Vitals | https://web.dev/articles/vitals | 访问 2026-10-13 | 当前核心指标与阈值。 [D11] Introduction to Web Accessibility | https://www.w3.org/WAI/fundamentals/accessibility-intro/ | 访问 2026-10-13 | 可访问性目标与适用人群。 [D12] Understanding page experience in Google Search | https://developers.google.com/search/docs/appearance/page-experience | 访问 2026-10-13 | 页面体验与排名保证的边界。 [D27] Fix lazy-loaded website content | https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading | 访问 2026-10-13 | 懒加载与内容发现。 [D29] Prevent abuse on your site | https://developers.google.com/search/docs/monitor-debug/security | 访问 2026-10-13 | 网站安全与滥用防护。 [D31] Optimize Largest Contentful Paint | https://web.dev/articles/optimize-lcp | 访问 2026-10-13 | 加载阶段拆分与关键资源发现。 [D32] Optimize Interaction to Next Paint | https://web.dev/articles/optimize-inp | 访问 2026-10-13 | 交互延迟分解。 [D33] Optimize Cumulative Layout Shift | https://web.dev/articles/optimize-cls | 访问 2026-10-13 | 布局偏移原因与加载后检查。 [D34] hydrateRoot | https://react.dev/reference/react-dom/client/hydrateRoot | 访问 2026-10-13 | React 水合一致性约束。 [D35] Dynamic rendering as a workaround | https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering | 访问 2026-10-13 | 动态渲染的定位。 [D36] Content Security Policy | https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP | 访问 2026-10-13 | CSP 实现与安全边界。 [D49] About PageSpeed Insights | https://developers.google.com/speed/docs/insights/v5/about | 访问 2026-10-13 | 实验室与CrUX真实数据、过去28天窗口、URL及来源域粒度。

保留各书来源与许可 · 示例运行条件以正文为准