在某个时候,我们忘记了如何编写 HTML——或者说,忘记了为什么它如此重要。
现代开发工作流程优先考虑组件、工具类和 JavaScript 重度渲染。HTML 变成了一个副产品,而不是基础。
这种转变是有代价的——在性能、可访问性、弹性以及机器(和人)如何解释你的内容方面。
我在其他地方写过关于 JavaScript 如何杀死 Web 的文章。但这个故事中最容易修复、最被忽视的部分是语义化 HTML。
这篇文章讲述的是我们失去了什么——以及为什么它仍然重要。
语义化 HTML 是机器理解含义的方式
HTML 不仅仅是我们在页面上放置元素的方式。它是一种语言——拥有表达含义的词汇表。
像 <article>、<nav> 和 <section> 这样的标签不是装饰性的。它们表达意图。它们传达层次结构。它们告诉机器你的内容是什么,以及它如何与其他所有内容相关联。
搜索引擎、可访问性工具、AI 代理和基于任务的系统都依赖于结构信号——有时是显式的,有时是启发式的。并非每个系统都需要完美的标记,但当它们能够利用时,语义化 HTML 可以为它们提供清晰度。在一个充满结构模糊页面的 Web 中,这种清晰度可能是竞争优势。
语义化标记不能保证更好的索引或提取——但它创建了系统现在和将来可以使用的基础。这是质量、结构和意图的信号。
如果一切都是 <div> 或 <span>,那么就没有什么是有意义的。
这不仅仅是糟糕的 HTML——而是无意义的标记
很容易将此视为纯粹性问题。只要看起来正确,谁在乎你使用 <div> 还是 <section> 呢?
但这不是关于学究气。无意义的标记不仅让你的网站更难阅读——它还让网站更难渲染、更难维护、更难扩展。
这种抽象导致的标记通常看起来像这样:
<div class="tw-bg-white tw-p-4 tw-shadow tw-rounded-md">
<div class="tw-flex tw-flex-col tw-gap-2">
<div class="tw-text-sm tw-font-semibold tw-uppercase tw-text-gray-500">ACME Widget</div>
<div class="tw-text-xl tw-font-bold tw-text-blue-900">Blue Widget</div>
<div class="tw-text-md tw-text-gray-700">Our best-selling widget for 2025. Lightweight, fast, and dependable.</div>
<div class="tw-mt-4 tw-flex tw-items-center tw-justify-between">
<div class="tw-text-lg tw-font-bold">$49.99</div>
<button class="tw-bg-blue-600 tw-text-white tw-px-4 tw-py-2 tw-rounded hover:tw-bg-blue-700">Buy now</button>
</div>
</div>
</div> 当然,这是有效的。它有样式。它能渲染。但它在语义上是死的。
它无法让你感知这个内容是什么。这是产品列表?博客文章?还是行动号召?
你无法一眼看出——屏幕阅读器、爬虫或试图提取定价数据的代理也无法看出。
这是具有有意义结构的相同内容:
<article class="product-card">
<header>
<p class="product-brand">ACME Widget</p>
<h1 class="product-name">Blue Widget</h1>
</header>
<p class="product-description">Our best-selling widget for 2025. Lightweight, fast, and dependable.</p>
<footer class="product-footer">
<span class="product-price">$49.99</span>
<button class="buy-button">Buy now</button>
</footer>
</article>现在它讲述了一个故事。有结构。有意图。你可以在 CSS 中定位它。你可以在爬虫中提取它。你可以在屏幕阅读器中导航它。它有意义。
语义化 HTML 是可访问性的基础。没有结构和意义,辅助技术无法解析你的内容。屏幕阅读器不知道要宣布什么。键盘用户会卡住。语音界面无法找到你埋在 div 中的内容。干净、有意义的 HTML 不仅仅是良好实践——它是人们访问 Web 的方式。
这并不是说框架本质上是坏的或不可访问的。Tailwind、原子类和内联样式绝对可以有用——特别是在复杂项目或大型团队中,一致性和速度很重要。它们可以减少认知开销。它们可以提高速度。
但它们是工具,不是答案。当每个组件都退化为几乎重复的工具类的汤——为每个布局和断点进行调整——你就失去了重点。结构消失了。目的被模糊了。
这不是关于抽象。这是关于你在过程中失去的东西。
这种损失不仅伤害语义——它还伤害性能。事实上,这是现代 Web 感觉更慢、更重、更脆弱的最大原因之一。
语义腐烂破坏性能
我们已经将 HTML 仅仅是渲染目标的想法正常化了——我们可以向浏览器抛出任意标记,并相信它会解决问题。确实如此。浏览器在修复我们的混乱方面非常出色。
但这种宽容是有代价的。
渲染引擎被设计为容错的。它们会推断角色,修补糟糕的结构,并尝试按你的意图渲染内容。但每次它们必须这样做——每次它们必须猜测你的 <div> 汤试图成为什么——都会消耗时间。这是 CPU 周期。这是 GPU 时间。这是功耗,特别是在移动设备上。
让我们分解膨胀在哪里以及如何打击最严重——以及为什么这很重要。
大型 DOM 渲染缓慢
DOM 中的每个节点都会增加开销。在渲染过程中,浏览器遍历 DOM 树,构建 CSSOM ,计算样式,解析布局,并绘制像素。更多的节点意味着每个阶段都有更多的工作。
这不仅仅是关于下载大小(尽管这也很重要——更多的标记意味着更多的字节,可能压缩效率更低)。这是关于渲染性能。臃肿的 DOM 意味着更长的布局和绘制阶段,更多的内存使用,以及更高的能耗。
即使是简单的交互——比如打开模态框或展开列表——也可能触发在你臃肿的 DOM 中爬行的重排。突然间,你的”简单”页面开始滞后、卡顿或抖动。
你可以在 Chrome DevTools 中看到这一点。打开性能选项卡,记录跟踪,观察每次布局引擎空转时火焰图的亮起。
有趣的事实:解析不是瓶颈——像 Chromium 这样的浏览器可以在现代 CPU 上以每秒数十 GB 的速度处理 HTML。真正的成本来自 CSSOM 构建、布局、绘制和合成期间。此外,HTML 解析只有在遇到非延迟的
<script>或阻塞渲染的样式表时才会阻塞——这再次强调了为什么干净的标记仍然重要,但你也需要智能的加载顺序。
复杂树结构导致布局抖动
但这不仅仅是关于你有多少标记——而是关于它是如何结构化的。深度嵌套、包装器膨胀和过度抽象的组件创建了难以理解且渲染成本高昂的 DOM 树。浏览器必须更努力地弄清楚哪些变化影响什么——这就是事情开始崩溃的地方。
切换一个类,你可能会使整个视口的布局失效。这种变化通过父子链级联,触发布局偏移和视觉不稳定。组件意外地重新定位自己。滚动锚定失败,用户在交互过程中失去位置。整个体验变得不稳定。
因为这一切都是实时发生的——在每次交互中——它会影响你的帧预算。目标是 60fps?这给你每帧只有约 16ms。超出这个预算,用户会立即感受到延迟。
你会在 Chrome 的 DevTools 中看到这一点——在布局偏移区域或帧图表中,错过的帧堆积起来。
当你改变 DOM 时,浏览器并不总是重新布局整个树——有增量布局处理。但深度嵌套或模糊的标记仍然会触发昂贵的祖先检查。像 Facebook 的 “Spineless Traversal” 这样的项目表明,当需要检查许多节点时,浏览器仍然会付出性能代价。
冗余的 CSS 增加重计算成本
臃肿的 DOM 已经够糟糕了——但臃肿的样式表让情况变得更糟。
现代 CSS 工作流程——特别是在组件化系统中——经常导致重复。每个组件都声明自己的样式——即使它们重复。没有级联。没有共享上下文。特异性变成一团糟,覆盖成为默认行为。
例如,这通常看起来像这样:
/* button.css */
.btn {
background-color: #006;
color: #fff;
font-weight: bold;
}
/* header.css */
.header .btn {
background-color: #005;
}
/* card.css */
.card .btn {
background-color: #004;
}每个文件都重新定义相同的东西。浏览器必须解析、应用并协调所有这些。将此乘以数百个组件,你的 CSSOM——浏览器所有 CSS 规则的内部模型——就会膨胀。
每次有变化时(比如类切换),浏览器必须重新评估哪些规则适用于哪里。更多规则,更多重计算。在低端设备上,这成为瓶颈。
是的,像 Tailwind 这样的原子 CSS 系统可以减少文件大小并增加重用。但只有在有意使用时才行。当每个组件都被包装在十几层实用类中,每个实用类都被稍微调整(这里的边距,那里的字体),你最终会得到数千种独特的组合——其中许多几乎相同。
成本不仅仅是大小。而是流失。
浏览器匹配选择器的方式:浏览器从右到左匹配选择器(例如,对于
div.card p span,它们检查 span → p → div.card 等)。这对于清晰、具体的选择器是高效的——但臃肿的深层树或通用级联规则会强制进行大量的过度扫描。
自动生成的类名破坏缓存和定位
看到像 .sc-a12bc、.jsx-392hf 或 .tw-abc123 这样的类名已经变得很常见。这些通常是 CSS-in-JS 系统、作用域样式或构建时哈希的结果。意图很明确:本地化样式以避免全局冲突。这不是一个坏主意。
但这种方法带来了不同类型的脆弱性。
如果你的类是短暂的——如果它们在每次构建时都会改变——那么:
你的分析标签会失效。
你的端到端测试需要持续维护。
你的缓存策略会崩溃。
你的标记差异变得不可读。
你的 CSS 默认变得不可重用。
从性能角度来看,最后一点是关键的。缓存只有在事物可预测时才有效。浏览器缓存和重用解析样式表的能力取决于一致的选择器。如果每个组件、每次构建、每次部署都改变其类名,浏览器必须重新解析和重新应用一切。
更糟糕的是,它迫使工具依赖脆弱的变通方法。想通过标签管理器定位结账漏斗中的按钮?如果它被包装在三层哈希组件中,祝你好运。
这不是假设的。这是现代前端技术栈中的常见痛点,它会使一切膨胀——代码、工具、渲染路径。
可预测的语义化类名不仅让你的生活更轻松。它们让 Web 更快。
语义标签可以提供布局提示
语义化 HTML 不仅仅是关于意义或可访问性。它是脚手架。结构。这种结构为你和浏览器提供了可以使用的东西。
像 <main>、<nav>、<aside> 和 <footer> 这样的标签不仅仅是语义化的——它们默认是块级的,并且自然地分割页面。这种分割通常与浏览器处理和绘制内容的方式一致。它们不保证性能提升,但它们为性能提升创造了条件。
当你的布局有明确的边界时,浏览器可以更有效地确定其工作范围。它可以隔离样式重计算,避免不必要的重排,并更好地管理滚动容器和粘性元素等内容。
更重要的是:在绘制和合成阶段,浏览器可以将渲染工作分布到多个线程中。GPU 合成管道受益于结构良好的 DOM 区域——特别是当它们与 contain: paint 或 will-change: transform 等属性配对时。通过创建隔离层,你减少了重新栅格化页面大部分内容的开销。
如果一切都是嵌套 <div> 的巨大堆栈,就没有这种隔离的明确机会。每次交互、动画或调整大小事件都有触发影响整个树的重排或重绘的风险。你不仅让自己的工作变得更困难——你还在给渲染引擎制造瓶颈。
简单地说:语义标签帮助你与浏览器合作,而不是对抗它。它们不是魔法,但它们让魔法成为可能。
动画和合成灾难
动画是结构良好的 HTML 要么闪耀……要么灾难性失败的地方。
现代浏览器旨在将动画工作卸载到 GPU。这就是在 60fps 或更高帧率下实现丝滑过渡的原因。但要实现这一点,浏览器需要将动画元素隔离到自己的合成层上。只有某些 CSS 属性符合这种 GPU 加速处理的条件——最显著的是 transform 和 opacity。
如果你动画化像 top、left、width 或 margin 这样的属性,你就在触发布局引擎。这意味着要为变化下游的所有内容重新计算布局。这是主线程工作,而且很昂贵。
在简单页面上?也许你能侥幸过关。
在具有数十个兄弟元素和依赖关系的深度嵌套组件上?每个动画都变成布局抖动。一旦你的动画帧预算超过 16ms(60fps 的限制),事情就会变得卡顿。动画结巴。交互延迟。滚动变得缓慢。
你可以在 DevTools 的性能面板中看到这一点——布局重计算、样式失效和绘制操作点亮了火焰图。
语义化 HTML 在这里也有帮助。适当的结构边界允许更有效地使用现代 CSS 包含策略:
contain: layout; 告诉浏览器它不需要重新计算元素外部的布局。
will-change: transform; 提示需要合成层。
isolation: isolate; 和 contain: paint; 可以帮助防止视觉溢出并强制 GPU 层。
但这些工具只有在你的 DOM 是合理的时候才有效。如果你的动画组件嵌套在不可预测的通用 <div> 堆中,浏览器无法干净地隔离它。它不知道什么可能受到影响——所以它保守行事并重新计算一切。
这不是浏览器缺陷。这是开发者失误。
动画不仅仅是关于什么移动。它是关于什么不应该移动。
渲染优化:渲染和绘制在现代引擎中是并行操作。但 DOM/CSS 变化经常强制主线程同步,破坏了这种优势。
通过
will-change: transform或更新的layer()语法进行 CSS 分层告诉 GPU 单独处理合成。这避免了主线程中的布局和绘制——但只有当 DOM 结构允许不同的分层容器时才行。
CSS 包含和可见性:强大但脆弱
现代 CSS 为我们提供了管理性能的强大工具——但只有当你的 HTML 给它们喘息空间时,它们才有效。
以 contain 为例。你可以使用 contain: layout、paint 甚至 size 来告诉浏览器”不要看这个盒子外面——这里面的任何东西都不会影响页面的其余部分。“这可以大幅减少布局重计算的成本,特别是在动态界面中。
但这只有在你的标记有明确的结构边界时才有效。
如果你的内容纠缠在非语义包装器的嵌套中,或者如果容器继承了意外的样式或依赖关系,那么包含就变得不可靠。你无法安全地包含你无法隔离的东西。浏览器不会冒这个风险。
同样,content-visibility: auto 是现代 CSS 武器库中最被低估的工具之一。它让浏览器跳过渲染屏幕上不可见的元素——有效地”虚拟化”它们。这对于长页面、信息流或无限滚动组件来说是巨大的。
但它有注意事项。它需要可预测的布局、滚动锚定和结构一致性。如果你的 DOM 很混乱,或者你的组件在树上下泄漏样式和依赖关系,它会适得其反——引入布局跳跃、渲染错误或损坏的焦点状态。
这些不是魔法子弹。它们是性能契约。混乱的标记会破坏这些契约。
语义化 HTML——以及干净、结构良好的 DOM——是使这些工具首先可行的原因。
CSS 包含性能:MDN 的文档强调了
contain: content(layout+paint+style的简写)如何让浏览器独立优化整个子树。现实世界的 A/B 测试显示,在使用content-visibility: auto的电商页面上,INP 延迟有所改善。
代理是新用户——它们关心结构
Web 不再只是为人类服务。
搜索引擎是第一波——解析内容、提取含义,并基于结构和语义进行排名。但现在我们正在进入 AI 代理、助手、爬虫、任务运行器和 LLM 支持的自动化时代。这些系统不浏览你的网站。它们不滚动。它们不点击。它们解析。
它们查看你的标记并询问:
这是什么?
它是如何结构化的?
什么是重要的?
它与其他一切有什么关系?
干净的语义化 DOM 清楚地回答了这些问题。<div> 汤则不能。
当这些代理必须在十个都声称销售相同小部件的网站中选择时,更容易解释、提取和总结的那个将获胜。
这不是假设的。Google 的购物系统、像 Perplexity 这样的总结代理、像 Arc 这样的 AI 浏览器,以及可访问性的辅助工具都是这种转变正在进行的例子。你的网站不再只是视觉体验——它是一个界面。一个 API 。一个数据集。
如果你的标记无法支持这一点?你就被排除在对话之外了。
是的——智能系统可以并且确实在必要时推断结构。但这是额外的工作。这是不精确的。这是风险。
在竞争激烈的环境中,结构良好的标记不仅仅是优化——它是差异化因素。
结构就是韧性
语义化 HTML 不仅仅是帮助机器理解你的内容。它是构建在压力下保持完整的界面。
干净的标记更容易调试。更容易适应。更容易渐进增强。如果你的 JavaScript 失败,或者样式表没有加载,或者布局在边缘情况的屏幕上崩溃——语义化 HTML 意味着那里仍然有可用的东西。
这不仅仅是良好实践。这是为现实世界构建软件的方式。
因为真实用户有不稳定的连接。真实设备有有限的电力。真实会话包括你没有测试过的边缘情况。
语义化标记给你一个基线。一个后备方案。一个基础。
结构不是可选的
如果你想为性能、可访问性、可发现性或韧性而构建——如果你想让你的网站快速、可理解且适应性强——从有意义的 HTML 开始。
不要把标记当作事后想法。不要让你的工具埋没结构。不要构建只有在星星对齐且 JavaScript 加载时才工作的界面。
语义化 HTML 是基础。它快速。它健壮。它自描述。它面向未来。
它不会阻止你使用 Tailwind 。它不会阻止你使用 React 。但它确实要求你深思熟虑。有意图地设计你的结构。编写讲述故事的代码——不仅仅对人类,也对浏览器、机器人和代理。
这不是怀旧。这是基础设施。
如果 Web 要在下一波复杂性、自动化和期望中生存——我们需要记住如何正确构建它。
这从记住如何编写 HTML 开始——以及为什么我们要这样编写。不是作为 JavaScript 的副产品,或工具的输出,而是作为随后一切的基础。
