普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月20日首页
昨天 — 2025年8月19日首页

解读 hidden=until-found 属性

2025年8月19日 16:06

本篇依然来自于我们的 《前端周刊》 项目!

由团队成员 嘿嘿 翻译,他的文章风格稳健而清晰,注重结构与逻辑的严谨性,善于用简洁的语言将复杂技术拆解成易于理解的知识点~

欢迎大家 进群 与他探讨,并持续追踪全球前端领域的最新动态!

原文地址:css-tricks.com/covering-hi…

image.png

我第一次发现这个功能是在 Firefox 139 发布说明 中,我当时心想:噢,有意思。然后我发现 Chrome 早在 2022 年就已经支持了。写到一半的时候,又看到 它已经在 Safari Technology Preview 125 中上线。嗯,这就是现状。

已经有一些不错的文章和教程在介绍 hidden=until-found,于是我决定记下一些要点,方便以后查阅。

它让隐藏内容“可被搜索”

简而言之:在 HTML 元素上加上 hidden=until-found,就能让该元素中隐藏的内容在浏览器的页面内搜索中被找到。

<div hidden="until-found">
  <!-- 隐藏内容 -->
</div>

你会看到(或者更准确地说,不会看到)内容被隐藏了:

本质上是 content-visibility: hidden

浏览器会把这个属性当作提示,从而在用户代理样式(user agent styles)上隐式地设置 content-visibility: hidden 来隐藏内容。

image.png

如果我们按下 Ctrl + F 来启用页面内搜索,输入一个查询词,匹配的内容就会显示出来,并高亮该匹配词。

image.png

我们为什么需要它

当我深入研究时,我自己也在问这个问题。最典型的示例来自 Chrome for Developers 文档,用作一个“伪手风琴(accordion)”。也就是一组可点击展开/收起的面板。

codepen.io/web-dot-dev…

但既然现在我们已经有了 [<details>](https://css-tricks.com/using-styling-the-details-element/) 元素,这不是已经解决了吗?毕竟它就是一个语义化的披露控件,用来 披露内容。实际上,浏览器在 <details> 元素的 ::details-content 部分也会设置 content-visibility: hidden 来隐藏内容。

image.png

我敢说 <details> 在 2022 年时并没有像今天这样广泛支持。它现在实际上是 Interop 2025 的一部分,其中提到的一个功能就是页面内搜索的能力。Chrome 已经支持它,Firefox 最近也上线了(大概是 hidden=until-found 发布的一部分),Safari 预计也会在 Interop 2025 中支持。Chrome 文档的例子实际上是一种绕过 <details> 不完全支持的做法,而如今 <details> 已经逐步补齐。

那么,为什么要有 hidden=until-found

我不确定。肯定有某些场景需要以一种可访问的方式隐藏内容,同时仍然让它能被搜索。但我一时想不到。比如说,我们还有 popover,不过它用的是 display: none,会把内容彻底从页面内搜索中移除。

image.png

浏览器支持和 polyfill

我们已经知道 Chrome 和 Firefox 都支持该功能,不过 Safari 还没有。但既然 <details> 的可搜索内容已经被列入 Interop 2025(Firefox 也因此增加了支持),我觉得 Safari 很快也会跟上。(事实证明没错,它已经在 Safari Technology Preview 125 中上线。)

那么问题来了:在此之前,hidden=until-found 值得用吗?如果我们追求一致的跨浏览器体验,就得在 content-visibility: hiddencontent-visibility: auto 之间来回切换。

Nathan Knowler 很专业地解释了其中的困境。我们无法在某个元素上直接用 content-visibility: hidden,因为那样会把它从页面内搜索中移除。而 hidden=until-found 的作用与 content-visibility: hidden 一样,但仍然保持可搜索。换句话说,我们没法用 content-visibility 来 polyfill 这个功能。

感谢 Nathan,他深入探索,提出了一个利用 Shadow DOM 的解决方案:检测 HTML 属性、检查支持情况,并在必要时回退其属性,以一种既可访问又不完全移除搜索能力的方式来隐藏内容。

样式(Styling)

对一个本来不可见的东西来说,似乎没什么可说的样式问题。但注意,页面内搜索功能会高亮匹配的内容。

image.png

看起来我们可能会迎来一个新的 ::search-text 伪元素,它允许我们选中匹配的搜索内容并设置高亮颜色,这在 CSS Pseudo-Elements Module Level 4 规范 中目前还是 Editor’s Draft 状态。

那如果有多个匹配呢?当前匹配和之后的匹配会有不同的高亮样式。

image.png

根据规范,我们大概可以把 ::search-text:current 伪类组合,来选中当前匹配:::search-text:current

如果你以为还能把 ::search-text:past:future 伪类混合使用,那恐怕要失望了。规范明确说这是无效的。但它也没有完全关上大门:

:past:future 伪类保留供将来使用。任何与 ::search-text 的不支持组合 必须 被视为无效。

还有别的吗?

没有太多了,但我很喜欢 Christian Shaefer 在 “Rethinking Find-in-Page Accessibility” 文章结尾的提示:我们需要考虑在搜索匹配后 接下来会发生什么。目前,在关闭或取消页面内搜索后,内容依然保持可见。也许我们还需要另一个 HTML 提示来解决这个问题。

参考链接

以下是我研究过程中用到的一些资料:

昨天以前首页

使用自定义高亮API增强用户‘/’体验

2025年8月16日 16:48

本篇依然来自于我们的 《前端周刊》 项目!

由团队成员 0bipinnata0 翻译,这位佬有技术追求、翻译风格精准细腻,还擅长挖掘原文背后的技术细节~

欢迎大家 进群 同该佬深度交流😁 以及持续追踪全球最新前端资讯!!

原文地址:Using the Custom Highlight API

生成前端周刊图.png

最近 CSS Custom Highlight API 引起了我的注意,因为 Firefox 最近开始支持它(Firefox 140,2025年6月),这使得所有主流浏览器都支持了这个 API。通过它,你可以对通过 JavaScript 中的 Range() 类获取的文本应用(某些)样式。我本来想说是你选择的文本,但这里实际上并没有涉及真正的普通选择器,这对于像我这样的 CSS 开发者来说是相当不寻常的。

我认为这里需要一个基本的文字说明,因为当我第一次开始研究它时,这样的说明肯定会对我有帮助:

  1. 你需要一个 textNode(例如 document.querySelector("p").firstChild

  2. 然后你需要一个 Range(),在其上执行 setStartsetEnd,这意味着范围现在在这两个整数之间。

  3. 然后你在该 Range 上调用 CSS.highlights.set(),给它一个名称。

  4. 然后你在 CSS 中使用 ::highlight(),传入你刚才使用的名称。

如果我们在页面上有一个 <p> 文本,整个过程看起来是这样的:

const WORD_TO_HIGHLIGHT = "wisdom";
const NAME_OF_HIGHLIGHT = "our-highlight";

const textNode = document.querySelector("p").firstChild;
const textContent = textNode.textContent;

const startIndex = textContent.indexOf(WORD_TO_HIGHLIGHT);
const endIndex = startIndex + WORD_TO_HIGHLIGHT.length;

const range = new Range();
range.setStart(textNode, startIndex);
range.setEnd(textNode, endIndex);

const highlight = new Highlight(range);
CSS.highlights.set(NAME_OF_HIGHLIGHT, highlight); 

在开发者工具中看到这个效果很有趣,单词 "wisdom" 明显应用了自定义 CSS 样式,但在该单词周围没有你通常认为应用这些样式所必需的元素。

image.png

这很可能就是浏览器本身在需要仅对文本的某些部分应用样式时所做的事情,比如当你使用浏览器内置的查找功能时。

image.png

这是演示:

codepen.io/editor/anon…

为什么这很有用?

  • 能够在完全不需要操作 DOM 的情况下定位和样式化文本是很有趣的。有时,DOM API 被批评为缓慢,所以能够避免这种情况可能是有利的,特别是如果你需要大量这样做的话。

  • 添加和删除 <span> 元素,除了可能"缓慢"之外,还会影响 DOM 结构,从而可能影响其他处理 DOM 的 CSS 和 JavaScript。

  • DOM 复杂度可能是网页性能的一个问题。过多的 DOM 节点,重新计算可能非常"昂贵",页面上的用户体验可能会受到影响,比如动画和滚动变慢。

这是一个只有 17 个更改文件的 GitHub PR 页面。该页面已经有超过 4,500 个 span 元素,用于诸如代码差异着色和语法高亮等功能。这已经相当重了,而且肯定会变得更糟。

![image.png](使用自定义高亮API增强用户‘+’体验+54149756-b7ed-41c7-9c27-b0ec61235095/image 2.png)

我确信这个 API 存在的原因还有很多,但这些只是我立即想到的几个原因。

做更多事情(搜索示例)

创建一个 new Highlight() 可以接受多个 Range。这意味着 CSS 中的单个 ::highlight() 可以应用于许多文本范围。如果我们在页面上构建自己的搜索功能,这将很有用。如果搜索是你正在构建的 Web 应用程序的关键功能,我可以很容易地想象为它构建自己的 UI,而不是依赖内置的浏览器功能。

这次,让我们让要在文本中查找的单词来自用户:

<label>
  Search the text below
  <input type="search" value="oven" id="searchTerm">
</label>  

然后我们监听变化:

window.searchTerm.addEventListener("input", (e) => {
  doSearch(e.target.value.toLowerCase());
}); 

注意我们将输入的值传递给一个函数,并在传递时将其转换为小写,因为搜索在不区分大小写时通常最有用。

我们的 doSearch 函数然后将接受该搜索词并在所有文本上运行正则表达式:

const regex = new RegExp(searchTerm, "gi"); 

我们需要的是一个包含所有找到的文本实例索引的数组。这是一段有点冗长的代码,但就是这样:

const indexes = [...theTextContent.matchAll(new RegExp(searchTerm, 'gi'))].map(a => a.index); 

有了这个索引数组,我们可以循环遍历它们创建 Range,然后将所有 Range 发送到新的 Highlight。

const arrayOfRanges = [];

indexes.forEach(matchIndex => {
  // 从索引值创建一个 "Range"。
  const searchRange = new Range();
  searchRange.setStart(par, matchIndex);
  searchRange.setEnd(par, matchIndex + searchTerm.length);

  arrayOfRanges.push(searchRange);
})

const ourHighlight = new Highlight(...arrayOfRanges);
CSS.highlights.set("search-results", ourHighlight); 

总的来说,它创建了一个功能完整的搜索体验:

codepen.io/editor/anon…

用于语法高亮

感觉语法高亮代码是这个 API 的一个很好的用例。André Ruffert 已经采用了这个想法并付诸实践,制作了一个 [<syntax-highlight> Web Component](https://andreruffert.github.io/syntax-highlight-element/),它使用 Lea Verou 的 Prism.js 来解析代码,但然后不像开箱即用的 Prism 那样应用 <span>,而是使用这个自定义高亮 API。

示例:

codepen.io/editor/anon…

我认为这很棒,但值得注意的是,这个 API 只能在客户端使用。对于语法高亮这样的功能,这可能意味着在看到代码和语法高亮"生效"之间会有延迟。我承认在可能的情况下,我更喜欢服务器端渲染的语法高亮。这意味着如果你可以从服务器提供一堆像这样的 <span>(并且不会严重影响性能或可访问性),那可能会更好。

我也承认我仍然对内置语法高亮的字体有些着迷,这感觉像是字体厂商可以进入的未开发领域。

我们让 JSON.stringify 的速度提升了两倍以上

2025年8月16日 15:32

本篇依然来自于我们的 《前端周刊》 项目!

由团队成员 掘金安东尼 翻译,欢迎大家 进群 持续追踪全球最新前端资讯!!

原文地址:v8.dev/blog/json-s…

生成前端周刊图.png

译者小结

JSON.stringify 提速的核心为以下6点:

  1. 快速路径:避开一大堆通用检查(节省 CPU 时间)
  2. 专用版本:按字符串类型分开编译(减少分支判断)
  3. 批量扫描:一次看多字符(降低循环次数)
  4. 缓存形状:重复对象直接批量处理(跳过重复工作)
  5. 更快算法:数字转字符串的计算加速(核心耗时优化)
  6. 分段缓冲:内存分配更聪明(避免大搬家)

原文

JSON.stringify 是 JavaScript 中用于序列化数据的核心函数。它的性能直接影响着 Web 上的常见操作——从为网络请求序列化数据,到将数据保存到 localStorage。更快的 JSON.stringify 意味着页面交互更迅速、应用响应更灵敏。这就是为什么我们很高兴地分享:最近的一次工程改进,使得 V8 中的 JSON.stringify 性能提升了两倍以上。本文将拆解实现这一提升的技术优化。

无副作用的快速路径

此次优化的基础是一条新的快速路径,建立在一个简单的前提上:如果我们能够保证序列化对象时不会触发任何副作用,就可以使用更快的专用实现。这里的“副作用”指的是任何会打破对象简单、顺序遍历的情况。

这不仅包括明显的情况,比如在序列化过程中执行用户定义的代码,还包括一些更隐蔽的内部操作,比如可能触发垃圾回收的过程。有关哪些情况会导致副作用,以及如何避免它们的更多细节,请参见 Limitations

只要 V8 能确定序列化过程不会出现这些情况,就可以一直停留在高度优化的路径上。这使它能够绕过通用序列化器中许多昂贵的检查和防御逻辑,从而在处理最常见的、代表纯数据的 JavaScript 对象时获得显著加速。

此外,这条新快速路径是迭代式的,而不是像通用序列化器那样递归。这一架构选择不仅免去了栈溢出检查,并允许我们在编码改变后快速恢复,还能让开发者序列化比以前更深层嵌套的对象图。

处理不同的字符串表示

在 V8 中,字符串可以用单字节或双字节字符表示。如果一个字符串只包含 ASCII 字符,它会被存储为单字节字符串,每个字符占 1 个字节。但如果字符串中有一个字符超出 ASCII 范围,那么整个字符串都会使用双字节表示,内存占用翻倍。

为了避免统一实现中不断分支和类型检查的开销,整个字符串序列化器现在基于字符类型进行模板化。这意味着我们会编译两个独立的、专门优化的版本:一个完全针对单字节字符串优化,另一个针对双字节字符串优化。这确实会影响二进制大小,但我们认为性能提升绝对值得。

该实现还能高效处理混合编码。在序列化过程中,我们必须检查每个字符串的实例类型,以检测无法在快速路径处理的表示形式(比如 ConsString,它在扁平化时可能触发 GC),这些会回退到慢路径。这个检查同时也能知道字符串是单字节还是双字节编码。

因此,从乐观的单字节序列化器切换到双字节版本几乎是零成本的。当检查发现双字节字符串时,就会新建一个双字节序列化器,并继承当前状态。最后,只需将初始单字节序列化器的输出与双字节版本的输出拼接即可。这种策略确保了在常见情况下保持高度优化的路径,同时转向处理双字节字符的开销很小且高效。

使用 SIMD 优化字符串序列化

在 JavaScript 中,任意字符串在序列化为 JSON 时都可能包含需要转义的字符(例如 " 或 ``)。传统的逐字符循环查找这些字符速度很慢。

为了加速这一过程,我们基于字符串长度采用了两级策略:

  • 长字符串:使用专用的硬件 SIMD 指令(例如 ARM64 Neon)。这样可以将字符串的大块内容加载到宽 SIMD 寄存器中,并在几条指令内同时检查多个字节是否存在需要转义的字符。
  • 短字符串:使用 SWAR(寄存器内 SIMD)技术。该方法通过在标准通用寄存器上进行巧妙的按位逻辑运算,以极低开销一次处理多个字符。

无论采用哪种方法,流程都很高效:按块快速扫描字符串。如果某个块中没有特殊字符(这是常见情况),就可以直接复制整个字符串。

快速路径上的“快速通道”

即使在主快速路径中,我们也找到了进一步加速的机会。默认情况下,快速路径仍需遍历对象的每个属性,并对每个键执行一系列检查:确认不是 Symbol、确保可枚举、扫描字符串是否包含需要转义的字符(例如 " 或 ``)。

为消除这些步骤,我们在对象的隐藏类上引入了一个标志。一旦我们序列化了对象的所有属性,就会将其隐藏类标记为 fast-json-iterable,前提是属性键都不是 Symbol、全部可枚举、且不包含需要转义的字符。

当我们序列化另一个具有相同隐藏类的对象(这种情况很常见,比如一组形状相同的对象数组)并且它是 fast-json-iterable 时,我们可以直接将所有键复制到字符串缓冲区,而无需进一步检查。

我们还将这种优化应用到了 JSON.parse,当解析数组时,如果数组中的对象通常有相同的隐藏类,就可以用它来进行快速键比较。

更快的数字转字符串算法

将数字转换为字符串是一个出乎意料的复杂且性能关键的任务。在 JSON.stringify 的优化中,我们发现可以显著加速这一过程,于是升级了核心的 DoubleToString 算法。我们用 Dragonbox 替换了长期使用的 Grisu3 算法,用于最短长度的数字转字符串转换。

虽然这一优化是为了 JSON.stringify,但新的 Dragonbox 实现会惠及 V8 中所有 Number.prototype.toString() 的调用。这意味着任何数字转字符串的代码,不仅仅是 JSON 序列化,都会自动获得这一性能提升。

优化底层临时缓冲区

任何字符串构建操作中的一个主要开销是内存管理。之前,我们的序列化器会在 C++ 堆上构建一个单一的连续缓冲区。虽然简单,但这种方式有一个显著缺点:一旦缓冲区空间耗尽,就必须分配更大的缓冲区,并将全部现有内容复制过去。对于大型 JSON 对象,这种反复分配和复制的过程会造成很大的性能损耗。

关键洞察是,强制这个临时缓冲区保持连续并没有真正的好处,因为最终结果只会在最后一步组装成一个字符串。

基于此,我们将旧系统替换为分段缓冲区。不再是一个大的、不断增长的内存块,而是使用 V8 的 Zone 内存分配一组较小的缓冲段。当一个段写满时,我们直接分配一个新的段继续写,完全消除了昂贵的复制操作。

限制

新的快速路径通过专门优化常见、简单的情况来实现速度提升。如果被序列化的数据不满足这些条件,V8 会回退到通用序列化器以确保正确性。要获得全部性能提升,JSON.stringify 调用需要满足以下条件:

  • 无 replacer 或 space 参数:提供 replacer 函数或 space/gap 参数(用于美化输出)会使其进入通用路径。快速路径仅支持紧凑的、未转换的序列化。
  • 纯数据对象和数组:被序列化的对象应是简单的数据容器,即它们及其原型不能有自定义的 .toJSON() 方法。快速路径假设标准原型(如 Object.prototype、Array.prototype),且无自定义序列化逻辑。
  • 对象无索引属性:快速路径针对具有常规字符串键的对象进行优化。如果对象包含类数组的索引属性(如 '0'、'1'…),则会使用较慢的通用序列化器。
  • 简单字符串类型:某些内部 V8 字符串表示(如 ConsString)在序列化前需要分配内存进行扁平化。快速路径避免执行可能触发这种分配的操作,最适合处理简单的顺序字符串。作为 Web 开发者,这一点难以直接控制,但大多数情况下都能正常工作。

对于绝大多数使用场景(如为 API 响应序列化数据、缓存配置对象),这些条件都是自然满足的,开发者可以自动享受到性能提升。

结论

通过从高层逻辑到底层内存与字符处理的全方位重构,我们在 JetStream2 的 json-stringify-inspector 基准测试中实现了超过 2 倍的性能提升。下图展示了在不同平台上的结果。这些优化从 V8 版本 13.8(Chrome 138)开始可用。

image.png

❌
❌