一个“全局变量”引发的线上事故

你正在给一家 SaaS 客户做「企业级仪表盘」项目。

需求很简单:把 20 多个子系统的实时指标汇总到一张大屏。

为了"图省事",你在入口文件里顺手写了这么一段:

js 复制代码
// main.js
window.dashboard = {};                 // 🔍 全局缓存
window.dashboard.cache = new Map();    // 🔍 存接口数据
window.dashboard.refresh = () => {     // 🔍 供子系统调用
  fetch('/api/metrics').then(r => r.json())
    .then(data => window.dashboard.cache = new Map(data));
};

上线两周后,客户反馈:

"切到别的标签页再回来,大屏偶尔会白屏,刷新又好了。"

排查发现,Chrome 在内存紧张时会 回收后台标签页的 JS 堆 ,但 window 对象属于宿主环境,不会被回收

结果:

  • 旧的 dashboard 引用还在,但闭包里的 Map 被 GC 清掉,变成空壳;
  • 子系统继续调用 window.dashboard.refresh,拿到的永远是空数据;
  • 页面逻辑崩溃,白屏。

解决方案:三层防护,把风险降到 0

1. 表面用法:用 WeakMap 做"软引用"

js 复制代码
// 把全局缓存改成弱引用
const cache = new WeakMap();           // 🔍 不阻止 GC
window.dashboard = {
  get data() { return cache.get(document); }, // 🔍 与当前文档生命周期绑定
  set data(v) { cache.set(document, v); }
};

这样当用户切走标签页、文档被卸载时,cache 自动释放,切回来重新初始化,白屏消失。

2. 底层机制:为什么 window 不会被 GC?

层级 角色 生命周期 是否可被 GC
JS 堆 闭包、普通对象 无引用即回收
Host 环境 window 与标签页同生同灭
渲染进程 GPU 纹理、DOM 标签页关闭后统一清理

结论:window 是宿主对象,生命周期长于 JS 堆,挂上去的东西如果强引用 JS 对象,会导致"僵尸引用"。

3. 设计哲学:把"全局"变成"注入"

与其在 window 上挂变量,不如用 依赖注入 把作用域限制在模块内部:

js 复制代码
// dashboard.js
export const createDashboard = () => {
  const cache = new Map();             // 🔍 私有状态
  return {
    refresh: () => fetch('/api/metrics')
      .then(r => r.json())
      .then(data => cache.clear() && data.forEach(d => cache.set(d.id, d)))
  };
};
js 复制代码
// main.js
import { createDashboard } from './dashboard.js';
const dashboard = createDashboard();   // 🔍 生命周期由模块控制

应用扩展:可复用的配置片段

如果你的项目必须暴露全局 API(比如给第三方脚本调用),可以用 命名空间 + Symbol 双保险:

js 复制代码
// global-api.js
const NAMESPACE = Symbol.for('__corp_dashboard__');
window[NAMESPACE] = {                  // 🔍 避免命名冲突
  version: '2.1.0',
  createDashboard
};

环境适配说明:

  • 浏览器:Symbol 在 IE11 需 polyfill;
  • Node:同构渲染时 window 不存在,用 globalThis[NAMESPACE] 兜底。

举一反三:3 个变体场景

  1. 微前端基座
    window.__MICRO_APP_STORE__ 传递状态,但内部用 Proxy 做读写拦截,防止子应用直接篡改。
  2. Chrome 插件 content-script
    通过 window.postMessage 与页面通信,避免污染宿主全局。
  3. Electron 主进程/渲染进程
    contextBridge.exposeInMainWorld('api', safeApi) 把 API 注入到隔离上下文,防止 Node 权限泄露。

小结:

window 当"公告栏"可以,但别把"保险箱"也挂上去。

用弱引用、模块化和依赖注入,才能把全局变量的风险真正关进笼子里。

相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万2 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋2 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁2 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95272 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大2 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师2 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学2 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端