技术探索与实践的一些思考:从一次差点背锅的线上事故说起
上周五晚上十点半,我正用 Vim 写一个组件的逻辑,突然钉钉“叮”一声——运维群里炸了:“首页白屏!大量用户反馈打不开!” 我心里咯噔一下,赶紧 :wq 切到监控面板。果不其然,JS 错误率飙升到 15%,CDN 日志里全是 499 和 502。
那一刻我真的想砸电脑。不是因为 bug,而是因为——这锅本来不该我背。
我是杭州某二线互联网公司的前端,干了快三年。平时写代码偏爱 Vim,IDE 开得少(别笑,真不是装,是习惯了)。公司离阿里园区就两站地铁,时不时还能蹭个技术沙龙。我们组不大,但产品节奏贼快,双 11、618 这种大促一来,全员进入“人肉压测”模式。产品经理(以下简称 PM)天天在群里喊:“这个功能明天上线啊,老板很关注!” —— 仿佛我们前端是炼丹炉,点个火就能出成品。
这次事故的起因,说来有点尴尬:为了优化首屏加载速度,我们团队决定对首页做“模块懒加载 + 动态 import”。听起来很高级对吧?结果上线前测试没覆盖到弱网场景,某个依赖库在动态加载时因为 CDN 超时直接抛了 Uncaught SyntaxError: Unexpected token '<' —— 你懂的,那就是返回了 HTML 的 404 页面,被当 JS 解析了。
“兄弟,你们前端是不是又搞什么花活?”
—— 运维老哥在群里阴阳怪气。
我一边道歉一边排查,心里却在想:技术探索没错,但脱离产品场景和用户环境的“优化”,就是耍流氓。
产品驱动的技术决策,不是炫技
很多人觉得前端就是切页面、调样式,但实际上,前端是离用户最近的一环。你的每一行 JavaScript,最终都会在用户的手机或电脑上跑。而用户可不管你用了 Webpack 还是 Vite,他们只关心“能不能用”。
去年双 11 前,PM 提了个需求:首页商品列表要支持“无限滚动 + 实时价格更新”。乍一听很简单,但仔细一想,问题来了:
- 用户滑到底部时,如果网络慢,新数据没加载出来,体验很差;
- 实时价格要用 WebSocket,但低端机可能扛不住频繁 DOM 操作;
- 如果用户开着页面去干别的事,回来发现价格变了,会不会觉得被“偷改”了?
我们一开始想用 Intersection Observer + requestIdleCallback 来做懒加载和低优先级更新,代码写得那叫一个优雅。结果在测试机上一跑——低端安卓机直接卡成 PPT。原因?requestIdleCallback 在低端机上调度延迟高达 200ms+,根本达不到“实时”的效果。
最后我们妥协了:放弃纯前端方案,让后端在首次渲染时带上“预加载的下一页数据”,前端只做展示层的增量更新。虽然架构上不够“先进”,但线上 FPS 稳稳 60,用户投诉为零。
这件事让我明白:技术选型必须服务于产品目标,而不是反过来。 你写再牛的 JavaScript,如果用户打不开页面,那都是 0。
JavaScript 不是万能胶,但它真的粘
说到 JavaScript,这几年真是又爱又恨。ES6 之后,语法糖多到飞起,React/Vue 让开发效率翻倍。但与此同时,包体积膨胀、运行时开销、兼容性问题也成了家常便饭。
我们有个内部工具平台,早期为了快,直接上 Vue 2 + Element UI,打包出来 vendor.js 快 1.2MB。后来老板说“要提升用户体验”,我们就琢磨着优化。
第一步:拆包。用 Webpack 的 splitChunks 把第三方库单独抽出来,利用长期缓存。
// webpack.config.js
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
// 把 lodash、moment 这些大块头单独拆
utils: {
test: /[\\/]node_modules[\\/](lodash|moment)[\\/]/,
name: 'utils',
chunks: 'all',
}
}
}
}
第二步:动态 import。把非首屏模块按路由拆。
// router.js
const Home = () => import(/* webpackChunkName: "home" */ '@/views/Home.vue');
const Dashboard = () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue');
第三步:砍掉不用的代码。用 webpack-bundle-analyzer 一看,好家伙,moment.js 光 locale 文件就占了 300KB。换成 dayjs,体积直接砍到 2KB。
| 方案 | 首屏 JS 体积 | 首屏加载时间(3G 模拟) |
|---|---|---|
| 优化前 | 1.2MB | 4.8s |
| 优化后 | 420KB | 1.9s |
效果立竿见影。但你知道最讽刺的是啥吗?上线后 PM 跑来说:“用户好像没感觉到变快啊?”
我:……(内心 OS:你让用户拿秒表测加载时间吗?)
不过这也提醒了我:性能优化的价值,有时候需要通过产品指标来体现。比如我们后来在埋点里加了“首屏可交互时间”,发现转化率提升了 7%。这下 PM 才信了。
从“能跑就行”到“值得信赖”
刚入行那会儿,我觉得只要功能实现了,代码跑通了,就 OK。现在回头看,那叫“能跑就行主义”——典型的初级思维。
真正让我转变的,是一次线上内存泄漏事故。我们有个数据看板页面,用 ECharts 渲染大量图表。用户长时间不刷新,内存占用能飙到 1.5GB,最后浏览器直接崩掉。
查了一周,发现是事件监听没销毁:
// 错误示范:组件卸载后,resize 事件还在
mounted() {
window.addEventListener('resize', this.handleResize);
},
// 忘了在 beforeDestroy 里 removeEventListener...
这种 bug 在本地 dev 环境根本复现不了,只有真实用户长时间使用才会触发。那次事故后,我们团队定了一条铁律:所有涉及 DOM 操作、定时器、WebSocket 的代码,必须有对应的销毁逻辑。
现在我写组件,第一件事就是想:“它怎么死?”
// 正确姿势
export default {
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize);
this.resizeObserver.observe(this.$el);
},
beforeUnmount() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
}
看起来啰嗦,但这是对用户负责,也是对自己职业生涯负责。前端早已不是“切图仔”,而是系统稳定性的守门员。
技术探索的边界:别让自己变成“孤勇者”
最后想聊聊心态。
作为一线开发者,我们总想尝试新技术:Svelte、Qwik、Turbopack……看到 GitHub Trending 上的新项目,手就痒。但现实是:公司不是实验室,业务不能停摆。
去年我迷上 Svelte,觉得它的编译时优化太香了。偷偷在内部小工具里试了试,结果 CI 构建失败,因为我们的构建流水线不支持 .svelte 文件。折腾三天,最后还是换回 Vue。
不是技术不好,而是团队协作成本太高。你一个人爽了,测试不会测,运维不会部署,新人看不懂——这反而拖累了整体效率。
所以现在的策略是:在可控范围内做实验。比如用 Web Components 封装一个独立组件,不影响主框架;或者在非核心页面试用新工具链。成功了,再推广;失败了,损失也小。
写在最后
回到开头那次白屏事故。我们最终的解决方案是:
- 动态 import 加上
error boundary降级逻辑; - CDN 配置增加重试和 fallback 机制;
- 关键模块预加载,避免首屏依赖动态加载。
上线后,错误率归零。运维老哥发了个 👍,PM 说“辛苦了”,但我心里清楚:技术探索的路上,踩坑是常态,关键是别让坑变成坑队友的陷阱。
作为前端,我们写的不只是 JavaScript,更是产品的用户体验、系统的稳定性、团队的信任度。每一次 npm install,每一次 git push,背后都是真实的人在使用。
所以啊,别光顾着追新框架,多问问自己:这个改动,会让用户的生活变得更好一点吗?
如果答案是 yes,那就值得干。哪怕用 Vim 写到深夜,也值了。
(完)
P.S. 最近在看跳槽机会,阿里网易这边有前端岗的兄弟可以滴滴我。要求不高:给配机械键盘,允许用 Vim,PM 别半夜提需求就行 😅

评论 0