跳到主要内容

Node 与浏览器运行时

JavaScript 语言相同,宿主(Host)不同:浏览器给 DOM、网络、渲染;Node 给文件系统、进程、TCP。全栈写错宿主 API、把 window 带进 SSR、或在 Node 里当有 fetch 的旧版本上跑,都属于这一层。事件循环通识见 事件循环与任务队列;模块格式见 模块系统。

解决什么问题​

痛点分清宿主之后
「这段 JS 为啥在 Node 挂」用了 document / window / localStorage
前端包在服务端 import 报错代码路径没做环境拆分或 polyfill
内存只升不降闭包、全局缓存、未移除的监听,和宿主对象生命周期绑在一起
同一份工具函数两边用只依赖 globalThis 与语言特性,副作用放到适配层

一句话:语言是 ECMAScript;能做什么由运行时注入的全局对象决定。

全局对象​

globalThis.crypto; // 两边都有(足够新的 Node / 浏览器)
浏览器Node
全局别名window ≈ self ≈ globalThisglobal ≈ globalThis(没有 window)
入口用户事件、页面脚本node file.js / 长期进程
模块默认ESM(type="module")或打包结果CJS 或 ESM,见模块篇
退出关标签;页面走了运行时就没了事件循环空了才退出;服务器通常永不空

判断环境(不要用 UA 字符串当逻辑开关):

const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";

SSR / 同构时,这个判断要配合 打包条件导出(browser field / Vite import.meta.env.SSR),否则服务端仍可能打进 DOM 代码。

能力对照(够用即可)​

能力浏览器Node
DOM / CSSOM有无(除非 JSDOM 等测式替身)
fetch有Node 18+ 全局 fetch;更老用 undici / node-fetch
文件无任意路径 FS(有 File API / OPFS)node:fs
环境变量构建时注入(见 多环境发布)process.env 运行时可读
二进制ArrayBuffer / Uint8Array / Blob另有 Buffer(Uint8Array 子类)
多线程Web Workerworker_threads;另有 cluster 多进程
定时器setTimeout 等同名 + setImmediate / nextTick

Buffer 与前端 Uint8Array 能互相转,但 JSON / 日志里直接把 Buffer 当对象打,会看到 { type: "Buffer", data: [...] }。边界上显式 Buffer.from / Uint8Array。

内存与泄漏直觉​

JS 堆由引擎 GC。你留不住的对象会被收;你还指着的对象就不会。

模式典型现场
闭包抓住大对象回调没卸,一直闭包着整个请求上下文
全局 Map 当缓存没有上限 / TTL,见手写 LRU
监听未移除DOM addEventListener、Node EventEmitter、setInterval
脱离的 DOM节点从树上拿掉了,JS 还握着引用

浏览器多一个「页面关掉进程就没了」;Node 服务是长进程,泄漏会表现为 RSS 只涨、最终 OOM。排障先抓 heap snapshot,不要先加机器。

// 长进程里:有 on 就要想到 off
const onTick = () => {};
emitter.on("tick", onTick);
// 结束时
emitter.off("tick", onTick);
clearInterval(id);

全栈同构时怎么拆​

core/ 纯函数、类型、与宿主无关
browser/ DOM、路由、存储
node/ fs、进程、密钥(密钥绝不进前端产物)
  • 密钥、数据库 URL 只放 Node 侧
  • 前端需要的配置走「构建注入」或「后端下发」,不要把 .env 整文件打进 bundle
  • 不要在通用包里 import fs 再指望 Vite 摇掉——静态分析失败就会把 Node 内建带进浏览器

要点​

  1. 先问这段代码跑在哪个宿主,再写 API
  2. globalThis 是唯一可靠的跨环境全局
  3. Node 长进程把「忘记取消的订阅」放大成 OOM
  4. 同构包按 core / browser / node 切开,比满地 if (window) 稳
  5. Buffer、process、window 都是宿主礼物,不是语言关键字

面试速记​

  1. JS 和运行时什么关系? ECMAScript 定义语言;DOM / fs / process 由宿主提供。
  2. 为什么 SSR 会报 window is not defined? 首屏在 Node 里执行,没有浏览器全局。
  3. Node 为啥比页面更容易内存泄漏? 进程不退出,全局缓存和监听会一直活。
  4. Worker 和 setTimeout 差在哪? Worker 是真的另一个线程/消息边界;setTimeout 仍在同一 JS 线程排队。
  5. 前端能读 process.env.SECRET 吗? 若打包器内联了就能进产物——等于泄露。密钥只留服务端。