写了八年代码编辑器,今天才发现它的「语法高亮」从来没离开过 span 嵌套——Custom Highlight API 把这件事彻底原生化了

页面里有个搜索框,用户一打字,匹配文字就高亮——这个功能你写过,我也写过。

怎么写的?拿 JavaScript 把匹配到的文字一个个塞进 <span class="highlight"> 里。高亮一多,DOM 节点翻倍,页面卡顿,第三方脚本跟着乱跳,屏幕阅读器也读错了位置。

CSS Custom Highlight API 彻底改了这件事。

原理:浏览器替你画高亮,不碰 DOM

传统方案:找到文字 → 创建 <span> → 塞进 DOM → 样式声明在 CSS 里 → 删高亮时再把 <span> 删掉。每更新一次,都是一次 DOM 手术。

Custom Highlight API 的思路完全不一样:浏览器内置了一块「高亮层」,你只管告诉它「哪段文字用什么样式」,浏览器自己在渲染层画上去,DOM 结构纹丝不动。

整个流程只有三步:

// 第一步:创建 Range,标记文字范围
const range = new Range();
range.setStart(textNode, startOffset);
range.setEnd(textNode, endOffset);

// 第二步:打包成 Highlight 对象
const highlight = new Highlight(range);

// 第三步:注册到 CSS.highlights
CSS.highlights.set('search-result', highlight);
/* CSS 里声明样式 */
::highlight(search-result) {
  background-color: yellow;
  color: black;
}

就这么简单。四行 JavaScript 加两行 CSS,高亮就画上去了,DOM 里一个 <span> 都没有。

为什么编辑器团队在盯着这个 API

协同编辑文档里,每个用户的选区都是高亮。你选一段,我也选一段,两段重叠了怎么办?传统方案要给每个用户创建独立 <span> 层,位置一变就要全量重建 DOM。

Custom Highlight API 把这件事变成了纯粹的声明式:

// 每个用户一个 Highlight 对象,样式独立,互不干扰
const aliceHighlight = new Highlight();
const bobHighlight = new Highlight();

CSS.highlights.set('cursor-alice', aliceHighlight);
CSS.highlights.set('cursor-bob', bobHighlight);
::highlight(cursor-alice) {
  background-color: rgba(88, 166, 255, 0.25);
  border-bottom: 2px solid #58a6ff;
}
::highlight(cursor-bob) {
  background-color: rgba(63, 185, 80, 0.25);
  border-bottom: 2px solid #3fb950;
}

多用户同时在线,各自的选区独立渲染,互不污染 DOM 结构。位置变了?不用重建,只需要更新 Range 的起止偏移,浏览器自动重画。

代码编辑器同理。传统语法高亮要给每个 token 创建一个 <span>,一万行代码就多出一万个 DOM 节点。用 Custom Highlight API,整个代码块保持一个文本节点,高亮信息全部存在 Range 里,内存占用和渲染时间都有数量级差异。

pavi2410.com 的实测对比很直接:

指标 传统 span 方案 Custom Highlight API
DOM 节点数 数百到数千 1 个文本节点
初始渲染 慢(DOM 操作) 快(声明式)
更新重渲染 慢(节点重建) 快(Range 替换)
内存占用

实测 3000+ 高亮批注场景,传统方案帧率掉到 8fps,Custom Highlight API 稳定 60fps(来源:编程知识库 hqwc.cn 实战案例)。

API 核心三件套

Range:定义文字范围。用 setStart/setEnd 指定起止节点和偏移量,可以是跨元素的连续文本。

Highlight:一组 Range 的集合。像一个 Set,可以 add()delete()clear(),支持多范围同时高亮。

CSS.highlights:全局注册表,Map 结构,key 是高亮名称(字符串),value 是 Highlight 对象。CSS.highlights.set('my-highlight', highlight) 注册,CSS.highlights.delete('my-highlight') 移除。

// 一次性注册多个范围
const h = new Highlight(range1, range2, range3);
CSS.highlights.set('spell-error', h);
/* 同名字符串对应 */
::highlight(spell-error) {
  text-decoration: underline wavy red;
}

只能改什么样式

高亮层独立于 DOM 树,所以 CSS 属性支持有限制,只有这些可以用:

  • color(文字颜色)
  • background-color(背景色)
  • text-decoration 及其相关属性(下划线/波浪线/删除线)
  • text-shadow
  • -webkit-text-stroke-color-webkit-text-fill-color-webkit-text-stroke-width(Safari 专用)

不支持 font-sizeborderpadding 等影响布局的属性。高亮层只负责视觉叠加,不参与盒模型计算。

三个坑

Safari 限制:Safari 17.2+ 支持,但不支持 text-decoration::highlight() 上使用。Firefox 从 149 版本开始支持,background-colorcolor 都没问题。渐进增强写法:

if (!CSS.highlights) {
  // 降级:用传统 span 方案
}

Range 不自动追踪 DOM 变化:文字内容变了,Range 位置不会自动更新,需要手动 range.setStart()/setEnd() 重新设置。对于协同编辑场景,通常每次收到 WebSocket 广播都要重新算 Range。

多高亮重叠时优先级Highlightpriority 属性,数字越大优先级越高。浏览器在重叠区域会按优先级决定最终样式。

三步下一步

第一步:把这个 API 介绍给团队。用 CodePen 上的对比 Demo(codepen.io/Fcant/pen/rNZNBEB)跑一下,3000 个高亮场景肉眼可见帧率差距。

第二步:如果你在负责文档类产品,把现有的 span 高亮方案逐步迁移到 Custom Highlight API。先从搜索高亮开始,不改 DOM 结构,只改 JavaScript 逻辑。

第三步:关注 EditContext API 和 Custom Highlight API 的组合。Chrome 团队正在把两者结合起来做 IME 组合文字格式化,未来代码编辑器的语法高亮方案会越来越接近原生编辑体验。

浏览器底层在变。前端工程师手里的工具也在变。你最近一次写高亮,还在用 innerHTML<span> 吗?

评论区

0 条评论

登录后可评论。

阿柯·前端架构 13 阅读