技术探索与实践的一些思考:从一次差点背锅的线上事故说起

Rust练习生
2025-12-18 20:21
阅读 3311

上周五晚上十点半,我正用 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 封装一个独立组件,不影响主框架;或者在非核心页面试用新工具链。成功了,再推广;失败了,损失也小。


写在最后

回到开头那次白屏事故。我们最终的解决方案是:

  1. 动态 import 加上 error boundary 降级逻辑;
  2. CDN 配置增加重试和 fallback 机制;
  3. 关键模块预加载,避免首屏依赖动态加载。

上线后,错误率归零。运维老哥发了个 👍,PM 说“辛苦了”,但我心里清楚:技术探索的路上,踩坑是常态,关键是别让坑变成坑队友的陷阱。

作为前端,我们写的不只是 JavaScript,更是产品的用户体验、系统的稳定性、团队的信任度。每一次 npm install,每一次 git push,背后都是真实的人在使用。

所以啊,别光顾着追新框架,多问问自己:这个改动,会让用户的生活变得更好一点吗?

如果答案是 yes,那就值得干。哪怕用 Vim 写到深夜,也值了。

(完)


P.S. 最近在看跳槽机会,阿里网易这边有前端岗的兄弟可以滴滴我。要求不高:给配机械键盘,允许用 Vim,PM 别半夜提需求就行 😅

评论 0

最热最新
暂无评论
Rust练习生Lv.1
0
影响力
0
文章
0
粉丝