文本高亮还在改 DOM?Custom Highlight API 把 Range 和样式彻底拆开了

你做搜索高亮、代码编辑器、协作文档批注时,是不是还在用 innerHTML.replace() 往正文里塞高亮标签?这套写法改了 DOM、打断事件监听、跨标签边界就炸,每次清高亮还要小心复原。2026 年了,CSS Custom Highlight API 把这件事从 JS 层还给了浏览器:只改 Range,不动 DOM,样式归 CSS 管。

问题:为什么文本高亮一直是个脏活

过去给一段文字加高亮,本质上只有一条路:把原始文本当字符串处理,匹配命中后用 innerHTML 重写,再塞一层 <span class="highlight">。这带来三个实际问题。

第一,DOM 被污染。高亮层的 wrapper 元素和原始文本节点混在一起,后续任何基于 DOM 的测量、选择、序列化都要绕过这些额外节点。框架场景下更麻烦:React/Vue 的虚拟 DOM Diff 会把这些高亮 span 当成需要保留的状态,清高亮时经常出现 wrapper 残留或 innerHTML 被框架覆盖的问题。

第二,跨元素边界不可用。真实文本很少刚好完整落在一个元素内。一个搜索词可能横跨两个 <p>,或者夹在一个 <strong> 中间。innerHTML.replace() 处理不了这种跨边界场景,surroundContents() 遇到跨 tag 边界直接抛异常。于是很多实现会先拆文本节点、再拼接 wrapper,复杂度翻倍。

第三,样式和结构耦合。高亮逻辑既要算文本偏移量,又要生成 HTML,还要管清掉时的清理顺序。团队里常见的做法是写一个 80 行的 highlight() 工具函数,还要附带一个 unhighlight(),任何接入搜索或批注的人都要先理解这套副作用。

方案:Custom Highlight API 的三步走

CSS Custom Highlight API 把高亮拆成了两层职责:JavaScript 只负责定义”哪段文本被高亮”,CSS 只负责定义”高亮长什么样”。

核心流程是三步:

  1. Range 定义文本区间;
  2. Highlight 把区间包装成可注册的高亮对象;
  3. 通过 CSS.highlights.set(name, highlight) 注册到 HighlightRegistry,然后用 ::highlight(name) 写样式。

以搜索高亮为例,逻辑从”改 DOM”变成了”注册 Range”:

const range = new Range();
range.setStart(textNode, startOffset);
range.setEnd(textNode, endOffset);

const highlight = new Highlight(range);
CSS.highlights.set("search-hit", highlight);

对应的 CSS 只需要:

::highlight(search-hit) {
  background-color: yellow;
  color: black;
}

没有 wrapper span,没有 innerHTML 重写,没有事件监听器丢失。

证据:浏览器已经 Baseline 了,生产可落地

这条 API 不是实验性特性。MDN 把它标记为 Baseline 2025 Newly available,Chrome 105+、Edge 105+、Firefox 140+(2025 年 6 月)都已支持,Safari 也已经在 Technology Preview 落地。interop 2026 把它列为重点互操作项,意味着主流浏览器都在朝统一实现推进。

支持的可样式属性有限:colorbackground-colortext-decoration 系列、text-shadow-webkit-text-stroke-*。这恰好覆盖了搜索高亮、批注下划线、语法错误波浪线、选中态变色这些最常见的场景。background-image 不被支持,所以不能在这个 API 里做图片纹理高亮,但绝大多数业务不需要那个能力。

真正值得关注的是 Highlight.priority。当多个高亮在同一个文本区间重叠时,浏览器默认按注册时间决定样式优先级,但这不稳定。priority 是整数,数值高的覆盖数值低的,让协作编辑器里的”我的批注”和”系统搜索命中”可以分层控制,不用手动算 DOM 顺序。

落地建议:什么时候直接用,什么时候留 fallback

如果你的场景是搜索高亮、文档批注、拼写/语法错误标记、代码编辑器选区装饰,今天就可以直接用。这类场景的共同特点是:高亮是临时视觉状态,不需要参与业务逻辑的 DOM 查询,不需要序列化到服务端。

一个容易踩的坑是:高亮区间依赖文本节点的引用。如果底层 DOM 更新了,之前注册的 Range 会失效或指向错误位置。生产里需要把高亮更新和文档变更绑定,类似 MutationObserver 或框架的渲染生命周期。很多编辑器已经在这样做:CodeMirror 和 Monaco 的内部实验分支都在评估 Custom Highlight API,方向是把装饰层从 DOM wrapper 换成 Range + ::highlight。

对不支持的老浏览器,降级成本很低:检测 CSS.highlights 是否存在,不存在就回退到原来的 wrapper span 方案,功能一致,只是实现路径不同。这条 API 天然适合渐进增强。

可落地的下一步:找你们产品里还在用 innerHTML.replace() 做搜索高亮的那个页面,把高亮逻辑改成 Range + HighlightRegistry + ::highlight(),压一遍主流浏览器的渲染差异,大概率会发现 selection 和 copy 行为比之前更稳定。这件事值得这周就开个头。

评论区

0 条评论

登录后可评论。