跨端架构终局总结
# 16.跨端架构终局
📍 上接第 15 篇《设计模式与函数式哲学》。代码怎么写已经优雅了。但你的 JS 不只跑在浏览器——桌面端(Electron)、移动端(React Native)、小程序、PWA、WASM,五条路摆在面前。本文回答:什么时候该选谁?
# 目录介绍
- 1. 案例与疑问引入
- 2. 架构全景概览
- 3. Electron
- 4. RN新旧架构
- 5. 小程序双线程
- 6. PWA离线缓存
- 7. WebAssem
- 8. WASI 与 S
- 9. 跨端决策矩阵
- 10. 全系列回顾总结
# 1. 案例与疑问引入
# 1.1 同一个业务,产品
某个周三下午,产品经理拉你进会议室:
"咱们的在线文档协作编辑器,下季度要上:Web 版(已有)、桌面端(离线编辑)、移动端 App(iOS + Android)、微信小程序(方便分享)。技术方案你来定,周五给我技术评审。"
你的团队:3 个前端 + 1 个 C++ 后端。你的技术栈:React + TypeScript。你的时间:Q3 上线,3 个月。
现在坐下来,你打开一个空白 Notion:
| 方案 | Web | 桌面 | 移动 App | 小程序 | 人力 | 工期 |
|---|---|---|---|---|---|---|
| 纯原生 | ✅ | ✅ (C++) | ✅ (Swift+Kotlin) | ✅ | 15人+ | 2年 |
| Flutter | ✅ (Web 弱) | ✅ | ✅ | ❌ | 5人 | 1年 |
| Electron + RN | ✅ | ✅ | ✅ (RN) | ❌ | 5人 | 9个月 |
| Electron + RN + Taro | ✅ | ✅ | ✅ | ✅ | 4人 | 6个月 |
第一直觉是"全上 Electron + RN + Taro 一把梭"——但这个方案真的最优吗?Electron 桌面端动辄 150 MB 安装包,用户买账吗?RN 的 FlatList 滚到 1000 条会不会白屏?小程序的双线程模型对编辑器这种高频交互场景意味着什么?
# 1.2 顺藤摸到根因
每条路线背后都不是"API 怎么调"的表面问题,而是 JS 运行时在什么宿主里、通过什么桥接与平台交互、代价是什么 的架构级决策:
- 假设 1:"Electron 本质是 Chromium,所以和 Web 一样快"——但 Electron 的冷启动是 Chromium fork + Node.js 初始化 + V8 初始化,首屏比 Chrome 慢 2~3 倍。
- 假设 2:"RN 共享一套代码,开发体验和 Web 一样"——但 RN(旧架构)JS ↔ Native 走 JSON 序列化桥,滚动列表每帧都要跨桥通信,这就是"白屏"的根因。
- 假设 3:"小程序就是跑在 WebView 里的 H5"——但微信小程序是双线程模型,逻辑层在独立 JS 沙箱,渲染层在 WebView,两者通过
setData传数据,本质上是序列化的远程过程调用。 - 假设 4:"WASM 可以替代 JS 做所有事"——但 WASM 不能操作 DOM,不能调 Web API,每个 JS ↔ WASM 调用都有边界开销。
- 假设 5:"PWA 就是加个 manifest.json"——但 Service Worker 的生命周期、缓存失效策略、iOS Safari 对 PWA 的功能阉割,每一项都能让离线体验变成灾难。
这五个假设背后藏着至少 10 个原理点:
① Electron 的进程模型:主进程 vs 渲染进程如何分工?IPC 为什么不能传大对象?
② RN 旧架构的 Bridge 瓶颈:为什么每帧跨桥 30ms 就必然掉帧?
③ 小程序双线程的本质代价:setData 序列化 1MB → 渲染层反序列化 → 总耗时 80ms
④ Service Worker 缓存命中逻辑:Cache-First 拿到旧版本的坑
⑤ WASM 的共享内存:SharedArrayBuffer 为什么需要 COOP/COEP 头?
⑥ 小程序 Skyline 引擎:它怎么绕过了 WebView 的渲染管线?
⑦ Hermes 引擎不做 JIT 为什么反而更适合移动端?
⑧ Tauri 用 Rust 后端为什么能比 Electron 小 90% 体积?
⑨ PWA 在 iOS 上的能力边界:Push Notification 到底支不支持?
⑩ WASI 怎么让 WASM 脱离浏览器跑在服务端?
2
3
4
5
6
7
8
9
10
# 1.3 我们要回答什么
这个会议室场景就是本篇的主线案例。我们带着上面 10 个问号往下走,每讲完一种方案就解开对应的原理;最后在第 9 章用七维打分模型量化评估所有方案,给出四个典型场景的最优推荐。
本篇路线:
架构全景 (第 2 章) ─→ 每种方案「JS 跑在哪」
↓
Electron (第 3 章) → 桌面端为什么慢、怎么安全
↓
React Native (第 4 章) → Bridge 到 JSI 的架构革命
↓
小程序 (第 5 章) → 双线程模型的约束与 Skyline 的突破
↓
PWA (第 6 章) → 离线不是加个 manifest 那么简单
↓
WASM + WASI (第 7-8 章) → 浏览器里的第二语言、服务端的新容器
↓
决策树 (第 9 章) → 七维打分 + 四场景推荐
↓
全系列总结 (第 10 章) → 16 篇知识回扣
2
3
4
5
6
7
8
9
10
11
12
13
14
15
📌 本篇定位:这是整个 JavaScript 专栏的终局篇。前 15 篇把 V8 引擎、作用域、事件循环、模块系统、设计模式掰开了讲——本篇回答:你的 JS 代码最终交付到哪、怎么跑。读完本篇后,面对任何"我的 JS 要跑在 X 端"的需求,都能立刻回答:"走哪条路线、代价是什么"。
# 2. 架构全景概览
# 2.1 跨端方案全景图
先把七种主流方案放进一张全景图:
┌─── Native UI ───┐
│ (Swift/Kotlin) │
└────────┬─────────┘
│ 全原生方案
┌──────────────────┼──────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌─────▼─────┐
│ Flutter │ │ React Native│ │ 原生小程序│
│(Skia渲染) │ │ (JS Bridge) │ │(双线程模型)│
└─────┬─────┘ └──────┬──────┘ └─────┬─────┘
│ │ │
┌────────▼────────┬─────────▼─────────┬────────▼────────┐
│ │ │ │
┌────▼────┐ ┌───────▼───────┐ ┌──────▼───────┐ ┌───────▼──────┐
│Electron │ │ PWA │ │ Web App │ │ WASM │
│(Chromium│ │(ServiceWorker)│ │(浏览器原生) │ │(浏览器内第二 │
│ +Node) │ │ │ │ │ │ 虚拟环境) │
└─────────┘ └───────────────┘ └──────────────┘ └──────────────┘
│ │ │ │
└─────────────────┴───────────────────┴──────────────────┘
│
【所有方案最终都依赖某种 JS 引擎 / WebView】
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
七大方案的能力矩阵:
| 方案 | 平台覆盖 | JS 代码复用率 | 安装包体积 | 冷启动速度 | 原生API访问 | 离线能力 | 生态成熟度 |
|---|---|---|---|---|---|---|---|
| Web | 全部(有浏览器) | 100% | 0 | ⭐⭐⭐⭐⭐ | 有限 | ServiceWorker | ⭐⭐⭐⭐⭐ |
| PWA | 桌面+Android iOS(阉割) | 100% | 0 | ⭐⭐⭐⭐⭐ | 少量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Electron | 桌面 Win/Mac/Linux | ~90% | 150~250 MB | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| React Native | iOS + Android | ~70% | 15~30 MB | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Flutter | iOS+Android+Web+桌面 | 0% (Dart) | 15~25 MB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 小程序 | 微信生态 | ~80% (Taro/uni-app) | 2 MB 限制 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| WASM | 浏览器内 | 0% (编译产物) | 内嵌 | ⭐⭐⭐⭐⭐ | 无(需JS桥接) | 同Web | ⭐⭐⭐ |
# 2.2 每种方案的「JS
疑惑:"Electron 里 JS 跑在 V8,小程序里 JS 跑在 JSC,RN 里 JS 跑在 Hermes——它们不都是 JS 引擎吗,有什么区别?"
论证:
┌─────────────────────────────────────────────────────────┐
│ 你的 JS 代码 │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────┼───────────────┬───────────────┐
│ │ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ 浏览器 │ │ Electron │ │ React Native│ │ 微信小程序 │
│ V8/JSC/Spider│ │ V8 (主进程) │ │ Hermes/JSC │ │ V8/JSC │
│ Monkey │ │ V8 (渲染) │ │ (独立线程) │ │ (逻辑层沙箱)│
│ ─────────── │ │ ────────── │ │ ────────── │ │ ────────── │
│ DOM ✓ │ │ DOM ✓ │ │ DOM ✗ │ │ DOM ✗ │
│ Web API ✓ │ │ Node API ✓ │ │ Native API ✓│ │ wx API ✓ │
│ 文件系统 ✗ │ │ 文件系统 ✓ │ │ 文件系统 ✓ │ │ 文件系统有限│
└──────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
核心区别不是"哪个 V8 版本",而是 JS 引擎旁边的宿主环境提供了什么 API:
| 宿主 | JS 引擎 | 全局对象 | DOM | 网络 | 文件 | 原生UI | 多线程 |
|---|---|---|---|---|---|---|---|
| 浏览器 | V8/JSC | window | ✅ | fetch/XHR | ❌ | ❌ | Worker |
| Electron 主进程 | V8 | global | ❌ | http 模块 | ✅ fs | ❌ | worker_threads |
| Electron 渲染进程 | V8 | window | ✅ | fetch | ❌ (需IPC) | ❌ | Worker |
| RN (旧) | JSC | global | ❌ | fetch | ❌ (需Bridge) | ✅ Bridge | ❌ |
| RN (新) | Hermes/JSC | global | ❌ | fetch | ❌ (需JSI) | ✅ JSI | ❌ |
| 小程序逻辑层 | V8/JSC | global | ❌ | wx.request | wx.getFileSystemManager | ❌ | ❌ |
| 小程序渲染层 | WebView | window | ✅ | ❌ (需 setData) | ❌ | ❌ | ❌ |
结论:跨端的本质不是"JS 代码怎么改",而是 JS 运行时被嵌入了什么宿主环境、宿主给了哪些能力、桥接代价有多大。下面我们逐个剖开每种宿主。
# 3. Electron
# 3.1 主进程(Node
Electron 把 Chromium 和 Node.js 缝在了一起——但用进程隔离保证它们不互相搞死:
┌────────────────────────────────────────────────────────┐
│ Electron App │
│ │
│ ┌──────────────────┐ IPC ┌─────────────────┐ │
│ │ Main Process │◄────────────►│ Renderer Process │ │
│ │ (Node.js) │ │ (Chromium) │ │
│ │ │ │ │ │
│ │ • app 生命周期 │ │ • HTML/CSS/JS │ │
│ │ • BrowserWindow │ │ • DOM 操作 │ │
│ │ • 原生菜单/托盘 │ │ • Web API │ │
│ │ • 文件系统(fs) │ │ • <webview> │ │
│ │ • 系统通知 │ │ │ │
│ │ • 自动更新 │ │ │ │
│ └──────────────────┘ └─────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
关键设计:
主进程是门卫——所有涉及操作系统的事必须通过主进程。渲染进程不知道文件在哪、不知道托盘图标怎么画、不知道系统通知怎么发。它只能通过 IPC 发消息:"主进程,帮我读
/Users/xxx/config.json"。IPC 走结构化克隆——
ipcRenderer.send('read-file', '/path')和ipcMain.handle('read-file', ...)之间传的数据经过结构化克隆(structuredClone),这意味着:Buffer、Map、Set可以通过- 函数、DOM 节点会报错
- 大对象(>10 MB)序列化耗时显著
// 渲染进程 (renderer.js) —— 不能直接调 fs
const { ipcRenderer } = require('electron');
// ❌ 渲染进程不能直接调 Node API
// const fs = require('fs'); // 默认开启 sandbox 后报错
// ✅ 通过 IPC 请求主进程
const content = await ipcRenderer.invoke('read-file', '/Users/xxx/config.json');
// ===========================================
// 主进程 (main.js)
const { ipcMain } = require('electron');
const fs = require('fs');
ipcMain.handle('read-file', async (event, filePath) => {
// 安全校验:防止路径穿越攻击
const allowed = path.resolve('/Users/xxx/data/');
const requested = path.resolve(filePath);
if (!requested.startsWith(allowed)) {
throw new Error('Path traversal detected');
}
return fs.readFileSync(requested, 'utf-8');
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.2 为什么 Elec
疑惑:VS Code 启动要 3 秒,记事本启动只要 0.5 秒——同样的 Electron 凭什么慢?
论证:Electron 冷启动的完整时间线(实测:Visual Studio Code 冷启动 ~3000ms):
时间轴 (ms)
0 ┌─ execve("Code") ─────────────────────
│
50 ├─ 动态链接器加载 ld-linux.so
│
150 ├─ 加载 libnode.so(Node.js 运行时)
│ ├── V8 初始化:Isolate 创建 + 内置对象初始化 ~80ms
│ ├── libuv 事件循环初始化 ~10ms
│ └── Node.js 内置模块注册(fs/http/buffer...) ~30ms
│
400 ├─ Chromium 预初始化
│ ├── 资源包 (.pak) 解压 + 加载 ~100ms
│ ├── Skia 图形库初始化 ~50ms
│ └── IPC 通道建立 ~30ms
│
800 ├─ BrowserWindow 创建
│ ├── GPU 进程 fork ~200ms
│ ├── 渲染进程 fork + V8 第二个 Isolate ~150ms
│ └── 渲染进程加载 HTML/CSS/JS ~400ms
│
1800 ├─ JS 执行:渲染进程 main script
│ ├── React/Angular 框架初始化 ~300ms
│ ├── 首屏数据请求 (IPC → 主进程 → 磁盘) ~400ms
│ └── 首帧渲染 ~200ms
│
3000 └─ 首屏可见 ─────────────────────────────────────
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
每条耗时的根因:
| 阶段 | 耗时 (ms) | 根因 | 是否可优化 |
|---|---|---|---|
| V8 冷启动 | ~80 | Isolate 创建必须初始化所有内置对象(数组/String/Map/Proxy...) | 极难 |
| Chromium .pak 加载 | ~100 | 资源文件解压是 I/O 密集操作 | SSD 可加速 |
| GPU 进程 fork | ~200 | GPU 进程独立是为了渲染稳定性(崩一个 tab 不影响其他) | 可禁用 GPU sandbox(不推荐) |
| 渲染进程 JS 启动 | ~300~700 | 框架初始化 + 首屏请求链 | 可优化空间最大 |
| 首屏网络/IO | ~400 | IPC → 主进程 → 磁盘 | 预取 + 本地缓存 |
优化的关键抓手:
// main.js —— 主进程端优化
const { app, BrowserWindow } = require('electron');
// 1. 提前创建窗口(不等 ready 事件之后才开始)
app.whenReady().then(() => {
// 先展示一个骨架屏 BrowserWindow(无 JS 的纯 HTML)
const splash = new BrowserWindow({
width: 800, height: 600,
frame: false,
webPreferences: { sandbox: true }
});
splash.loadFile('splash.html'); // 纯静态页面,~50ms 可显示
// 2. 后台加载主窗口
const mainWin = new BrowserWindow({
width: 1200, height: 800,
show: false, // 先不显示
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true
}
});
mainWin.loadURL('http://localhost:3000');
// 3. 主窗口 ready 后,替换骨架屏
mainWin.once('ready-to-show', () => {
splash.close();
mainWin.show(); // 用户看到的是完整界面,跳过了白屏
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
结论:Electron 启动慢的根本原因是它要塞进一个完整的 Web 浏览器引擎——包括 V8、Blink、Skia、GPU 进程——这些组件是为"通用浏览器"设计的,不是为"你的单个应用"。骨架屏 + 懒加载是最有效的优化手段,但冷启动 1 秒以下是几乎不可能的目标。
# 3.3 preload
Electron 最大的安全风险:渲染进程如果能直接调 Node.js API,任何一个 XSS 漏洞都能执行系统命令。
没有隔离的 Electron (❌ 不安全):
┌─────────────────────────────────┐
│ 渲染进程 (sandbox: false) │
│ │
│ <script> │
│ // XSS 进来了 │
│ require('child_process') │
│ .exec('rm -rf /'); │ ← 💀 直接操作系统
│ </script> │
└─────────────────────────────────┘
有隔离的 Electron (✅ 安全):
┌──────────────────────────────┐ ┌──────────────────────┐
│ 渲染进程 (sandbox: true) │ │ preload.js │
│ │ │ (沙箱中的安全通道) │
│ <script> │ │ │
│ // XSS 进来了 │ │ contextBridge │
│ require('child_process') │ │ .exposeInMainWorld( │
│ // → ReferenceError: │ │ 'electronAPI', │
│ // require is not def. │ │ { │
│ </script> │ │ readFile: (p) │
│ │ │ → IPC 到主进程 │
│ ✅ 可以安全调: │ │ } │
│ window.electronAPI │◄───┤ ); │
│ .readFile('/safe/path') │ │ │
└──────────────────────────────┘ └──────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
preload.js 的标准写法:
// preload.js —— 运行在渲染进程内,但有关闭的 Node 环境
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// 只暴露有限的安全 API,不暴露整个 ipcRenderer
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
saveFile: (filePath, content) => ipcRenderer.invoke('save-file', filePath, content),
onMenuAction: (callback) => {
// 手动管理监听器,防止内存泄漏
const handler = (_event, action) => callback(action);
ipcRenderer.on('menu-action', handler);
// 返回清理函数
return () => ipcRenderer.removeListener('menu-action', handler);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contextBridge 的三条安全规则:
- 函数不能传引用——
exposeInMainWorld暴露的每个属性都经过structuredClone,渲染进程 JS 拿不到 preload 里的原始引用。 - 不支持原型链——暴露的对象是"平面"的,没有
__proto__污染路径。 - 不暴露 EventEmitter——
ipcRenderer.on不直接暴露,封装在有清理能力的函数里。
# 3.4 Electron
疑惑:"Tauri 是 Rust 写的,比 Electron 小、快、安全——那为什么不直接全换 Tauri?"
论证:Electron 和 Tauri 的核心架构差异:
Electron: Tauri:
┌─────────────────────┐ ┌──────────────────────┐
│ 你的前端代码 │ │ 你的前端代码 │
│ (React/Vue/Svelte) │ │ (React/Vue/Svelte) │
├─────────────────────┤ ├──────────────────────┤
│ Chromium 渲染引擎 │ │ 系统原生 WebView │
│ 150 MB+ │ │ 0 MB (系统自带) │
├─────────────────────┤ ├──────────────────────┤
│ Node.js 后端 │ │ Rust 后端 │
│ (V8 + libuv) │ │ (~5 MB 编译产物) │
│ 50 MB+ │ │ │
└─────────────────────┘ └──────────────────────┘
安装包: 150~250 MB 安装包: 5~15 MB
内存占用: 200~500 MB 内存占用: 50~150 MB
2
3
4
5
6
7
8
9
10
11
12
13
14
六维对比:
| 维度 | Electron | Tauri | 选谁 |
|---|---|---|---|
| 安装包大小 | 150~250 MB | 5~15 MB | Tauri 碾压 |
| 内存占用 | 200~500 MB | 50~150 MB | Tauri 碾压 |
| 冷启动速度 | 1.5~3s | 0.3~0.8s | Tauri 碾压 |
| Web 兼容性 | Chromium 最新版,100% Web API | 系统 WebView(macOS=Safari, Win=Edge WebView2, Linux=WebKitGTK) | Electron 碾压 |
| 后端生态 | npm 全生态 | Crates.io + 学 Rust 成本 | Electron 碾压 |
| 跨平台一致性 | 100% 一致(自带 Chromium) | 不同 OS WebView 渲染差异 | Electron 碾压 |
| 安全性 | 需手动 sandbox + preload | 默认最小权限 + Rust 内存安全 | Tauri 略优 |
决策公式:
如果你的用户:
├── 对安装包大小敏感(如海外用户、移动网络) → Tauri
├── 需要极致 Web 体验(如复杂富文本编辑器) → Electron
├── 需要 npm 生态 + 第三方 SDK(如 Agora/声网) → Electron
├── 团队有 Rust 经验 + 内存敏感(如监控面板) → Tauri
└── 团队前端为主、想快速交付 → Electron
2
3
4
5
6
结论:Tauri 不是 Electron 的全面替代品,两者是不同取舍——Electron 追求"Web 能力最大化 + 跨平台一致性",Tauri 追求"体积最小化 + 资源效率"。目前在富交互桌面应用领域,Electron 仍是更安全的选择;Tauri 在工具类、辅助类轻量桌面应用上优势明显。
# 4. RN新旧架构
# 4.1 JS Bridge
RN 旧架构的核心是 JS Bridge——一条异步、批量、单工的消息通道:
旧架构 (RN < 0.68):
┌─────────────────┐ ┌──────────────────┐
│ JS 线程 │ Bridge │ Native 线程 │
│ (JavaScriptCore)│◄──────────────────►│ (Main Thread) │
│ │ JSON 序列化 │ │
│ React 协调 │ ───────────────► │ UIKit/Android │
│ Diff + 渲染指令 │ │ View 创建/更新 │
│ │ ◄─────────────── │ │
│ │ 回调 / 事件 │ 触摸/滚动事件 │
└─────────────────┘ └──────────────────┘
所有通信走这一条队列
2
3
4
5
6
7
8
9
10
11
白屏的根因:滚动列表场景下的 Bridge 瓶颈:
// FlatList 滚动时,每一帧 (16.67ms) 要完成:
// 1. 触摸事件 Native → Bridge → JS (~5ms 序列化 + 传输)
// 2. JS VirtualizedList 计算可见行 (~3ms)
// 3. 渲染指令 JS → Bridge → Native (~8ms 序列化 + 执行)
// 4. Native 创建/回收 View (~5ms)
// ==============================================
// 总计:~21ms > 16.67ms (一帧时间)
// → 掉帧 → 用户看到 "白屏"(新行还没渲染出来)
2
3
4
5
6
7
8
时序图的本质——三线程各自忙什么:
时间 →
主线程(Native): ───[渲染帧1]───[渲染帧2]───[渲染帧3]───
↑ ↑
│ 等 JS 结果 │ 等 JS 结果
│ │
JS 线程: ─────[React Diff]──[计算可见行]───
↑
│ 等触摸事件
│
Shadow 线程: ─────[Yoga 布局计算]──────────
(RN 的布局引擎,用 C++ 写)
2
3
4
5
6
7
8
9
10
11
关键数字:
| 操作 | JSON 序列化 | Bridge 排队 | Native 执行 | 总延迟 |
|---|---|---|---|---|
setState({ text: 'hello' }) | 0.05ms | 0~2ms | 0.3ms | 0.5~2.5ms |
| FlatList 滚动一帧 | 2ms | 0~5ms | 3ms | 5~15ms |
| 复杂表单 setState | 3ms | 5~10ms | 5ms | 10~20ms |
| JS 线程阻塞(如 GC) | — | 卡住所有通信 | — | 100~500ms |
结论:旧架构 RN 的 JS Bridge 把"JS 世界"和"Native 世界"强行隔离,好处是安全,代价是每次交互都要付 JSON 序列化 + Bridge 排队 + 线程切换的三重税。对于"点击按钮改个文字",这没感觉;对于"60fps 滚动 100 条消息",这是致命的。
# 4.2 JSI + Fa
RN 新架构(0.68+)用三个新组件彻底重构了通信模型:
新架构 (RN ≥ 0.68):
┌────────────────────────────────────────────────────┐
│ JS 线程 │
│ ┌──────────────┐ ┌──────────────────────────────┐│
│ │ React 18 │ │ JSI (JavaScript Interface) ││
│ │ 并发渲染 │ │ ─ C++ 层直接暴露给 JS ─ ││
│ └──────┬───────┘ │ Native 函数 → JS 可直接调 ││
│ │ │ JS 对象 → Native 可直接持有 ││
│ ┌──────▼───────┐ └──────────────┬───────────────┘│
│ │ Fabric │ │ │
│ │ (新渲染器) │◄────────────────┘ │
│ │ ────────── │ 同步调用,不再排队 │
│ │ Shadow Tree │ │
│ │ 直接在 C++ │ │
│ │ 层管理 │ │
│ └──────┬───────┘ │
└─────────┼───────────────────────────────────────────┘
│ 共享 C++ 对象
┌─────────▼───────────────────────────────────────────┐
│ Native 线程 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ TurboModules│ │ Fabric │ │
│ │ 按需加载 │ │ 原生渲染 │ │
│ │ 原生模块 │ │ 管线 │ │
│ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
三大组件的核心突破:
① JSI(JavaScript Interface)——废除 Bridge:
旧架构每次 JS 调 Native 都序列化成 JSON → 排队 → 反序列化。JSI 直接在 C++ 层暴露 Native 函数的引用给 JS 引擎,JS 调它就是 C++ 函数调用。
// JSI 核心概念(简化)
// C++ 侧注册一个 Native 函数
jsi::Runtime& runtime = /* Hermes/JSC 的 C++ Runtime */;
auto nativeMultiply = jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "nativeMultiply"),
2, // 2 个参数
[](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) {
double a = args[0].getNumber();
double b = args[1].getNumber();
return jsi::Value(a * b); // C++ 直接返回,无序列化
}
);
// 注入到 JS 全局对象
runtime.global().setProperty(runtime, "nativeMultiply", std::move(nativeMultiply));
// JS 侧直接同步调用(不再是异步消息)
// const result = global.nativeMultiply(3, 4); // 立即返回 12
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
② Fabric(新渲染器)——Shadow Tree 直接管:
旧架构中,React 在 JS 线程算出 Virtual DOM diff,序列化后发给 Shadow 线程做 Yoga 布局,再发给主线程渲染——三个线程来回倒。Fabric 把 Shadow Tree 放在 C++ 层,JS 线程和主线程共享同一棵树:
旧架构:
JS线程 ─JSON─► Shadow线程 ─JSON─► 主线程 ← 3 段序列化
新架构:
JS线程 ──JSI──► Fabric C++ Shadow Tree ◄──JSI── 主线程
(共享数据结构,无序列化)
2
3
4
5
6
③ TurboModules——按需加载原生模块:
旧架构在启动时把所有 Native Module(Camera、Push Notification、Bluetooth...)全部注册到 JS,即使这个页面根本不用相机。TurboModules 在 JS 第一次 require('NativeCamera') 时才去加载对应的原生模块——启动时间缩短 30~50%。
新架构的性能对比:
| 场景 | 旧架构延迟 | 新架构延迟 | 提升 |
|---|---|---|---|
setState 文本变化 | 2ms | 0.3ms | 6倍 |
| FlatList 滚动(60fps) | 不可达(45fps) | 稳定 60fps | 从不可用到可用 |
| 复杂表单输入 | 20ms | 3ms | 6倍 |
| 启动时间 | 800ms | 400ms | 2倍 |
# 4.3 Hermes 引擎
疑惑:"V8 有 JIT 编译器,跑得快。Hermes 不做 JIT,凭什么更快?"
论证:这个问题的关键是移动端场景和桌面端完全不同:
桌面端 V8:
启动 → 解析 → Ignition 解释执行 → 热点检测 → TurboFan JIT 编译 → 高速执行
└── 1-3 秒后 ────────────── 5-30 秒后 ──────────────→ 极快
移动端 (App 使用场景):
启动 → 1 秒内看到首屏 → 用户翻 2-3 页 → 切走 → 15 分钟后回来 → 可能被杀掉
└── JIT 编译还没生效,应用就进后台了 ──────────────→ 每次都冷启动!
2
3
4
5
6
7
Hermes 的策略完全不同——AOT(Ahead-of-Time)编译:
Hermes:
构建时(你的 Mac 上) 运行时(用户手机上)
┌─────────────────────┐ ┌──────────────────────┐
│ JS 源码 │ │ 字节码 (.hbc) │
│ ↓ │ │ ↓ │
│ Hermes 编译器 │ │ Hermes 字节码解释器 │
│ → 优化 IR │ │ → 直接从字节码执行 │
│ → 字节码生成 │ │ 无 Parse 阶段 │
│ ↓ │ │ 无 JIT 预热 │
│ 文件: index.hbc │──────────► 内存占用: ~50% of JSC │
└─────────────────────┘ └──────────────────────┘
2
3
4
5
6
7
8
9
10
11
Hermes 的三板斧:
① 无 JIT → 内存砍半:
JIT 编译器需要分配"可执行内存页"来存放编译后的机器码。在 iOS 上,可执行内存页有严格限制,且不能与 Android 共享。V8 的 JIT 在移动端吃掉 ~30MB 额外内存,Hermes 直接省掉这一块:
| 引擎 | Parse 时间 | 字节码/编译模式 | 内存占用 | 启动时间 |
|---|---|---|---|---|
| JSC (旧 RN) | ~200ms | 解释执行 + JIT | ~40 MB | 800ms |
| V8 (少用) | ~150ms | 解释 + 多层 JIT | ~50 MB | 900ms |
| Hermes | 0ms (AOT) | 字节码解释器 | ~20 MB | 400ms |
② 字节码直接下发:
# 构建时:JS → HBC (Hermes Bytecode)
$ hermesc -emit-binary -out index.hbc index.js
# 运行时:直接加载字节码,跳过 Parse
# 传统 JSC: 读 JS → 词法分析 → 语法分析 → AST → 字节码 → 执行
# Hermes: 读 .hbc → 执行(词法/语法/AST 三步全免)
2
3
4
5
6
③ 移动端定制的 GC:
Hermes 使用 分代 + 增量标记-清除 GC,专门为移动端的内存压力做了优化——不追求极致吞吐量(JSC 的并发 GC),而是追求低内存峰值 + 可预测的暂停时间。
结论:Hermes 不做 JIT 不是"做不了",而是"在移动端场景下不做更好"。移动 App 的生命周期太短,JIT 预热成本收不回来;而提前做 AOT 编译,虽然单次执行速度比 JIT 慢 20~30%,但从"打开 App 到首屏可见"的总时间缩短了 50%,这在移动端是更关键的指标。
# 5. 小程序双线程
# 5.1 逻辑层(JS
微信小程序最核心的架构决策:把 JS 执行和页面渲染彻底分开:
┌──────────────────────────────────────────────────────┐
│ 微信客户端 │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 逻辑层 (AppService) │ │ 渲染层 (WebView) │ │
│ │ │ │ │ │
│ │ • JS 沙箱 (JSC/V8) │ │ • 多个 WebView │ │
│ │ • 无 DOM │ │ • 每个页面一个 │ │
│ │ • 无 window │ │ • 有 window/DOM │ │
│ │ • wx API 可用 │ │ • 无 wx API │ │
│ │ │ │ │ │
│ │ Page({ │ │ <view>...</view> │ │
│ │ data: { msg } │ │ │ │
│ │ onLoad() { │ │ 渲染 WXML 模板 │ │
│ │ this.setData( │ │ │ │
│ │ { msg:'hi' } │ │ │ │
│ │ ) ─────────────► 更新对应的 DOM 节点 │ │
│ │ } │ │ │ │
│ │ }) │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │ ▲ │
│ │ Native 层 序列化/转发 │ │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
为什么不能直接操作 DOM?——两条设计考量:
安全:如果逻辑层能直接操 DOM,就意味着小程序可以执行任意 JS——注入广告、窃取用户数据、XSS 攻击。沙箱把 JS 关在纯逻辑世界,无法触碰任何网页内容。
管控:微信可以通过 Native 层控制
setData的频率和数据量——超出限制就告警/降级,保证整个微信 App 的稳定性和流畅度。如果是自由 DOM 操作,微信无法介入。
setData 的真实成本:
// 逻辑层
Page({
data: {
list: [] // 假设 100 条,每条 100 个字段
},
onLoad() {
// 加载数据
const hugeList = Array.from({ length: 100 }, (_, i) => ({
id: i,
title: `Item ${i}`,
content: 'x'.repeat(5000), // 每条 5KB
// ... 更多字段
}));
// list 约 500KB
console.time('setData');
this.setData({ list: hugeList }); // 💥 500KB 序列化
console.timeEnd('setData');
// 输出:setData: 45ms
// Android 低端机上可能 80-120ms!
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
setData 的全链路耗时:
逻辑层 Native层 渲染层
this.setData({list})
│ │ │
├─ JSON.stringify(500KB) ────►│ │
│ 耗时: ~8ms │ │
│ ├─ 转发给目标 WebView ────────►│
│ │ 耗时: ~2ms │
│ │ ├─ JSON.parse(500KB)
│ │ │ 耗时: ~12ms
│ │ ├─ Virtual DOM diff
│ │ │ 耗时: ~15ms
│ │ ├─ 真实 DOM 更新
│ │ │ 耗时: ~8ms
│ │ │
◄── 回调 setData 完成 ────────◄────────────────────────────◄──
总耗时: 45ms(500KB 数据)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 5.2 Skyline
疑惑:"不用 WebView 渲染,小程序到底怎么画页面?"
论证:Skyline 在渲染层完全抛弃 WebView,用自己的 C++ 渲染管线:
传统 WebView 渲染层: Skyline 渲染层:
┌────────────────────┐ ┌────────────────────────┐
│ WebView │ │ Skyline 渲染引擎 (C++) │
│ ┌──────────────┐ │ │ │
│ │ WXML 模板 │ │ │ WXML → C++ 布局树 │
│ │ ↓ │ │ │ ↓ │
│ │ Virtual DOM │ │ │ CSS → 样式计算 │
│ │ ↓ │ │ │ ↓ │
│ │ 真实 DOM 树 │ │ │ Layout → 原生绘制 │
│ │ ↓ │ │ │ ↓ │
│ │ Blink 渲染 │ │ │ 直接操作 GPU/CA Layer │
│ └──────────────┘ │ │ │
│ 内存占用: ~80MB │ │ 内存占用: ~30MB │
│ 首帧时间: ~200ms │ │ 首帧时间: ~50ms │
└────────────────────┘ └────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Skyline 为什么更快:
| 对比项 | WebView 渲染层 | Skyline 渲染层 |
|---|---|---|
| 布局引擎 | Blink 全功能(包含所有 CSS 特性) | 裁剪版,只支持小程序 CSS 子集 |
| 渲染方式 | Web 渲染管线(Layout → Paint → Composite) | 直接原生绘制(跳过 Paint 阶段的很多步骤) |
| WXML 编译 | 运行时 Virtual DOM diff | 编译期静态分析 + 运行时最小化 diff |
| setData 传输 | JSON.stringify → 传给 WebView → JSON.parse | 结构化二进制传输,无需序列化 |
| 长列表复用 | 依赖框架实现(虚拟滚动) | 内置回收池,原生高效 |
| 手势交互 | JS 处理 → 逻辑层 | C++ 层直接响应,无需跨线程 |
Skyline 的长列表性能:
// Skyline 下长列表自动获得原生回收池效果
// 开发者不需要写 recycle-view 之类的组件
Page({
data: {
list: Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }))
}
});
// WXML:
// <scroll-view type="list" scroll-y>
// <view wx:for="{{list}}" wx:key="id">{{item.text}}</view>
// </scroll-view>
// WebView 渲染层:10000 个 DOM 节点 → 内存爆炸 → 卡死
// Skyline 渲染层:只渲染可见区域 ± 缓冲区的节点 → 始终 ~30 个节点 → 60fps
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5.3 小程序与 H5
小程序和 H5 的 API 差异不是"谁多谁少",而是授权模型彻底不同:
| 能力 | H5 (浏览器) | 小程序 | 差异本质 |
|---|---|---|---|
| 地理位置 | navigator.geolocation(弹一次授权) | wx.getLocation(每次调用需用户授权,或后台定位需额外配置) | 小程序的授权更细粒度 |
| 获取手机号 | ❌ 不可能 | wx.getPhoneNumber(需用户点击按钮授权) | 小程序的核心变现能力 |
| 蓝牙 | Web Bluetooth API(Chrome 独有) | wx.openBluetoothAdapter(全平台) | 小程序兼容性好得多 |
| 文件系统 | ❌(File API 有限) | wx.getFileSystemManager(完整的沙箱文件系统) | 小程序的离线能力基石 |
| 推送通知 | Web Push(需 Service Worker) | 服务端直接调微信开放平台 API | 小程序无需注册 SW |
| 后台运行 | Service Worker(有限制) | 小程序后台存活 5 分钟后被挂起 | 都比原生 App 受限 |
| 分享到朋友圈 | ❌ | wx.showShareMenu | 小程序的核心社交能力 |
| WebSocket | new WebSocket() | wx.connectSocket()(最大连接数限制) | 小程序有限额 |
| 支付 | 调起第三方 SDK | wx.requestPayment(原生体验) | 小程序完胜 |
小程序 API 的设计哲学:能力给你,但每次使用都要经过微信的权限校验层——微信是"能力中介"。这是微信生态的控制力和安全性的根源,也是"小程序不能做 X"的根本原因——不是技术上做不了,是微信没给接口。
# 6. PWA离线缓存
# 6.1 Service
Service Worker 是独立于页面主线程的 JS Worker——可以拦截所有网络请求、管理缓存,生命周期完全独立于页面:
register()
│
▼
┌─────────────────┐
│ installing │ ← install 事件
│ 缓存核心资源 │ self.skipWaiting() 可跳过等待
└────────┬────────┘
│ install 完成
▼
┌─────────────────┐
│ waiting │ ← 等待旧 SW 控制的所有页面关闭
│ (已安装待激活) │ 或 self.skipWaiting()
└────────┬────────┘
│ 所有 client 关闭 / skipWaiting
▼
┌─────────────────┐
│ activating │ ← activate 事件
│ 清理旧缓存 │ self.clients.claim() 立即接管
└────────┬────────┘
│ activate 完成
▼
┌─────────────────┐
│ activated │ ← 完全激活
│ 拦截 fetch │
│ 监听 message │
│ 可被 terminated │ ← 浏览器随时可杀掉 SW
└────────┬────────┘
│ 无页面使用 + 收到 push/sync
▼
┌─────────────────┐
│ terminated │ ← 被回收
│ 下次 fetch 唤醒 │
└─────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
完整注册代码:
// sw.js —— Service Worker 脚本
const CACHE_NAME = 'v2'; // 改版本号触发更新
// ① install: 预缓存核心资源
self.addEventListener('install', (event) => {
console.log('[SW] Installing...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
]);
})
);
// self.skipWaiting(); // 强制跳过 waiting → 立即 activate(有风险,见下文)
});
// ② activate: 清理旧缓存
self.addEventListener('activate', (event) => {
console.log('[SW] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME) // 不是当前版本的缓存
.map((name) => caches.delete(name)) // 全部删掉
);
})
);
// self.clients.claim(); // 让此 SW 立即接管所有 client(通常必须调)
});
// ③ fetch: 拦截所有网络请求
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
// 缓存命中 → 返回缓存;缓存未命中 → 走网络
return cachedResponse || fetch(event.request);
})
);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
为什么 skipWaiting 有风险:如果用户在 v1 SW 控制下打开了页面 A,你发了 v2 SW(改了所有 CSS)。skipWaiting() 让 v2 立刻激活——但页面 A 的 DOM 还在用旧 CSS → 页面错乱。正确做法是让用户关闭所有页面后再自然激活,或通过 postMessage 通知页面刷新。
# 6.2 Cache-Fi
三种缓存策略的本质区别:
Cache-First (缓存优先):
┌──────┐ 命中 ┌──────┐
│ 请求 │──────────►│ 返回 │ ← 适合:不常变的静态资源
└──┬───┘ └──────┘
│ 未命中
▼
┌──────┐ ┌──────┐
│ 网络 │──────────►│ 返回 │
└──────┘ └──────┘
风险:缓存了旧版本,用户永远看不到更新
Network-First (网络优先):
┌──────┐ 成功(200ms超时) ┌──────┐
│ 请求 │──────────► │ 返回 │ ← 适合:经常变的数据
└──┬───┘ └──────┘
│ 超时/网络错误
▼
┌──────┐ ┌──────┐
│ 缓存 │──────────►│ 返回 │
└──────┘ └──────┘
风险:每次都要等网络超时,离线体验差
Stale-While-Revalidate (陈旧但可用,后台更新):
┌──────┐ 立即返回缓存 ┌──────┐
│ 请求 │───────────────────►│ 返回 │ ← 适合:图片、头像
└──┬───┘ └──────┘
│ 同时发起网络请求
▼
┌──────┐ 更新缓存 (下次生效)
│ 网络 │──────────► 用户看到的是「上次的版本」,
└──────┘ 但缓存已更新为「最新的版本」
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
三种策略的代码实现:
// sw.js
// 策略 1: Cache-First —— 适合 JS/CSS/Fonts(有版本号的静态资源)
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open('static-v1');
cache.put(request, response.clone());
return response;
}
// 策略 2: Network-First —— 适合 API 请求(数据最新优先)
async function networkFirst(request) {
try {
const response = await fetch(request, { signal: AbortSignal.timeout(3000) });
const cache = await caches.open('api-v1');
cache.put(request, response.clone());
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
// 缓存也没有 → 返回离线 fallback
return new Response(JSON.stringify({ error: 'offline' }), {
headers: { 'Content-Type': 'application/json' }
});
}
}
// 策略 3: Stale-While-Revalidate —— 适合图片、用户头像
async function staleWhileRevalidate(request) {
const cache = await caches.open('images-v1');
const cached = await cache.match(request);
// 立即返回缓存(不等待网络)
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise; // 有缓存返回缓存,无缓存等待网络
}
// fetch 监听器路由分发
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
// API → 网络优先
event.respondWith(networkFirst(event.request));
} else if (url.pathname.match(/\.(js|css|woff2)$/)) {
// 静态资源(有版本号)→ 缓存优先
event.respondWith(cacheFirst(event.request));
} else if (url.pathname.match(/\.(png|jpg|svg|webp)$/)) {
// 图片 → SWR
event.respondWith(staleWhileRevalidate(event.request));
}
// 其他(如 HTML)→ Network-First fallback
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
决策树:
这个资源会不会经常变?
├── 几乎不变(JS/CSS 带 hash 的文件名) → Cache-First
├── 经常变(API 响应、实时数据) → Network-First
├── 变了但不需要立即看到(头像、封面图) → Stale-While-Revalidate
└── 导航请求(HTML 页面本身) → Network-First + 离线 fallback 页
2
3
4
5
# 6.3 Web App
// manifest.json
{
"name": "我的编辑器",
"short_name": "编辑器",
"start_url": "/?utm_source=pwa",
"display": "standalone", // 关键:去掉浏览器 UI(地址栏、工具栏)
"background_color": "#ffffff",
"theme_color": "#1976d2",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
],
"screenshots": [
{ "src": "/screenshots/home.png", "sizes": "1280x720", "type": "image/png" }
],
"categories": ["productivity", "utilities"],
"display_override": ["window-controls-overlay"] // 桌面 PWA 的自定义标题栏
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PWA vs 原生 App 的差距——iOS 是最大的鸿沟:
| 能力 | Android PWA | iOS PWA (Safari) | 原生 App |
|---|---|---|---|
| 添加到主屏幕 | ✅ | ✅ (Safari 分享菜单) | ✅ |
| 离线运行 | ✅ (Service Worker) | ✅ (SW 支持较完整) | ✅ |
| 推送通知 | ✅ (Web Push API) | ❌ (iOS 16.4+ 才支持,且需用户手动开启) | ✅ |
| 后台同步 | ✅ (Background Sync) | ❌ | ✅ |
| 存储配额 | ~60% 磁盘(按源) | ~500MB(超出可能被清理) | 几乎无限 |
| 访问蓝牙 | ✅ (Web Bluetooth) | ❌ | ✅ |
| 访问 NFC | ✅ | ❌ | ✅ |
| 应用内购买 | ❌ (只能用 Web 支付) | ❌ | ✅ (IAP) |
| 图标角标 | ✅ | ❌ | ✅ |
| 后台执行 | 有限 (SW) | SW 30秒后被挂起 | ✅ |
结论:PWA 在 Android 上已接近原生体验(尤其是配合 TWA 上架 Play Store),但在 iOS 上存在功能阉割——推送、后台、存储三大硬伤,让"离线文档编辑器"这类需求在 iOS PWA 上几乎不可行。如果你的用户群以 iOS 为主,PWA 不能替代原生 App。
# 7. WebAssem
# 7.1 Linear
WASM 的内存模型和 JS 的堆模型完全不同:
JavaScript 引擎内存: WebAssembly 线性内存:
┌────────────────────┐ ┌──────────────────────┐
│ GC 管理的堆 │ │ Linear Memory │
│ (V8 的 young/old) │ │ (连续的 ArrayBuffer) │
│ │ │ │
│ 对象1 对象2 对象3 │ │ 0x0000 ┌──────────┐ │
│ ↓ ↓ ↓ │ │ │ 全局变量 │ │
│ 随机分布、随时移动 │ ← GC 会整理 │ 0x1000 ├──────────┤ │
│ │ │ │ 栈 (↓) │ │
│ JS 无需关心地址 │ ← 无指针暴露 │ │ │ │
└────────────────────┘ │ 0x8000 ├──────────┤ │
│ │ │ │
│ │ 堆 (↑) │ │
│ │ │ │
│ 0xFFFF └──────────┘ │
│ │
│ 完全手动、无 GC │
└──────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WASM 的线性内存就是一个大号 ArrayBuffer——从 JS 侧看:
// 创建 WASM 实例时分配线性内存
const memory = new WebAssembly.Memory({
initial: 256, // 256 页 = 16 MB
maximum: 512 // 最大 512 页 = 32 MB
});
// JS 可以直接读写 WASM 的内存!
const buf = new Uint8Array(memory.buffer);
buf[0] = 42; // WASM 的地址 0 现在是 42
console.log(buf[1000]); // 读 WASM 地址 1000 的值
// ⚠️ 安全边界:WASM 只能访问自己的线性内存
// WASM 不能访问 JS 对象的内部、不能访问其他 WASM 实例的内存
// → 这就是 WASM 的「沙箱」——和 JS 的沙箱是同一套硬件 + OS 机制
2
3
4
5
6
7
8
9
10
11
12
13
14
WASM 是栈式虚拟机:
WASM 指令:i32.add
含义:从栈顶弹出两个 i32,相加后压回栈顶
示例:`(i32.add (i32.const 3) (i32.const 4))`
执行过程:
步骤1: push 3 → 栈: [3]
步骤2: push 4 → 栈: [3, 4]
步骤3: i32.add → pop 4, pop 3 → 3+4=7 → push 7
栈: [7]
这与 JVM 字节码、.NET CIL 是同类设计——栈机器模型让编译器和验证器都更简单。
2
3
4
5
6
7
8
9
10
11
12
# 7.2 JS ↔ WAS
疑惑:"JS 调 WASM 函数是不是和调普通 JS 函数一样快?"
论证:完全不一样。JS ↔ WASM 调用涉及 trampoline(蹦床) 和类型转换:
// 假设我们有一个 WASM 模块,导出了 multiply 函数
const wasmInstance = await WebAssembly.instantiate(wasmBytes, {
env: {
// JS 暴露给 WASM 的函数
jsLog: (ptr, len) => {
const bytes = new Uint8Array(memory.buffer, ptr, len);
console.log(new TextDecoder().decode(bytes));
}
}
});
const { multiply, memory } = wasmInstance.exports;
// JS → WASM: 参数和返回值必须是数字类型
const result = multiply(3, 4); // FAST: ~10ns(JIT 内联后)
// WASM → JS: 调用 jsLog → 需要 trampoline
// 1. WASM 把 i32 参数 push 到自己的栈
// 2. 跳到 trampoline → 转成 JS 的 Number
// 3. 调 JS 函数
// 4. 返回值(如果有)通过 trampoline 转回 WASM 栈
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
JS ↔ WASM 调用的性能数据:
| 调用方向 | 数据量 | 延迟 | 备注 |
|---|---|---|---|
| JS → WASM (基础类型) | 1~4 个 Number | ~10ns | JIT 可内联,极快 |
| JS → WASM (String) | 1KB 字符串 | ~2μs | 需要 UTF-8 ↔ UTF-16 转换 |
| WASM → JS (基础类型) | 1~2 个 Number | ~50ns | trampoline 开销 |
| WASM → JS (大对象) | ArrayBuffer 共享 | ~50ns | 共享内存,无需拷贝 |
| JS → WASM (频繁调用) | 100万次/秒 | 10ms 总耗时 | 可接受 |
| JS → WASM (每帧调 10000 次) | 16ms 内 | ~0.1ms | 可接受 |
关键结论:WASM 不适合"大量细碎交互"的场景——每帧调 WASM 一千次没问题,每帧调一百万次就有问题。WASM 适合"JS 把一大块数据丢给 WASM,WASM 算完把结果丢回来"的模式。
# 7.3 Rust → w
用一个完整的图像处理库演示全链路:
// Cargo.toml
[package]
name = "image-processor"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
image = "0.24" // Rust 的图像处理库
// ==========================================
// src/lib.rs —— Rust 源码
use wasm_bindgen::prelude::*;
use image::{load_from_memory, ImageFormat, DynamicImage};
// #[wasm_bindgen] → 自动生成 JS 绑定代码
#[wasm_bindgen]
pub fn grayscale(input: &[u8]) -> Vec<u8> {
// 从内存中加载图片
let img: DynamicImage = load_from_memory(input).unwrap();
// 转灰度
let gray = img.grayscale();
// 编码为 PNG 返回
let mut output = Vec::new();
gray.write_to(&mut output, ImageFormat::Png).unwrap();
output
}
#[wasm_bindgen]
pub fn blur(input: &[u8], sigma: f32) -> Vec<u8> {
let img = load_from_memory(input).unwrap();
let blurred = img.blur(sigma);
let mut output = Vec::new();
blurred.write_to(&mut output, ImageFormat::Png).unwrap();
output
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 构建 + 打包
$ wasm-pack build --target web
# 产物:
# pkg/
# image_processor_bg.wasm ← WASM 二进制
# image_processor.js ← JS 绑定(wasm-bindgen 自动生成)
# image_processor.d.ts ← TypeScript 类型声明
# package.json
$ cd pkg && npm publish # 直接发布到 npm
2
3
4
5
6
7
8
9
10
// 前端使用 —— 和普通 npm 包一样
import init, { grayscale, blur } from 'image-processor';
// 初始化(只做一次)
await init(); // 加载 .wasm 文件 + 编译 + 实例化
// 读取图片文件
const input = await fetch('/photo.jpg').then(r => r.arrayBuffer());
const inputBytes = new Uint8Array(input);
console.time('WASM grayscale');
const output = grayscale(inputBytes);
console.timeEnd('WASM grayscale'); // 约 15ms (4K 图)
// 和 JS 版对比
console.time('JS grayscale');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// ... Canvas 2D 灰度处理 ...
console.timeEnd('JS grayscale'); // 约 120ms (4K 图)
// → WASM 快 8 倍
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 7.4 图像滤镜的 JS
用 Canvas 2D 做 JS 版,Rust + wasm-bindgen 做 WASM 版,同一张 4K (3840×2160) 图片:
| 操作 | JS (Canvas 2D) | WASM (Rust) | 倍数 | 原因 |
|---|---|---|---|---|
| 灰度 | 120ms | 15ms | 8x | Rust 可 SIMD 自动向量化 + 无 GC 暂停 |
| 高斯模糊 | 850ms | 45ms | 19x | Rust 的 image crate 实现了分离卷积(O(2n) vs O(n²)) |
| 旋转 | 35ms | 8ms | 4x | Canvas 需要进行 Layout → Paint 全程 |
| 缩略图(10MB→200KB) | 200ms | 18ms | 11x | Rust 有高度优化的 image 编码器 |
| 批量处理 100 张 | OOM 崩溃 | 2.3s (无 GC) | 不可能 → 可能 | WASM 线性内存无 GC,100 张图处理完只占 ~200MB |
为什么 WASM 快这么多——不是"WASM 指令比 JS 快",而是:
- 无 GC 暂停:JS 在处理大量图片数据时,频繁分配
Uint8ClampedArray,触发 GC 造成暂停。WASM 在自己的线性内存里管理,永不触发 JS GC。 - SIMD:WASM 的
v128指令直接映射到 CPU 的 SSE/NEON 指令,Rust 编译器自动向量化循环。JS 要手动用SIMD.js提案(还不太稳定)。 - 编译器优化:Rust 的 LLVM 后端可以做函数内联、循环展开、寄存器分配等 JS 引擎 JIT 做不了的深度优化(WASM 在构建时就被优化好了)。
结论:WASM 不是 JS 的替代品,而是 JS 的计算翅膀——日常逻辑用 JS 写,计算密集部分用 Rust/C++ 编译成 WASM 嵌入。一个典型前端项目里可能 95% 的代码是 JS,但那 5% 的 WASM 决定了性能天花板。
# 8. WASI 与 S
# 8.1 WASI Pre
WASI(WebAssembly System Interface)是 WASM 的操作系统抽象层——好比 POSIX 之于 C 程序:
浏览器 WASM: WASI (Server-side WASM):
┌──────────────────┐ ┌──────────────────────────┐
│ WASM 模块 │ │ WASM 组件 │
│ │ │ │
│ 导入: │ │ 导入: │
│ ─────── │ │ ─────── │
│ env.memory │ │ wasi:io/streams │
│ env.jsLog │ │ wasi:filesystem/types │
│ (由 JS 提供) │ │ wasi:http/types │
│ │ │ wasi:cli/run │
│ 能做什么: │ │ (由 WASI 运行时提供) │
│ 调用 JS 函数 │ │ │
│ 操作线性内存 │ │ 能做什么: │
└──────────────────┘ │ 读写文件 ✓ │
│ 网络请求 ✓ │
│ 环境变量 ✓ │
│ 时间 ✓ │
│ 随机数 ✓ │
│ fork 进程 ✓ (preview 2) │
└──────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WASI 的核心设计理念——基于能力的权限模型(Capability-based Security):
# 传统程序:程序自己决定能干什么(风险)
$ ./my_program # 它想读 /etc/passwd 就能读
# WASI 程序:权限由启动者显式授予
$ wasmtime run \
--dir=/data \ # 只允许访问 /data 目录
--env=API_KEY=xxx \ # 只注入这一个环境变量
--tcplisten=:3000 \ # 只允许监听 3000 端口
my_program.wasm
# my_program.wasm 不能访问 /etc/passwd → 强制隔离
# 这就是 "least privilege" 的硬编码实现
2
3
4
5
6
7
8
9
10
11
WASI Preview 2 的组件模型——不同语言写的 WASM 可以互相调:
┌──────────────────────────────────────────┐
│ 主机 (Host) │
│ │
│ ┌─────────────┐ ┌───────────────┐ │
│ │ WASM 组件 A │ │ WASM 组件 B │ │
│ │ (Rust 编译) │◄──►│ (C++ 编译) │ │
│ │ │WIT │ │ │
│ │ 导出: │ │ 导出: │ │
│ │ image- │ │ http- │ │
│ │ processor │ │ handler │ │
│ └──────┬───────┘ └───────┬───────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ WASI 接口: filesystem / http / cli │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────┘
WIT (Wasm Interface Type) 定义:
interface image-processor {
grayscale: func(input: list<u8>) -> list<u8>;
blur: func(input: list<u8>, sigma: float32) -> list<u8>;
}
// Rust 组件 A 可以调 C++ 组件 B 的 http-handler
// 不需要共享内存、不需要序列化——WASM 运行时在内部做了高效桥接
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 8.2 WasmEdge
| 特性 | wasmtime | WasmEdge | Wasmtime + Spin (Fermyon) |
|---|---|---|---|
| 开发者 | Bytecode Alliance (Fastly/AWS/Google/Mozilla) | CNCF 孵化项目 | Fermyon |
| 语言 | Rust | C++ (核心) + Rust (工具) | Rust |
| WASI 支持 | Preview 2 完整 | Preview 1 + 部分 Preview 2 | Preview 2 |
| 冷启动速度 | ~1ms | ~0.1ms | ~1ms |
| 热启动速度 | ~10μs | ~5μs | ~10μs |
| 内存占用 (空载) | ~5 MB | ~2 MB | ~8 MB |
| AI 推理 | 可通过 WASI-NN | 内置 TensorFlow/PyTorch/PiNet 插件 | 通过 WASI-NN |
| 异步支持 | ✅ (Tokio) | ✅ (自定义) | ✅ (Tokio + Spin) |
| 适用场景 | 通用 Wasm 运行时、CLI | 边缘计算、Serverless、微服务 | Serverless 应用框架 |
WasmEdge 为什么适合边缘计算:
// 边缘节点上跑一个 WASM 函数(云函数场景)
// 传统方案:Node.js 容器
// 冷启动: 500ms (容器启动 + Node 加载) 内存: 50MB
// WasmEdge 方案:WASM 模块
// 冷启动: 0.5ms (直接加载字节码) 内存: 5MB
// 对比:1000 个并发请求
// Node.js: 需要 50 个容器 → 2.5 GB 内存
// WasmEdge: 1000 个 WASM 实例 → 200 MB 内存(共用运行时)
2
3
4
5
6
7
8
9
10
结论:WASI 让 WASM 脱离了浏览器的束缚,成为一种"比 Docker 更轻、比 Node.js 更安全"的 Serverless 运行时。它的核心优势是冷启动速度 + 内存密度 + 权限沙箱——这三个指标在边缘计算和 Serverless 场景下是决定性优势。但它目前还缺生态(不是所有 Rust crate 都支持 wasm32-wasi 目标),不适合作为通用后端替代品。
# 9. 跨端决策矩阵
# 9.1 性能 / 开发人
回到第 1 章的场景——在线文档协作编辑器,四端需求。我们用七维打分模型量化评估:
评分标准:1 分(最差/最高成本)→ 5 分(最优/最低成本)
| 维度 / 方案 | 纯原生 | Electron + RN + Taro | Flutter | PWA + Tauri |
|---|---|---|---|---|
| 性能 | 5 | 3 (桌面低速/RN 旧架构掉帧) | 4 | 3 (PWA 受 WebView 限制) |
| 开发人力 | 1 (5 种技术栈) | 4 (JS 全家桶) | 3 (学 Dart + 布局模型) | 2 (需 Rust + PWA 调试) |
| 生态成熟度 | 5 | 5 (npm 全生态) | 4 | 3 (Rust 生态还在完善) |
| 用户体验 | 5 | 3.5 (RN 的非原生感) | 4.5 | 3 (iOS PWA 阉割) |
| TTM (Time To Market) | 1 | 5 | 4 | 2 |
| 代码复用 | 1 | 4 (业务逻辑可复用) | 3 (Dart 独有) | 2 (Rust 只有后台逻辑) |
| 长期维护 | 2 (多语言多 repo) | 4 (统一 JS) | 4 (单一技术栈) | 2 (Rust 专家难招) |
| 加权总分 | 2.7 | 4.1 | 3.8 | 2.4 |
七维矩阵热力图(1=差, 5=优):
性能 人力 生态 体验 TTM 复用 维护
纯原生 ████ █ ████ ████ █ █ ██
Electron ███ ████ ████ ███ ████ ████ ████
Flutter ████ ███ ████ ████ ████ ███ ████
PWA+Tauri ███ ██ ███ ███ ██ ██ ██
2
3
4
5
解读:
- 纯原生:除了性能和体验满分,其他全是最差。适合"不差钱不差人"的大厂核心产品(微信、淘宝)。
- Electron + RN + Taro:JS 全家桶的甜蜜点。人力成本低、生态最全、代码复用率高。适合人力紧张的全栈前端团队。代价是:桌面端安装包大、RN 长列表需要精细优化。
- Flutter:性能接近原生,TTM 也不错,但团队要学 Dart。适合不计较学习成本的移动优先团队。
- PWA + Tauri:体积小、快、安全,但生态不成熟,iOS PWA 功能阉割。适合工具类轻量桌面应用(如 API 测试工具、Markdown 编辑器)。
# 9.2 四个典型场景的推
场景 A:在线文档协作编辑器(含离线编辑)
需求:Web + 桌面 + 移动 App + 小程序,四端全部 团队:3 前端 + 1 后端,3 个月
推荐:Electron (桌面) + React Native (移动) + Taro (小程序) + Web (React)
共享:业务逻辑层 (hooks/services/state) → 90%+ 代码复用
代价:RN FlatList 要做虚拟滚动优化; Electron 骨架屏优化冷启动
分层架构:
┌──────────────────────────────────────────────┐
│ 共享 UI 组件 (React) │
│ 平台特定适配 (Platform.OS / process.env) │
├──────────────────────────────────────────────┤
│ 共享业务逻辑 Hooks (useAuth/useDoc/useSync) │
│ 共享状态管理 (Zustand / Jotai) │
│ 共享网络层 (Axios / Socket) │
├──────────────────────────────────────────────┤
│ 平台适配层 │
│ Web: localStorage / IndexedDB │
│ 桌面: Electron IPC → fs (文件系统) │
│ 移动: RN AsyncStorage / SQLite │
│ 小程序: wx.setStorage / wx.cloud │
└──────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
场景 B:企业 IM(Slack 竞品)
需求:桌面 + 移动,消息实时推送,离线消息 团队:3 前端 + 1 移动端
推荐:Electron + Tauri 混合策略
开发/内部版: Electron (快速迭代 → 功能稳定后移植 Tauri)
发布/正式版: Tauri (体积小、内存省、启动快)
移动端: Flutter (Dart 的性能 + FFI 直接调原生)
理由:IM 的桌面端只要消息列表 + 输入框 + 通知,Tauri 的 WebView 完全够用
2
3
4
5
场景 C:工具型 App(API 测试/正则调试)
需求:桌面端,轻量、快速、离线 团队:1 人全栈
推荐:Tauri + Vite + React
理由:
- 安装包 <10 MB
- 启动 <500ms
- 不需要复杂 Web 组件
- Rust 后端处理 HTTP/正则等计算密集任务
2
3
4
5
6
场景 D:电商小程序
需求:微信生态内购物、分享裂变 团队:3 前端
推荐:Taro (React) / uni-app (Vue) → 编译到微信小程序
复杂长列表 → 用 Skyline 渲染引擎
分享 → 微信原生分享 API
支付 → wx.requestPayment
理由:小程序是微信生态的入口,不需要 Web/桌面端——专注打通微信能力
2
3
4
5
# 10. 全系列回顾总结
# 10.1 16 篇全景知识
你的 JavaScript 代码
│
▼
┌───────────────────────────────────────────────────────────┐
│ 第 1-6 篇: JS 引擎核心 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────┐│
│ │ 执行管线 │→│ 隐藏类GC │→│ 类型转换 │→│ 作用域链 │→│ this │→│原型链 │
│ │(Parser │ │(Hidden │ │(隐式转换)│ │(闭包) │ │(四规则)│ │(class)│
│ │ Ignition)│ │ Classes) │ │ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └──────┘ └─────┘
│ │
│ 第 7-9 篇: 高级机制 ▼
│ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ │ Proxy元 │ │ 事件循环 │ │ Worker │
│ │ 编程 │ │ Promise │ │ 并发 │
│ └─────────┘ └─────────┘ └─────────┘
│ │
│ 第 10-12 篇: 工程化 ▼
│ ┌──────────┐ ┌─────────┐ ┌─────────┐
│ │ 浏览器渲染│ │ 网络协议 │ │ 性能优化 │
│ │ 像素之路 │ │ HTTP/WS │ │ 全链路 │
│ └──────────┘ └─────────┘ └─────────┘
│ │
│ 第 13-15 篇: 代码组织 ▼
│ ┌──────────┐ ┌─────────┐ ┌─────────────┐
│ │ 模块系统 │ │ 现代工程链│ │ 设计模式+FP │
│ │ ESM/CJS │ │ 三件套 │ │ │
│ └──────────┘ └─────────┘ └──────────────┘
│ │
│ 第 16 篇: 交付终局 【本篇】 ▼
│ ┌──────────────────────────────────────────────┐
│ │ 跨端架构: Electron / RN / 小程序 / PWA / WASM │
│ └──────────────────────────────────────────────┘
└───────────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
16 篇的递进逻辑:
理解 JS 怎么执行 (1~6) → 理解运行时怎么调度 (7~9)
→ 理解代码怎么优化 (10~12) → 理解项目怎么组织 (13~15)
→ 理解代码交付到哪里 (16)
2
3
# 10.2 JavaScri
回顾整个专栏,提炼五条跨篇适用的核心哲学:
哲学 1:单线程但有异步——JS 用"非阻塞 I/O + 事件循环"模拟并发
JS 只有一个调用栈,但通过宏任务/微任务队列实现了"不等待"的并发体验。这不是多线程,是协作式多任务——每个任务自觉快速完成,不要把持事件循环超过 50ms。这条哲学从第 1 篇的 Parser 到第 16 篇的 Worker 并发,贯穿始终。
哲学 2:动态类型但有隐藏类——JS 用"形状推断"弥补类型不确定的代价
V8 不要求你声明类型,但它在运行时偷偷为你的对象创建隐藏类(Hidden Classes)。同一个构造函数创建的对象共享同一个隐藏类,属性访问变成"基址 + 固定偏移"而非哈希查找。这条哲学支撑了第 2 篇的全部内容。
哲学 3:基于原型但模拟类——JS 用 prototype 链实现继承,class 只是语法糖
class 关键字让你以为 JS 是 Java-like OOP,但本质上是 prototype 链 + constructor 函数的组合。理解这个,才能理解 super 的内部实现和为什么 typeof class {} 是 "function"。第 6 篇和第 15 篇的设计模式都依赖这条。
哲学 4:浏览器是宿主但可以被替换——JS 的"平台无关性"来自于宿主环境的抽象
V8 引擎不绑定浏览器——它可以在 Node.js 里、在 Electron 里、在 Deno 里、嵌入到任何 C++ 程序里。JS 的跨平台能力不是因为"JS 本身是跨平台的",而是因为有一群人给不同平台写了不同的宿主。这条哲学是第 16 篇的全部主题。
哲学 5:运行时改一切——JS 的 Proxy、Reflect、eval 让"元编程"比静态语言激进得多
你可以用 Proxy 拦截 obj.x 的读取、用 eval 动态执行字符串代码、用 Object.defineProperty 修改属性的可写性。这种激进性让 JS 成为"库作者的天堂、应用开发者的沼泽"——MobX 的响应式、Vue 3 的 ref() 都依赖 Proxy。第 7 篇专注于这条哲学。
# 10.3 你的下一步:三条
读完 16 篇后,根据你的角色选择下一步:
路径 A:深入引擎(如果你热爱底层)
1. 读 V8 源码: src/parsing/ (Parser) → src/compiler/ (TurboFan) → src/heap/ (GC)
2. 用 d8 --trace-opt --trace-deopt 观察优化/去优化过程
3. 读 JavaScriptCore 的 DFG/FTL 编译器文档(感受另一种 JIT 哲学)
4. 看 WebAssembly Spec 原文 (https://webassembly.github.io/spec/)
2
3
4
路径 B:深入框架(如果你想成为架构师)
1. 读 React 源码: Reconciler → Scheduler → Fiber 架构
2. 读 Vue 3 源码: reactivity → compiler (模板编译) → runtime
3. 读 SolidJS: 看 "无 Virtual DOM" 的响应式怎么做
4. 读 Next.js App Router + RSC (React Server Components)
2
3
4
路径 C:深入运行时(如果你想做跨端框架)
1. 读 Hermes 源码: 看字节码解释器怎么做到 <20MB 内存
2. 读 React Native 新架构 (JSI + Fabric + TurboModules)
3. 读 Tauri 源码: Rust 怎么管理 WebView 生命周期
4. 读 wasmtime / WasmEdge: WASI 标准怎么落地
5. 读微信小程序的双线程模型通信协议(逆向分析)
2
3
4
5
# 10.4 速查表速览
速查表 A:跨端方案七维矩阵
| 场景 | 推荐方案 | 团队要求 | 包体积 | 冷启动 | 代码复用 | 备注 |
|---|---|---|---|---|---|---|
| 四端(Web/桌面/App/小程序) | Electron + RN + Taro | 3~4 前端 | 桌面 150MB / 移动 20MB | 桌面 2s / 移动 1s | 90% | 业务逻辑全共享 |
| 桌面 + 移动 | Electron + Flutter | 2 前端 + 1 Dart | 桌面 150MB / 移动 20MB | 桌面 2s / 移动 0.5s | 50% | 移动体验更优 |
| 纯桌面(轻量) | Tauri | 1 Rust + 1 前端 | 5~10MB | 0.3s | N/A | 速度+体积王者 |
| 纯桌面(重交互) | Electron | 1~2 前端 | 150MB | 2s | N/A | 生态最强 |
| 纯移动 | Flutter 或 RN(新架构) | 2 移动/前端 | 15~25MB | 0.5s | N/A | Flutter 性能更稳 |
| 小程序生态 | Taro/uni-app | 1~2 前端 | <2MB | 0.5s | N/A | 微信生态必备 |
| 高性能计算模块 | WASM (Rust/C++) | 1 系统语言 | 嵌入 JS 包 | 即时 | 0% | 计算密集场景专属 |
| Serverless 函数 | WASI (Rust → WASM) | 1 Rust | <5MB | <1ms | N/A | 替代部分微服务 |
速查表 B:本专栏 16 篇知识图谱
| 篇号 | 主题 | 核心概念 | 实战验证 |
|---|---|---|---|
| 01 | 引擎执行管线 | Parser → Ignition → TurboFan | 同一行代码在三引擎的 3 倍耗时差 |
| 02 | 隐藏类与 GC | Hidden Classes / 分代 GC | 对象属性顺序影响性能 5 倍 |
| 03 | 类型隐式转换 | == / ToPrimitive / valueOf | [] + {} vs {} + [] 的诡异差异 |
| 04 | 作用域链与闭包 | LexicalEnvironment / [[Scopes]] | 闭包内存泄漏检测与修复 |
| 05 | this 与函数组合 | 四规则 / call/apply/bind / compose | React 类组件 this 绑定原理 |
| 06 | 原型链与 class | __proto__ / prototype / ES6 class 脱糖 | instanceof 跨 iframe 失效真相 |
| 07 | Proxy 与迭代协议 | Proxy 13 种 trap / Symbol.iterator | Vue 3 reactive() 手写实现 |
| 08 | 事件循环 Promise | 宏任务 vs 微任务 / async/await 脱糖 | 死循环卡死 vs 微任务堵死 |
| 09 | Worker 并发 | DedicatedWorker / SharedArrayBuffer | Web 端图像处理 Worker 池 |
| 10 | 浏览器渲染像素之路 | CRP / Layout/Paint/Composite | 强制同步布局的 200ms 灾难 |
| 11 | 网络协议深探 | HTTP/1.1 → HTTP/3 / WebSocket / SSE | 长列表实时推送的最优协议选型 |
| 12 | 性能优化全链路 | 首屏/FCP/LCP/TTI 优化闭环 | lighthouse 100 分的真实案例 |
| 13 | 模块系统双轨 | CJS/ESM/动态 import/打包产物对比 | npm 包同时支持 CJS 和 ESM 的配置 |
| 14 | 现代工程链 | Vite/esbuild/swc/Turbopack 管线对比 | 从 Webpack 5s → Vite 0.3s 的迁移 |
| 15 | 设计模式与 FP | 单例/观察者/策略 + Monad/pipe/curry | MobX 响应式是手写 Observable |
| 16 | 跨端架构终局 | Electron/RN/小程序/PWA/WASM | 在线文档编辑器四端方案决策 |
60 秒跨端决策清单:
# 1. 我需要覆盖哪些平台?
→ Web only → 纯前端(React/Vue/Svelte)
→ 桌面 + Web → Electron (重交互) 或 Tauri (轻工具)
→ 移动 + Web → React Native + Web (JS 共享) 或 Flutter (Dart)
→ 微信生态 → Taro/uni-app
→ 全部 → Electron + RN + Taro (JS 全家桶)
# 2. 我的团队有多少人?
→ 1 人 → PWA / Tauri (最简单)
→ 3~5 人 → Electron + RN (JS 全家桶)
→ 5+ 人 → 多平台分治(原生 + Web 分离)
# 3. 我的用户对包体积/速度有多敏感?
→ 极敏感(海外低端设备)→ Tauri + PWA
→ 一般(国内用户)→ Electron
→ 不敏感(企业内部工具)→ Electron,别优化
# 4. 计算密集场景?
→ 图像/视频处理 / AI 推理 / 加密 → WASM (Rust/C++)
→ 常规 CRUD → JS 足矣
# 5. iOS 用户占比?
→ >60% → PWA 不能替代原生,必须上 RN/Flutter
→ <20% → PWA 可以考虑
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
🎉 本专栏全 16 篇骨架已就位。感谢一路读到这里——从 V8 Parser 到 Electron 跨端,JavaScript 的每一层,我们都已经拆开看过了。