裸辞半年后,我重新理解了“技术探索”这四个字的重量
大家好,我是去年从某大厂裸辞的成都打工人,Gap了整整半年——不是在青城山喝茶,就是在玉林路小酒馆听赵雷。但别误会,我可不是彻底躺平,中间其实断断续续写了不少代码,只是没打卡、没站会、没被产品经理凌晨三点拉进飞书群说“这个需求很简单”。
最近开始重新找工作,刷面试题刷到头秃。尤其是 JavaScript 相关的问题,表面问你 Promise 和 Event 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