html:not([lang]) 是最准最快的缺失 lang 属性定位方式,需用 document.queryselectorall 检查、验证 document.documentelement.lang 值、w3c 校验时带 doctype、排查构建压缩误删、服务端输出正确 lang 或 spa 中配合 title 重读触发。

用控制台一行CSS选择器直接高亮缺失元素
html:not([lang]) 是最准、最快的定位方式。打开开发者工具,切到 Console 面板,粘贴执行这行代码:document.querySelectorAll('html:not([lang])'),返回非空 NodeList 就说明问题存在。
注意别用 $$('html:not([lang])')——某些浏览器(如旧版 Safari)不支持该简写,且它不返回实时 DOM 节点,容易漏判动态渲染页。实际排查时,还要顺手检查 document.documentElement.lang 的值是否为空字符串或 undefined,因为 和没写等效,都算缺失。
W3C Validator 里重点盯 Warning 级提示
“No language specified” 这类 Warning 不阻断渲染,但它是 axe、Lighthouse、axe-core 等工具标为“严重”的第一项。直接去 W3C Markup Validation Service 的 “Validate by Direct Input” 栏粘贴完整 HTML 源码(含 和 <code> 标签),避免因解析不全导致误报。
- 必须带
,否则校验器会降级为 quirks mode,lang 检查逻辑失效 - 别用 URL 验证 localhost 页面,代理或重定向可能返回 404 或中间页,结果不可信
- 看到 “Element html is missing required attribute lang” 提示,就不用再往下扫了——这是根因,优先修复
检查构建压缩流程是否误删 lang 属性
Vite 默认用 html-minifier-terser 压缩 index.html,而它的 removeRedundantAttributes 选项会把 中的 lang 当作冗余属性删掉。Webpack 的 html-webpack-plugin 同理。
验证方法很简单:在构建产物 dist/index.html 里搜索 ,看是否存在 <code>lang=。如果没了,就是压缩器干的。解决方案是显式关闭危险选项:
minify: {
removeRedundantAttributes: false,
removeScriptTypeAttributes: false,
collapseWhitespace: true
}
另外确认原始 index.html 中 <meta charset="UTF-8"> 确实在 开头位置——若被 JS 插入或位置靠后,压缩器可能判定其“无效”并一并清除,连带影响 lang 的上下文可靠性。
动态语言切换时 document.documentElement.lang 修改无效
JS 执行 document.documentElement.lang = "en" 后,VoiceOver 或 NVDA 很可能仍在用旧语言朗读,因为辅助技术在 DOM 初始解析阶段就读取了 lang 值,后期修改不触发重读。
真正有效的做法只有两个:
- 服务端渲染时就输出正确值:Next.js 在
app/layout.tsx里用;PHP 模板用 - SPA 必须配合强制重读信号:改完 lang 后立即执行
document.title = document.title(空赋值),部分读屏软件会将其识别为根节点变更
最容易被忽略的是:第三方脚本(比如广告 SDK、埋点 JS)动态插入的 <div> 或 <code><p></p>,它们继承的是初始 lang,不会自动同步切换后的值。局部语言标注得靠语义化标签显式声明,比如 <blockquote lang="ja"></blockquote>,而不是指望全局变量。











