写了八年代码编辑器,今天才发现它的「语法高亮」从来没离开过 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-size、border、padding 等影响布局的属性。高亮层只负责视觉叠加,不参与盒模型计算。
三个坑
Safari 限制:Safari 17.2+ 支持,但不支持 text-decoration 在 ::highlight() 上使用。Firefox 从 149 版本开始支持,background-color 和 color 都没问题。渐进增强写法:
if (!CSS.highlights) {
// 降级:用传统 span 方案
}
Range 不自动追踪 DOM 变化:文字内容变了,Range 位置不会自动更新,需要手动 range.setStart()/setEnd() 重新设置。对于协同编辑场景,通常每次收到 WebSocket 广播都要重新算 Range。
多高亮重叠时优先级:Highlight 有 priority 属性,数字越大优先级越高。浏览器在重叠区域会按优先级决定最终样式。
三步下一步
第一步:把这个 API 介绍给团队。用 CodePen 上的对比 Demo(codepen.io/Fcant/pen/rNZNBEB)跑一下,3000 个高亮场景肉眼可见帧率差距。
第二步:如果你在负责文档类产品,把现有的 span 高亮方案逐步迁移到 Custom Highlight API。先从搜索高亮开始,不改 DOM 结构,只改 JavaScript 逻辑。
第三步:关注 EditContext API 和 Custom Highlight API 的组合。Chrome 团队正在把两者结合起来做 IME 组合文字格式化,未来代码编辑器的语法高亮方案会越来越接近原生编辑体验。
浏览器底层在变。前端工程师手里的工具也在变。你最近一次写高亮,还在用 innerHTML 塞 <span> 吗?
评论区
登录后可评论。