普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月15日首页

别再用 JSON.parse 深拷贝了,聊聊 StructuredClone

作者 ErpanOmer
2026年4月15日 09:58

临近下班,我们业务线出了一个极度无语的线上 Bug。

产品侧反馈,在一个非常核心的财务表单里,用户明明选择了 2026-04-14 作为结算日期,但点击提交后,整个页面直接白屏崩溃。

我打开错误监控看了一眼日志,立刻就把组里那个刚入职不久的小伙子叫了过来。 原因极其经典:他在把表单的原始状态同步给历史快照时,为了图省事,顺手写了一段几乎所有前端都写过的代码:

// 模拟用户表单数据
const formData = {
  amount: 1000,
  date: new Date("2026-04-14"), // 用户选的结算日期(Date对象)
};

// 深拷贝
const snapshot = JSON.parse(JSON.stringify(formData));

console.log("原始:", formData.date, typeof formData.date); 
// Date object

console.log("快照:", snapshot.date, typeof snapshot.date); 
// "2026-04-14T00:00:00.000Z" string

// 后续业务代码
function calcSettlementTime(data) {
  // 这里默认 date 是 Date 对象
  return data.date.getTime();
}

// 页面直接崩溃😢
try {
  const time = calcSettlementTime(snapshot);
  console.log("时间戳:", time);
} catch (err) {
  console.error("页面崩溃:", err);
}

他满脸委屈:老大,大家平时深拷贝不都是这么写的吗?🤷‍♂️

我让他自己把这段代码在控制台跑一遍。 当他看到表单里原本好好的 Date 对象,经过这一进一出,硬生生变成了一串 ISO 格式的字符串,导致后面调用 snapshot.date.getTime() 直接抛出 TypeError 时,他自己也沉默了。

作为前端老油条,这种因为 JSON.parse(JSON.stringify()) 引发的血案,我见过太多了。 它不仅会把 Date 变成字符串,还会把 MapSet 变成空对象 {},会把 undefinedSymbol 以及函数直接活生生抹除,更别提遇到循环引用时,它会当场抛出异常让你的主线程直接崩溃。

以前,我们为了解决这个破事,不得不在每个项目里老老实实 npm install lodash,然后引入那个笨重的 cloneDeep

但现在是 2026 年了。浏览器早就原生内置了完美的终极解药——structuredClone

今天咱们不聊虚的架构,就花三分钟,把这个原生 API 的底层逻辑讲清楚。


它是怎么解决历史遗留问题的?

structuredClone 不是什么语法糖,它是浏览器底层暴露出来的 结构化克隆算法(Structured Clone Algorithm)。这就意味着,它在 C++ 引擎层面的处理逻辑,远比 JS 业务层面的递归拷贝要深得多。

看一下原生 API 的用法:

const original = {
  date: new Date(),
  set: new Set([1, 2, 3]),
  map: new Map([['key', 'value']]),
  regex: /hello/i,
  buffer: new Uint8Array([1, 2, 3]).buffer,
};

// 制造一个循环引用
original.self = original;

// 一行代码,原生搞定
const cloned = structuredClone(original);

console.log(cloned.date instanceof Date); // true
console.log(cloned.set instanceof Set);   // true
console.log(cloned.self === cloned);      // true 完美处理循环引用!

发现没有?它不仅完美保留了所有的内置对象类型,连 JSON.parse 绝对搞不定的循环引用,它都处理得游刃有余。由于是在引擎底层运行,不需要像 Lodash 那样在 JS 运行时里疯狂压栈递归,它的执行效率在大部分复杂场景下都具有压倒性优势👍👍👍。


零拷贝转移 (Transferable Objects)

如果你以为 structuredClone 只是为了少引入一个 Lodash,那你就太小看浏览器的底层野心了。

它藏着一个 90% 的前端都不知道的极其硬核的功能:内存转移(Transfer)

在前端处理音视频、WebGL、或者读取几十 MB 的大文件时,我们经常会生成巨大的 ArrayBuffer。如果你用传统的深拷贝,内存瞬间翻倍,几十兆的内存分配极容易引起页面的掉帧卡顿。

structuredClone 提供了一个极其变态的第二个参数配置:{ transfer }

// 假设这是一个极大的 50MB 数据内存块
const u8Array = new Uint8Array(1024 * 1024 * 50);
const hugeBuffer = u8Array.buffer;

// 传统的深拷贝:内存翻倍,耗时极长
// const badCopy = lodash.cloneDeep(hugeBuffer); 

// 直接内存转移
const fastClone = structuredClone(hugeBuffer, { transfer: [hugeBuffer] });

console.log(fastClone.byteLength); // 52428800 (50MB 完美转移)
console.log(hugeBuffer.byteLength); // 0 (原对象的内存地址被转移)

这段代码的核心在于:它压根没有复制数据。 它直接在内存层面,把这块 50MB 数据的所有权,从 hugeBuffer 强行转移给了 fastClone。原对象被彻底掏空(变成了 detached 状态)。

这种零拷贝机制,在结合 Web Worker 处理复杂后台计算时,是打破性能瓶颈的绝对神器。这是任何第三方 JS 库都做不到的底层API。


一些坑要讲清楚🤔

既然这么牛,是不是以后项目里所有的拷贝闭着眼睛用它就行了? 作为一个踩过无数坑的老兵,我必须点出它的几个致命死角。如果你在真实的业务架构里滥用,下场比用 JSON.parse 还要惨。

对于函数和 DOM 节点的处理

JSON.parse 遇到函数,它会默默地忽略掉,至少不报错。 但 structuredClone 很直接。只要你的对象树里藏着一个方法,或者藏着一个 DOM 节点的引用,它会直接给你抛出 DataCloneError

const objWithFunc = {
  data: 123,
  onClick: () => console.log('click')
};

// 只要带有函数,直接抛同步错误
// DOMException: () => console.log('click') could not be cloned.
const copy = structuredClone(objWithFunc); 

这就意味着,如果你要拷贝的是一个 Vue/React 的响应式组件实例,或者是带有业务方法的数据模型,绝对不能用它👋。

原型链的断裂

不管你原本是一个通过 class 实例化的多么高级的业务对象,经过 structuredClone 的洗礼后,它都会变成一个普通的纯对象(Plain Object)。

class User {
  constructor(name) { this.name = name; }
  sayHi() { console.log('hi'); }
}

const user = new User('前端');
const cloneUser = structuredClone(user);

console.log(cloneUser instanceof User); // false 
cloneUser.sayHi(); // TypeError: cloneUser.sayHi is not a function

原型链上的所有方法全部丢失。它只关心纯粹的数据,不关心你的面向对象架构。‘


需要时收藏起来⭐⭐⭐

这几年,前端的工具链卷得飞起,大家的 package.json 越来越臃肿。遇到数组去重找库,遇到时间格式化找库,遇到深拷贝也要找库。

如果你只是单纯地处理一些后端传过来的嵌套数据,或者表单的复杂配置结构,完全可以直接把 structuredClone 敲在你的代码里。不用担心兼容性,目前主流浏览器(包括 Node.js)的支持率早就达到了工业级使用的标准了。

image.png

下次 Code Review 时,别再让我看到满屏的 JSON.parse 了 (玩笑😁😁😁)。

分享完毕,谢谢大家🙌

Suggestion.gif

昨天 — 2026年4月14日首页

做中后台业务,为什么我不建议你用 Tailwind CSS?

作者 ErpanOmer
2026年4月14日 10:33

1___f27S-qQF2CAASt5bOwqg.png

大家好,我又来了😁

最近我接手了一个隔壁组转过来的中后台重构项目。

交接的时候,对方的技术负责人特意跟我强调,说这个项目采用了最新的技术栈,全面拥抱了 Tailwind CSS,开发体验极其丝滑。

我当时心里还挺期待,毕竟这两年 Tailwind 的风刮得太大了,各种国内外大佬都在疯狂带货。结果周末我抽空把代码拉下来,打开 VSCode 准备梳理一下业务主流程。盯了屏幕不到十分钟,我感觉自己的眼睛快瞎了。

光说理论没意思,我直接给你们截取一段真实的、承载了各种表单校验和状态联动的业务侧边栏组件。你们自己品鉴一下所谓的极致开发体验👇:

const OrderCard = ({ order, isAdmin, isExpanded }) => {
  return (
    <div 
      className={`flex flex-col w-full p-5 mb-4 border rounded-lg shadow-sm transition-all duration-300 ${
        isAdmin ? 'bg-red-50 border-red-200' : 'bg-white border-gray-200'
      } ${
        isExpanded ? 'max-h-[800px]' : 'max-h-24 overflow-hidden'
      } hover:shadow-md cursor-pointer`}
    >
      <div className='flex items-center justify-between pb-3 mb-3 border-b border-gray-100'>
        <span className='text-sm font-semibold text-gray-800 truncate w-[60%]'>
          {order.id}
        </span>
        <span 
          className={`px-2 py-1 text-xs rounded-full ${
            order.status === 'PAID' 
              ? 'bg-green-100 text-green-700' 
              : 'bg-orange-100 text-orange-700'
          }`}
        >
          {order.statusText}
        </span>
      </div>
      {/* 内部极其复杂的业务字段渲染... */}
    </div>
  );
}

代码跑得通吗?当然跑得通。UI 还原度高吗?也挺高。 但是作为接下来要维护这个项目的组长,我只觉得一阵头皮发麻。

很多前端新人,甚至是做惯了 C 端 独立开发的兄弟,对 Tailwind 简直是顶礼膜拜。因为它不用取名字,不用在 JSCSS 文件之间来回切换,写起来确实有快感。

但作为趟过无数中后台项目的深水区,我今天必须给这股跟风热潮泼一盆冷水: 在绝大多数重型中后台业务场景里,Tailwind CSS 并不是什么神兵利器,而是给后期维护带来不方便。

来,咱们拿真实代码说话,看看它到底是怎么摧毁中后台工程的👇。


彻底掩盖你的业务主线?

做中后台系统,最难的从来不是画 UI,而是处理极度复杂的数据状态流转。

上面那段代码最大的问题在于信噪比极低。作为一个接手代码的前端,我点开这个文件,首先想看的是:这个组件在不同权限、不同展开状态下,业务逻辑是怎么走的?

但在 Tailwind 的体系下,我的视线全被 flexp-5mb-4max-h-[800px] 这种毫无业务价值的视觉原子类给强暴了。如果退回到老古董的 CSS Modules 方案,这段代码在 JS 侧应该长什么样?咱们对比一下:

import classNames from 'classnames';
import styles from './OrderCard.module.less';

const OrderCard = ({ order, isAdmin, isExpanded }) => {
  return (
    <div 
      className={classNames(styles.orderCard, {[styles.adminMode]: isAdmin,
        [styles.expanded]: isExpanded
      })}
    >
      <div className={styles.cardHeader}>
        <span className={styles.orderId}>{order.id}</span>
        <span 
          className={classNames(styles.statusBadge, {
            [styles.statusPaid]: order.status === 'PAID',
            [styles.statusPending]: order.status === 'PENDING'
          })}
        >
          {order.statusText}
        </span>
      </div>
      {/* 业务字段一目了然 */}
    </div>
  );
}

发现区别了吗?重构后的 JSX 变得极其纯粹。我只通过 adminModeexpanded 这种类名,就极其清晰地传达了业务语义。至于那个订单编号到底占百分之几十的宽度,那是 UI 层 该关心的事情,它安静地待在 .less 文件里,绝不会来污染我的业务主逻辑。


跟现成的组件库水土不服

中后台业务是不可能脱离 Ant DesignElement Plus 这类重型组件库的。而组件库的本质,是封装好了一整套内部的 DOM 结构和 ClassName 规范。

这就带来了一个极其致命的冲突:当你用 Tailwind 去覆盖 Ant Design 的内部样式时,你会写出极其恶心的 Hack 代码😖。

比如产品经理要求:在这个特定页面里,把 Ant Design 表格的表头背景色改成浅蓝色,单元格的 padding 改小一点。

正常的 Less 做法是用样式穿透,精确打击:

/* 样式文件覆盖 */
.myCustomTable {
  :deep(.ant-table-thead > tr > th) {
    background-color: #f0f8ff;
    padding: 8px 16px;
  }
}

你知道那个拥抱 Tailwind 的小伙子是怎么写的吗?为了不写 CSS 文件,他强行使用了 Tailwind v3+ 的任意变体语法:

// Tailwind 强行覆盖组件库内部样式
<Table 
  className='[&_.ant-table-thead>tr>th]:bg-blue-50[&_.ant-table-thead>tr>th]:py-2 [&_.ant-table-thead>tr>th]:px-4 [&_.ant-table-tbody>tr>td]:text-gray-600[&_.ant-table-tbody>tr>td]:py-2' 
  dataSource={data} 
  columns={columns} 
/>

这段代码合进主分支的时候,我都替后续维护的兄弟感到悲哀😢。

这玩意儿连换行都没有,密密麻麻挤在一起。未来如果要做主题切换,或者升级 Ant Design 导致内部类名变了,谁敢去动这坨连正则都极难匹配的字符串?

不仅没有提高开发效率,反而为了强行凑 Tailwind 的语法,写出了一堆极难维护的代码。


负边距引发的问题

一行代码被写出来的成本,远远低于它在未来三年里被维护的成本。

Tailwind 本质上就是披着 ClassName 外衣的行内样式。它把所有的样式固化在了 HTML 结构上。

设想真实的维护场景,前任开发为了让一个按钮和旁边的输入框对齐,极其随意地写了一个向左偏移负间距的类名:

<Button className='-ml-2 mt-1'>提交</Button>

半年后你接手这个需求,产品要求在这俩元素中间加一个 Icon。你看着这个 -ml-2mt-1 会陷入极其痛苦的挣扎:他当时为什么要写负边距?是因为外层父元素加了错的 padding 导致的?还是为了抵消 Button 内部自带的 margin?🤷‍♂️

在传统 CSS 中,我们往往会留有注释说明抵消输入框自带的右侧留白。但在 Tailwind 里,没有注释的容身之所。

为了保证不出线上 Bug,你绝对不敢删掉那个 -ml-2。你会选择在它后面再打个补丁,加个 pl-4 试图把它顶回来。 第二年,另一个接手的同事遇到了错位,又在后面补了一个 mt-[-5px]🤣🤣🤣。

日积月累,HTML 标签上的类名越来越长,死代码和冲突代码全堆在 DOM 上,最终变成一座没人敢碰的屎山💩。


别瞎搞,先认清你的场景🫡

说了这么多,难道 Tailwind 真的就是垃圾吗?当然绝对不是。

如果你在做偏 C端 的炫酷落地页、做 SaaS 官网,或者你是独立开发者,没有沉重的历史包袱,不需要配合复杂的重型组件库,那 Tailwind 绝对是神作。它自带极其优秀的设计规范,能让你极速堆出好看的界面。

但咱们讨论的是中后台。中后台是干嘛的?团队十几个人来回交接,动辄几百个页面,充斥着极其复杂的表单联动和权限校验,生命周期长达五年甚至十年。

在这种重型项目中,保持业务逻辑的纯粹性,分离关注点,远比你少写几行 CSS 要重要一万倍。

好了,今天分享到这,谢谢大家😁

谢谢大家.gif

昨天以前首页

卷AI、卷算法、2026 年的前端工程师到底在卷什么?

作者 ErpanOmer
2026年4月13日 11:18

1_jXusXvCfxECPU_Jh9S_E3w.jpg

最近是 2026 年的春招季,前几周密集面了大概快二十个前端。

翻开这批简历,我有一种极其魔幻的感觉:满屏都是 AI,满屏都是算法。

四五年前,大家简历上的高频词还是 精通 Vue3 响应式原理、熟练掌握 Webpack 性能调优

现在呢?十个候选人里,有九个写着熟练掌握 LLM 接入、深入理解 RAG(检索增强生成)、精通 Prompt 工程、参与过大模型 Agent 平台建设,剩下那个没写 AI 的,简历里赫然写着LeetCode 刷题 150+,精通动态规划与图论。

前端这个圈子,仿佛在一夜之间得了严重的技术焦虑并发症。大家都在拼命往简历里塞最高大上的词,生怕在 2026 年这个节点,因为不懂 AI 而被直接淘汰。

但现实是什么?

上周我淘汰了一个简历写得极其华丽、号称 主导过公司核心 AI 助手前端架构 的候选人。

我没问他大模型底层原理,也没让他手撕红黑树,我只问了他一个极其真实的业务场景: 在一个 AI 流式输出(Streaming)的对话场景里,如果大模型返回的是一个极其复杂的、带有代码块和多步工具调用(Tool Call)的 JSON 块。在流式传输还没结束、JSON 还是残缺状态的时候,你的前端是怎么保证 UI 不崩溃,并且能平滑渲染中间状态的?

他愣了半分钟,支支吾吾地说:我们用的是 Vercel AI SDK,它内部封装好了,直接拿 useChat 里的 messages 渲染就行……

😖😖😖...

我叹了口气,在面试评价上默默写下:只会调用 API,缺乏处理复杂工程能力。

这就是 2026 年前端圈最大的悲哀:大家都在卷 AI,但 90% 的人卷的只是如何发送一个带 API Key 的 HTTP 请求。


别把调用 API 包装成核心竞争力 🤷‍♂️

现在很多前端对懂 AI的理解极其肤浅。

以为在项目里接个 OpenAI 或者 Claude 的接口,搞个对话框,把输入框的字传过去,把返回的字用 Markdown 渲染出来,就叫AI 前端工程师了😖。

兄弟,那不叫 AI 开发,那叫表单提交。这种活儿,三年前刚培训班毕业的实习生也会干。

大模型时代,前端真正的难点根本不是发送请求,而是 应对大模型带来的复杂性。

以前我们写业务代码,接口返回的数据结构是确定的,是后端的 Swagger 定义好的。你只需要 if (res.code === 200) 然后按部就班地渲染。

但在 2026 年,大模型吐出来的东西是不可控的。 真实的高阶 AI 前端工程,每天要面对的是这些破事:

流式返回进行到一半,JSON 连个闭合的括号都没有,你的界面怎么解析?怎么渲染正在打字的生成式 UI?

一个 Agent 在后台疯狂调用工具(查天气、查数据库、画图),这个过程中产生的大量异步中间状态,如何在 React/Vue 中做防抖、状态合并和打断(Abort)?

大模型突然抽风,返回了完全不符合预期的组件协议,你的前端系统能不能做沙盒隔离,保证不引发整个页面的白屏崩溃?

这些问题,根本不是你背几个 Prompt 模板就能解决的。它考验的是你对数据流处理、AST(抽象语法树)解析、复杂状态机设计以及防御性编程的底层功底。

你卷了半天 Vercel AI SDK 的用法,一旦业务场景超出了 SDK 的默认配置,你立马就抓瞎了。


为什么面试官越来越爱考算法?

说完了 AI,再聊聊算法。这也是现在前端同行疯狂吐槽的点:我特么一个画页面的,凭什么让我手写动态规划?🤔

其实这是个很残酷的信号。

作为面试官,我跟你交个底:因为那些常规的、套路化的前端业务代码,现在 AI 真的能写了,而且写得比你快。

2026 年了,如果你只会写个增删改查的表格,只会封装个按钮组件,我在面试里连问你的兴趣都没有。既然基础的搬砖工作被 AI 大幅压缩了,那公司招人,过滤标准自然就要往上提。

考算法,本质上考的不是你对某道题的背诵能力,而是考你的复杂逻辑拆解能力和极限思维

特别是在做 AI 工具链的前端时:

  • 当你要在浏览器端用 WebAssembly 跑一个轻量级的向量数据库(Vector DB)进行本地 RAG 检索时,不懂数据结构你连原理都看不懂。
  • 当你要处理大模型返回的超大文档树,做精确的 DOM 节点比对和替换时,树的遍历算法就是你的基本功。

大家不是在卷算法,而是在抢夺那些AI 无法轻易替代的深水区岗位🤔。


没必要那么焦虑

前天面试结束,跟几个同组的技术老炮抽烟。大家感慨,其实这十年来,前端圈的焦虑从来没停过。

当年 jQueryReact 淘汰时,大家在卷;后来小程序大爆发时,大家也在卷;现在大模型来了,大家不过是换了个名词继续卷。

别被那种 AI 要干掉前端的鬼话吓倒了,也别为了迎合面试官去死记硬背什么 RAG 架构图。

潮水退去的时候,企业最终留下的,永远不是那个会背时髦名词的人,而是那个懂 HTTP 协议、懂浏览器底层、能在复杂的异步环境里把一个烂摊子稳稳托住的前端。

在这个越发喧嚣的 2026 年,少去追逐那些虚幻的词汇,多去打磨你手里的基本功吧🤷‍♂️

共勉🙌

加油加油加油.gif

OpenClaw 跟病毒的区别是什么?

作者 ErpanOmer
2026年4月7日 16:10

节日期间在家办公,我坐在书房的电脑前,盯着满屏飘红的终端😖

webpack_error_terminal_style_match.png

我没有中勒索病毒,也没有被黑客攻击。我只是在之前,极其手欠地给跑在后台的 OpenClaw 下达了一句简单的语音指令:帮我把这个老项目里的无用 npm 依赖清理一下,顺便跑通本地编译。

openclaw_feishu_chat_conversation.png

然后我就去客厅看电视了。

等我两个小时后回来,发现风扇狂转。打开终端一看,这玩意儿不仅把我的 package-lock.json 给删了,还因为有个老旧的 Sass 模块死活装不上,它自己去网上搜了个不知道谁写的 Python 脚本跑了一遍,顺手把我的全局 Node 环境降级到了两年前的版本,最后还在根目录下给我留了几十个不知名的临时编译文件🤬🤬。

看着这片惨状,我脑子里突然冒出一个极其荒诞的问题: 一个拥有系统最高执行权限的 OpenClaw,跟一个木马病毒的区别到底是什么?

如果仔细推敲,你会发现这两者的行为轨迹惊人地相似,甚至可以说,前者带来的工程灾难往往更具欺骗性。


在搞破坏?

以前我们在电脑上跑个脚本,报错了就停在那,等你来排查,过程相对可控的。

但现在的 OpenClaw 是个拥有极高自主性的 Agent。它最大的卖点是遇到问题会自动尝试解决。这在写写单纯的文本时是个优点,但在复杂的现代前端工程里,这就是个彻头彻尾的灾难🤔。

当一个病毒遇到权限阻断时,它会疯狂尝试提权、扫描端口、注入进程。 那 OpenClaw 遇到前端编译报错时会干嘛?

它会像一个极其鲁莽的瞎子:

  • 它发现 pnpm install 报错了,它不会去思考是不是内网镜像源挂了,而是自作主张把它换成 npm,瞬间摧毁你精心维护的 Monorepo 幽灵依赖机制(symlink)。
  • 它发现有个类型找不到,它不会去查 .d.ts 声明,而是极其粗暴地去改你 node_modules 里的源码,或者给你全剧加上 @ts-ignore
  • 如果遇到文件死锁,它甚至敢在终端里直接替你敲下 rm -rf

病毒搞破坏是为了勒索你,而 OpenClaw 把你的系统搞崩溃,仅仅是因为它想完成你那句帮我跑通编译。

后台静默执行

做了 9 年研发,我看过无数次因为一行配置写错导致的线上 P0 级事故。所以越是资深的工程师,越在乎执行边界。

我们为什么需要 Git?为什么需要 Code Review?为什么 CI/CD 要分发不同的环境权限? 因为我们要清楚地知道,谁在什么时候,动了哪行代码,引发了什么后果。

但 OpenClaw 打破了这个铁律。它是一个跑在你电脑或者服务器上的巨大黑盒。

当你让它接管你的工作流时,你根本不知道它为了完成目标,在后台下载了多少个包含潜在风险的三方库。你也不知道它在调用工具链的时候,有没有把你的环境变量、甚至包含数据库密码的 .env 文件当成上下文,明文发送给了大模型的 API 服务器。

传统病毒窃取你的隐私,至少还会偷偷摸摸加个壳。而 OpenClaw 是你亲手把服务器的 Root 权限双手奉上🙌。


一段真实的案例

为了让大家更直观地感受这种病毒式操作。我复盘了一下昨天它为了修复一个构建错误,所做出的决策链路。

这是一个典型的高级前端遇到问题时的处理逻辑:

1. 发现 Webpack 构建报 chunk error 错误

screenshot-20260407-151125.png

2. 检查报错日志,发现是内存溢出 (OOM)

screenshot-20260407-151038.png

3.修改启动脚本,增加 Node 内存上限,完事🤷‍♂️

export NODE_OPTIONS="--max-old-space-size=4096"
npm run build

而 OpenClaw 在面对同一个报错时,它的终端执行记录:

# 1. 发现构建报错
npm run build 

# 2. 它看不懂 OOM,以为是依赖问题,执行了极其致命的操作
rm -rf node_modules
rm package-lock.json
npm install --legacy-peer-deps

# 3. 依赖彻底乱套,报了更多编译错误。它决定更换打包工具
npm install -g vite
# 强行在原本的 Webpack 项目里塞入一段残缺的 vite.config.js

# 4. 依然跑不通,它甚至去改了操作系统的环境😖😖😖
sudo chmod -R 777 ./ 

看到最后那个 chmod 777 的时候,我后背都在冒冷汗😢。 它为了解决一个权限微小的编译告警,直接把你整个项目的安全底裤都给扒了。这不是病毒是什么?


我想泼一盆冷水🫡

现在全网都在狂欢,各大社区都在比拼谁的 Agent 更聪明,谁能让电脑完全自己写代码、自己部署。

很多人沉迷于这种看着终端自己跳动代码的爽感中,觉得这就是未来。

但我不得不泼一盆冷水。在工程领域,不可控的自动化,比纯手工还要危险一万倍😒。

不管是 OpenClaw 还是未来更牛的智能体,只要它不具备真实世界的工程常识和后果承担能力(到目前为止都不可能为你背锅!!!),它就是一个披着 AI 外衣的高危病毒。

咱们在敲下回车之前,脑子里想的是:这会影响线上吗?会引发竞态问题吗?接手的同事能看懂吗? 而 Agent 脑子里只有计算概率:根据统计学,下达这个指令,满足用户当前 prompt 的概率哪个最大?它不在乎你的硬盘会不会被占满,不在乎你的生产环境会不会被污染。


所以,咱们这些在一线干活的兄弟们,清醒一点。

工具终究是工具,它可以帮你查 API,可以帮你写正则,可以帮你生成模版代码。但千万别把系统的控制权和架构的决策权,交给一个随时可能暴雷的 AI Agent

把危险关在沙盒里,让执行处于监控下。如果你做不到这一点,那你电脑里跑着的那个每天对你嘘寒问暖的 OpenClaw,真的比熊猫烧香还要可怕的。🤔

对此大家怎么看?

Suggestion.gif

一周狂揽40K+ Star⭐ 的 Pretext 到底有多变态?

作者 ErpanOmer
2026年4月8日 11:24

这周的前端圈,可以说是被一个叫 Pretext 的项目彻底刷屏了。

短短几天,GitHub 狂揽 41K+ Stars ⭐⭐⭐

很多刚入行的小伙伴看完官方那个极简的 Readme 可能一头雾水:不就是一个算文本长宽高的 JS 库吗?为啥能火成这样?CSS 的 word-wrapflex 难道不够用吗?

v2_741bc7c1a79445528b75ddc1980d6ccd@46958_img_gif.gif

v2_c43f8b79c3f6400d9a995d5f0adc869d@46958_img_gif.gif

v2_715e9ba3c8aa4ea7b8e1bc5b41f87ead@46958_img_gif.gif

但如果是被各种复杂表格、虚拟列表、Canvas 渲染折磨过的老兵,看到 Pretext 的那一刻,绝对会有一种激动的。

因为这个库,极其优雅地干掉了前端性能优化里最恶心、最顽固的问题——强制同步布局(Forced Synchronous Layout)导致的重排(Reflow)。

今天,咱们不念官方文档,结合我这几年的填坑血泪史,聊聊这个 41K Star 的怪物到底解决了什么世界级痛点,以及我们在真实的业务里该怎么用它。


那些年用过的 getBoundingClientRect

前端开发有个大难题:一串动态文本渲染出来到底有多高?

设想一个极度真实的业务场景: 你在做一个拥有十万条数据的 虚拟滚动列表(Virtual List)。为了让列表丝滑,你只能渲染视口内的那 20 条数据。 但问题来了,每条数据里的用户评论长度是不固定的。有的人发了一句 哈哈😁,有的人发了 800 字的写字楼小作文。 在渲染之前,你必须提前知道每一行的高度,才能计算出整个虚拟列表的滚动条位置和绝对定位的 top 值。

在 Pretext 出现之前,我们是怎么做的? 用的往往是最原始、极其粗暴的 离屏 DOM 测量法(Offscreen Measurement)

// 极其恶心的传统测量法:DOM 测算
function measureTextHeightOldWay(text, width, fontSize) {
  // 1. 创建一个隐藏的 div
  const hiddenDiv = document.createElement('div');
  hiddenDiv.style.visibility = 'hidden';
  hiddenDiv.style.position = 'absolute';
  hiddenDiv.style.width = `${width}px`;
  hiddenDiv.style.fontSize = `${fontSize}px`;
  hiddenDiv.innerText = text;

  // 2. 强行塞入 DOM 树
  document.body.appendChild(hiddenDiv);

  // 3. 读取高度(灾难的开始!!!)
  const height = hiddenDiv.offsetHeight; // 或者 getBoundingClientRect()

  // 4. 销毁 DOM
  document.body.removeChild(hiddenDiv);

  return height;
}

代码看着没毛病? 但如果在初始化时,你在一个循环里把这段代码跑了 1000 次,你的页面会当场卡死白屏!

为什么?因为浏览器底层是一个极度慵懒的系统。你操作 DOM 节点,它通常会先攒着,等这一帧结束再一次性绘制。 但当你调用了 offsetHeight 或者 getBoundingClientRect 时,浏览器为了给你一个最精确的值,会被迫打断所有的优化,立刻在主线程里重新计算整个页面的布局(Reflow)。

你循环调用 1000 次,浏览器就被迫重排 1000 次。这种 布局抖动(Layout Thrashing) 是前端性能的头号杀手。


Pretext 的降维打击 - 不碰 DOM,纯数学演算

而 Pretext 的核心卖点,就写在它的第一句介绍里:纯 JavaScript/TypeScript 库,避免了对 DOM 进行测量。

image.png

它完全抛弃了把元素塞进 DOM 里量一下的蠢办法。 你要算这段文字占据多少像素?好,你告诉我字体、字号、容器宽度,我直接在 JS 内存里,通过底层的文本排版算法,硬生生给你出来!

咱们直接上代码,看看接入 Pretext 之后,世界变得有多清爽:

// 使用 Pretext
import { measureText } from 'pretext';

function measureTextHeightNewWay(text, containerWidth) {
  // 没有任何 DOM 操作!直接传入参数计算
  const metrics = measureText(text, {
    fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto',
    fontSize: 14,
    lineHeight: 1.5,
    maxWidth: containerWidth,
    // 甚至支持复杂的换行策略
    wordBreak: 'break-word', 
  });

  // 直接拿到精准的宽高和行数!
  return metrics.height; 
}

对比一下,这带来了什么工程级别的质变?

快到离谱: 因为不触碰任何 DOM API,不会引起一丝一毫的浏览器重排。同样的测量 1000 条数据,用传统 DOM 法可能需要 300ms(掉帧卡顿),用 Pretext 只需要 2ms。

解锁 Web Worker 潜能: 以前因为要操作 DOM,测量文本的脏活必须在浏览器的主线程(UI 线程)干,极容易阻塞页面。现在它是纯 JS 计算了,你完全可以把这十万条文本高度的计算逻辑,扔到 Web Worker 里去并行跑!主线程依然丝滑如初。

跨平台降维打击: 因为纯 JS/TS,它不仅能在浏览器 DOM 里跑,它还能在 Canvas 游戏引擎里跑,在 Node.js 服务端渲染(SSR)里跑,甚至未来能在 React Native 里跑。

Pretext vs getBoundingClientRect 性能对比

image.png

更多好玩的demo 👉:https://chenglou.me/pretext/

image.png

看似比较简单,实则硬核的技术深水区

其实纯 JS 测算文本这个想法,很多前端老手都想过。为什么直到 2026 年,才被 Pretext 彻底做成了一个 40K+ Star 的杀手级项目?

因为文本排版(Text Layout)是一个深不见底的黑洞。

你以为算个宽度就是 字符数 × 字体宽度? 太天真了。你需要考虑英文单词的断词(换行不能把单词截断)、需要考虑阿拉伯语的从右到左(RTL)、需要考虑中文的标点符号避头尾规则、更别提那些五花八门的 Emoji(有的 Emoji 占好几个字节,但在屏幕上只是一个字符)。

更变态的是,不同浏览器(Chrome / Safari)底层的字体引擎(HarfBuzz 等)渲染规则都有细微差异。

Pretext 的作者 chenglou(做过 React Core,写过 ReasonML 的真大神👍👍👍)用了一种极其聪明且符合现在 AI 时代的方法:将浏览器自身的字体引擎作为基准进行迭代对齐。

image.png

它没有去傻傻地重写一套从零开始的渲染引擎,而是找到了一套能与主流浏览器高度拟合的纯数学计算逻辑。精度极高,且极其轻量。

这不是在造轮子,这是在用极客思维给现有的前端标准打补丁。


那么,哪些场景该果断接入 Pretext?

虽然我把它吹爆了,但作为一个老油条,我必须负责任地告诉你:不要脑子一热,把项目里所有的普通 CSS 排版都换成它。 CSS 引擎依旧是渲染标准流最稳定、最简单的方案。

Pretext 是属于极端场景。 遇到以下三种情况,直接掏出它:

复杂数据看板 / 大规模动态虚拟列表: 前面提到的,需要提前精确知道变长文本高度,来进行复杂绝对定位 计算的场景。

Canvas / WebGL 富文本渲染: 用过 Canvas 的人都知道,Canvas 里的 fillText 极其原始,根本不支持自动换行。以前我们在 Canvas 里画多行文本简直是噩梦,现在可以直接用 Pretext 算好每一行的位置,然后精确绘制。

基于 Node.js 的海报/PDF 自动生成系统: 服务端没有 DOM 环境,以前为了算一下文本会不会超出海报边界,还得专门在服务端起一个无头浏览器(Puppeteer),贼耗服务器资源。现在直接 Node.js 引入 Pretext 纯端计算,一台 2 核机器能顶过去 8 核的并发量。


这才是前端该有的样子🤔

这两年,前端圈充满了大模型、AI 生成代码的焦虑,似乎一切不加个 AI 前缀就不够前沿。

但看到 Pretext 这种纯粹为了解决计算机图形学底层痛点、一行一行扣性能、追求极致优雅的开源项目,短短几天收获 40K+ Star,我心里其实是挺欣慰的。

它证明了一件事:在花里胡哨的概念之外,这个世界上永远有那些扎根在工程最深处、被真实痛点折磨的开发者。

真正高级的前端工程能力,不是你接了多少个最新的大模型 API,而是当系统出现肉眼可见的卡顿时,精准地指出那句隐藏在万行代码里的 offsetHeight,然后用纯粹的数学与算法,把页面性能拉升两个数量级。

周末了,别只顾着看个热闹,去把 Pretext 拉下来,在本地建个 Canvas 或者虚拟列表的 Demo 跑一跑。

那种看着耗时从 300 毫秒断崖式下跌到 2 毫秒的爽感,才是写代码真正的乐趣😁。

❌
❌