博客
一行正则把 CPU 打到 100%:我把回溯的账算了一遍日期: 2026-10-10上周我给一个表单写了行邮箱校验正则,看着人畜无害,结果 32 个字符的非法输入让它跑了 2.65 秒——而且这 2.65 秒里整个 Node 进程是完全卡死的。我把正则回溯从头测了一遍:为什么 `^(a+)+$` 每多一个字符耗时就翻一倍(30 字符 2.9 秒,外推 50 字符是 35 天);为什么慢的永远是"匹配不上"(同一个正则,能匹配 0.041 ms,匹配不上 2904 ms);为什么 `^(a|aa)+$` 的增长倍率刚好是黄金比例 1.618;为什么 k 个相邻的 `a*` 就是 O(n^k)(10 万字符的 `^a*a*$` 要 3.3 秒,而它在 10 个字符的输入上永远是 0.01 ms);V8 的两级执行让同一个正则第一次跑比第二次慢 7.7 倍;一次 717 ms 的正则会让 50 ms 的定时器一次都触发不了;以及 V8 其实已经写好了一个线性时间引擎(`l` 标志,10 万字符 3.61 ms,代价是普通正则慢 64 倍)和一个"回溯过量就自动降级"的开关(默认阈值 50000),但两个开关默认都是关的。所有数字来自本机 Linux x86_64、Node v24.16.0 实测。
require() 一个 .mjs 之后:我把 ESM 和 CJS 的互操作账算了一遍日期: 2026-10-09上周我给一个 CJS 老项目接了个只有 ESM 版的依赖,require() 之后返回的不是我以为的那个对象;同一周,另一个包被 import 和 require 各加载了一份,`instanceof` 判断直接失效。我把 Node 24 的模块互操作从头测了一遍:require() 一个 .mjs 到底返回什么(会凭空多出一个 `__esModule: true`,属性描述符写着 writable: true 但严格模式下赋值照样抛错);ESM 具名导入 CJS 时的名字是 cjs-module-lexer 静态"猜"出来的,13 种写法里只有 6 种猜得中,`module.exports = { a, b }` 这种最常见的写法反而猜不中;双包危险会让同一个类变成两个类;import 进来的 CJS 可变字段是快照而 require 进来的 ESM 是活的;以及加载 500 个模块 CJS 要 9.9 毫秒、ESM 要 20.7 毫秒、require(esm) 要 28.1 毫秒。所有数字都来自本机 Node v24.16.0 实测。
setTimeout(fn, 0) 到底等多久:我把定时器的账算了一遍日期: 2026-10-07我一直以为 setTimeout(fn, 0) 就是"下一个tick马上执行"。直到我把它测了一遍:Node 里 0 会被强制成 1,稳定地等 1.05 毫秒;Chrome 里非嵌套时是 0 毫秒,但只要嵌套超过 5 层就跳到 4.1 毫秒——而 `await new Promise(r => setTimeout(r, 0))` 这种写法天然就是嵌套的,实测每次让出主线程要花 3.99 毫秒,换成 MessageChannel 是 0.006 毫秒。我还测了:300 万次 nextTick 能把一个 I/O 回调活活饿死、setInterval 跑 40 轮会漂 11 毫秒、setTimeout 的 delay 超过 2147483647 会溢出成立刻执行。所有数字都来自本机 Node v24.16.0 和 Chrome 149 的实测,脚本不依赖网络。
深拷贝这笔账:structuredClone 到底能拷什么,又偷偷丢下了什么日期: 2026-10-06我用 JSON.parse(JSON.stringify(obj)) 深拷贝了很多年,直到一个 Date 变成字符串、一个 undefined 变成 null、一个循环引用直接把页面炸掉。然后我才发现 structuredClone 已经能用很久了——但它也不是万能的:class 实例的原型会被剥掉、getter 会被求值成普通值、RegExp 的 lastIndex 会归零、Symbol 和函数直接抛异常、深度超过 1562 层就 RangeError。我把 Node v24.16.0 当成实验台,把可克隆类型清单、属性描述符的退化、引用语义、transfer 的移交语义、以及 structuredClone 与 JSON 路线在八种载荷上的性能对比全部跑了一遍。所有数字都是本地实测的。
gzip 今年 34 岁了:我把 1 MB 的 bundle 压了几十遍,算清 brotli 和 zstd 这笔账日期: 2026-10-05构建产物 1 MB,gzip -9 压完 293 KB,brotli q11 压完 205 KB——同样一份文件,差了 88 KB。我把 Node 24 的 zlib 当成实验台,把这三个算法在同一批真实文件上挨个跑了一遍:为什么 brotli 在小 HTML 上能压到 gzip 的 48%(静态字典)、为什么间隔超过 32 KB 的重复 gzip 就再也抓不住而 brotli 在 125 KB 上还抓得住(滑动窗口)、brotli q11 那个 1.1 MB/s 的压缩速度该怎么用、什么时候压缩反而会让文件变大、10 MB 的零能被压成 14 个字节意味着什么、以及压缩体积为什么会泄露你页面上的 CSRF token(BREACH)。全部数字都是本地跑出来的,脚本可以照着复现。
浏览器把自己关进了沙箱:Spectre、跨源隔离与那 95 微秒日期: 2026-10-04我在一个空白页面上连打二十万次 performance.now(),只得到了 203 个不同的数字。加上两个响应头之后,同样的代码吐出了 3960 个,最小步长从 100 微秒变成 5 微秒——浏览器把高精度能力的开关交了一半回来,代价是页面上所有没登记过的跨源资源全部断供。这篇用 Chrome 149 在两个 origin 上把这笔交易逐条拆开:COOP 和 COEP 的哪些组合才真的生效、拿回来的共享内存能跑到 2.89 亿次/秒(以及伪共享怎么把它打回 0.43 亿)、为什么帐单里 iframe 最贵、自己的 Service Worker 为什么也绕不过这道检查、以及 credentialless 和 anonymous iframe 这两扇逃生门。
浏览器终于有了个真文件系统:OPFS 与硬盘上那个叫 00000000 的文件日期: 2026-10-03我一直以为浏览器里没有文件系统,只有 localStorage 那几个凑数的柜子。直到我在 Chromium 的 profile 目录里 cat 出了一个 34 字节的文件,路径是 Default/File System/000/t/00/00000000,而文件名存在另一个 LevelDB 里。这篇把 OPFS 从 API 一路扒到磁盘:同步句柄为什么只在 Worker 里、写 1GB 有多快(以及为什么这个数字不可信)、SQLite 为什么盯上它、你的文件什么时候会被悄悄删掉。
一次 git commit 之后,硬盘上到底多了什么日期: 2026-10-02.git 用了十几年,一直是个黑盒。这次我建了个测试仓库把它整个拆开:git add 写进去的其实是个 26 字节的 zlib 文件、文件名根本不在文件旁边、31 个版本 5.26MB 的文档 gc 完只剩 26KB、而最新版本居然挂在 delta 链的最尾上。文里每个数字都是 git 2.47.3 真跑出来的。
同样的对象,换个写法慢十倍:V8 的隐藏类、内联缓存与垃圾回收日期: 2026-10-01一样的 { x, y },只是赋值顺序不同、或者多 delete 了一次,热循环就能慢 3 倍到 10 倍。这篇文章从 V8 的隐藏类(Map)讲起,一路讲到内联缓存、属性存储、elements kind 和 Orinoco 的分代 GC,最后给一份"什么值得改、什么不值得改"的清单。文里的每个数字都是我在 Node v24 上真跑出来的。
HTTP/3 不是 HTTP/2 加个 3:QUIC 为什么要在 UDP 上把 TCP 重造一遍日期: 2026-09-30打开 DevTools 看到 Protocol 那一列写着 h3,我一度以为 HTTP/3 就是 HTTP/2 换个版本号。往下挖一层才发现它把整个传输层重做了一遍,而且故意跑在不保证可靠的 UDP 上。这篇文章记一下我搞明白的过程:队头阻塞、中间盒僵化、0-RTT 的重放代价、换网不断线的 Connection ID,以及什么时候它反而更慢。