裸辞半年后,我重新理解了“技术探索”这四个字的重量

TPS计算员
2025-12-13 23:05
阅读 432

大家好,我是去年从某大厂裸辞的成都打工人,Gap了整整半年——不是在青城山喝茶,就是在玉林路小酒馆听赵雷。但别误会,我可不是彻底躺平,中间其实断断续续写了不少代码,只是没打卡、没站会、没被产品经理凌晨三点拉进飞书群说“这个需求很简单”。

最近开始重新找工作,刷面试题刷到头秃。尤其是 JavaScript 相关的问题,表面问你 PromiseEvent Loop,背地里其实在考察你有没有真正“动手做过事”。这让我突然意识到:技术探索不是刷 LeetCode,而是把问题搞明白,再优雅地解决掉

今天想和大家聊聊我在一个真实项目中,如何从一个看似普通的前端性能问题,一步步深入到 JS 引擎底层机制,最终完成一次“有味道”的技术实践。


事情要从去年双11说起

当时我在老东家(一家对用户体验要求近乎偏执的大厂),负责一个商品详情页的重构。产品经理信誓旦旦地说:“这次我们一定要做到首屏 0.8 秒内加载完成!”——结果上线前一周,页面在低端安卓机上白屏长达 3 秒,用户直接流失。

我打开 Chrome DevTools Performance 面板一看,好家伙,主线程被一段“无辜”的数据处理逻辑卡住了整整 1.2 秒。这段代码大概是这样的:

// 模拟从后端拿来的 10w 条商品评论
const comments = fetchHugeComments();

// 在主线程直接处理
const processed = comments.map(item => {
  return {
    id: item.id,
    summary: summarize(item.content), // 耗时文本处理
    sentiment: analyzeSentiment(item.content) // 更耗时的情感分析
  };
});

当时我真的想砸电脑。这代码是谁写的?哦,是我自己上周五晚上赶 deadline 时写的……(别问,问就是“临时方案”)


别让 JS 阻塞你的 UI

面试题里经常问:“JS 是单线程的吗?”
答:“是,但 Web Workers 可以开子线程。”
但真到了项目里,谁用过?反正我以前觉得“用不到”,直到这次被现实毒打。

我第一反应是优化算法,比如缓存、懒加载、分页。但产品说:“不行,用户要看到所有评论的聚合情感趋势图!”——行吧,那只能上 Web Worker 了。

但问题来了:Worker 和主线程通信靠 postMessage,传大量数据会序列化/反序列化,反而更慢。怎么办?

这时候我翻了翻 V8 的文档(对,裸辞期间我居然看起了 V8 源码注释,人果然不能闲太久),发现了一个冷门但超好用的 API:SharedArrayBuffer + Atomics

不过先别急,现代浏览器出于安全考虑,默认禁用了 SharedArrayBuffer(Spectre 漏洞的锅)。得加 HTTP 头:

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

运维小哥看到后一脸懵:“你这是要搞跨域攻击还是咋的?” 我只能苦笑:“不,我只是想让页面快点。”


实践:用 SharedArrayBuffer 做零拷贝通信

思路很简单:主线程把原始数据转成 Uint8Array,放进 SharedArrayBuffer,Worker 直接读取并处理,结果也写回同一个 buffer。全程无序列化!

主线程代码(简化版):

// 1. 准备共享内存
const sab = new SharedArrayBuffer(1024 * 1024); // 1MB
const view = new Uint8Array(sab);

// 2. 把评论数据 encode 成二进制(这里用简单示例)
const encoder = new TextEncoder();
const encoded = encoder.encode(JSON.stringify(comments));
view.set(encoded, 0);

// 3. 启动 Worker
const worker = new Worker('processor.js');
worker.postMessage({ sab, length: encoded.length });

// 4. 等待结果(用 Atomics.wait 阻塞?不!用信号量更安全)
let result;
worker.onmessage = (e) => {
  const { offset, size } = e.data;
  const resultBytes = new Uint8Array(sab, offset, size);
  result = JSON.parse(new TextDecoder().decode(resultBytes));
};

Worker 端:

self.onmessage = (e) => {
  const { sab, length } = e.data;
  const inputView = new Uint8Array(sab, 0, length);
  const inputData = JSON.parse(new TextDecoder().decode(inputView));

  // 处理数据(这里可以跑 heavy computation)
  const output = processComments(inputData);

  // 写回结果
  const encoder = new TextEncoder();
  const outputBytes = encoder.encode(JSON.stringify(output));
  const outputView = new Uint8Array(sab, length);
  outputView.set(outputBytes);

  // 通知主线程:结果从 length 开始,长度为 outputBytes.length
  self.postMessage({ offset: length, size: outputBytes.length });
};

⚠️ 注意:实际项目中要考虑内存对齐、多段数据、错误边界等,这里仅为示意。


效果对比:从 1.2s 到 200ms

我把这套方案上线灰度后,低端机首屏时间直接从 3s+ 降到 1.1s,主线程阻塞从 1.2s 降到 200ms 以内。最关键的是,用户不再看到白屏!

方案 主线程阻塞时间 首屏加载 内存占用
原始同步处理 1200ms 3100ms 中等
分页 + 懒加载 300ms 900ms(但需滚动)
Web Worker (postMessage) 150ms 1200ms 高(双份数据)
SharedArrayBuffer <200ms 1100ms 低(共享内存)

虽然首屏没到产品经理吹的 800ms,但团队已经感动哭了——毕竟我们是在兼容 IE11 的遗产系统上改的(别问,问就是“历史原因”)。


面试题背后的真相

现在回过头看那些 JS 面试题:

  • “Event Loop 是什么?” → 其实是在问你是否理解任务调度与 UI 响应的关系
  • “微任务和宏任务区别?” → 背后是如何避免长时间任务阻塞渲染
  • “Web Worker 怎么用?” → 考察你是否具备将计算移出主线程的意识

这些题,光背答案没用。面试官(尤其是大厂)更想听你讲:“我在某个项目里遇到了 XX 问题,尝试了 A/B/C 方案,最后选 D,因为……

这半年 Gap 期,我没刷题,但我重写了三个开源库的文档,给 V8 提了一个 tiny 的 doc fix,还用 Rust 写了个 WASM 模块来加速文本处理(虽然最后没用上)。这些经历让我明白:技术探索不是为了炫技,而是为了解决真实世界的毛刺


最后一点碎碎念

在成都的生活节奏确实舒服,茶馆里敲代码都比在工位香。但程序员这行,停一天就可能落后一圈。裸辞不可怕,可怕的是停止思考。

如果你也在准备面试,别只盯着“手写 Promise”、“实现 LRU”。试着问自己:

“我最近一次为性能问题睡不着觉,是因为什么?”

那个答案,才是你技术深度的起点。

共勉。

(PS:有成都的前端小伙伴想组队搞 side project 吗?我 Mac 已充好电,就差一个 idea 了 🍵)

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝