预渲染输出的HTML必须通过HTMLHint静态检查,关键规则包括doctype-first、meta-charset-require、tagname-lowercase、attr-lowercase和attr-quote-style,确保语义标签原生存在、script标签分层处理,并隔离window/navigator等API依赖。

预渲染输出的 HTML 必须通过 HTMLHint 静态检查
预渲染生成的静态 HTML 文件不是“一次构建就完事”,它们脱离了运行时框架约束,容易暴露原始 HTML 的结构性缺陷:比如缺失 <!DOCTYPE html>、<meta charset="UTF-8"> 不在前 1024 字节、<html lang=""> 值为空或不匹配实际语言。这些错误在 CSR(客户端渲染)下可能被 Vue/React 自动兜底,但在预渲染产物里会直接生效,影响 SEO、屏幕阅读器解析甚至 IE/旧 Safari 渲染模式。
必须把 htmlhint 加入构建后验证环节,而不是仅在开发阶段跑。例如在 prerender-spa-plugin 输出到 dist/ 后,用脚本遍历所有 .html 文件执行:
htmlhint dist/*.html --config .htmlhintrc
关键规则要显式启用:doctype-first、meta-charset-require、tagname-lowercase、attr-lowercase、attr-quote-style。特别注意:attr-value-double-quotes 能捕获像 class=header 这类无引号属性——Puppeteer 渲染时虽能容错,但 W3C 校验失败且部分辅助技术会跳过解析。
语义化标签结构不能依赖 JS 水合补全
预渲染输出的是最终 DOM 快照,<main>、<nav>、<header> 等语义标签必须在 HTML 字符串里原生存在,不能靠 Vue mounted() 或 React useEffect 动态插入。否则会出现:
立即学习“前端免费学习笔记(深入)”;
- SEO 抓取器看到空
<main></main>或纯<div class="main">,无法识别主体内容区域 - NVDA/VoiceOver 在页面加载瞬间只读到
<div>层级,跳过逻辑区块导航 -
document.querySelector('main')在 hydration 前就为null,导致水合逻辑报错
确保模板中直接写语义标签,例如:
<main>
<article>
<h1>标题</h1>
<p>正文</p>
</article>
</main>
不要写成:<div id="app"></div> 然后靠 JS 插入 <main> —— 预渲染时它就是空的。
避免预渲染产物中出现 window.location 或 navigator API
react-snap 和 prerender-spa-plugin 都基于 Puppeteer,在无头浏览器中执行 JS 并截图。但 Puppeteer 的 window 对象不支持某些浏览器专属 API,调用即崩溃或返回空值,典型如:
-
window.location.href→ 返回about:blank,导致路由判断失效 -
navigator.geolocation→ 直接抛TypeError: Cannot read property 'geolocation' of undefined -
window.matchMedia→ 返回空MediaQueryList,响应式逻辑错乱
解决方式不是 try/catch,而是从源头隔离:
- 把依赖这些 API 的逻辑移到
mounted/useEffect中,并加if (typeof window !== 'undefined')判断 - 预渲染期间用占位数据模拟:比如用
process.env.NODE_ENV === 'production' && !window.__PRERENDER_INJECTED区分环境 - 在
vue.config.js或craco.config.js中配置插件忽略含敏感 API 的 chunk,防止其注入到预渲染上下文
script 标签保留策略直接影响 hydration 行为
预渲染 HTML 中的 <script> 标签不能全删,也不能全留。删光会导致 CSR 无法接管;全留则第三方统计脚本重复执行(如 GA4、百度统计),且可能因执行顺序错乱破坏水合状态。
正确做法是分层处理:
- 必须保留:入口 JS(如
main.js)、框架 runtime(vue.runtime.esm-bundler.js)、hydration 所需的初始化脚本 - 可删除:埋点脚本(
gtag.js、_bdhm.js)、A/B 测试 SDK、非关键广告代码 - 应 defer:字体加载、非阻塞分析脚本,避免阻塞 DOM 解析
注意:<script src="xxx.js" type="module"> 在预渲染时不会执行,但若该模块内有副作用(如修改全局变量),仍可能干扰 hydration —— 建议这类模块仅在客户端动态 import()。



















