编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

    • 基础入门

    • 综合案例

    • 专栏博客

      • README
      • 引擎解析编译执行
      • 隐藏类与回收机制
      • 类型隐式转换精算
      • 作用域链闭包原理
      • 函数绑定规则组合
      • 原型链语法糖本质
      • 代理与元编程协议
      • 事件循环承诺机制
      • 工作线程并发调度
      • 页面渲染像素原理
      • 网络接口存储架构
      • 服务端运行时编程
      • 模块系统双轨操作
      • 现代工程链三件套
      • 设计模式函数哲学
      • 跨端架构终局总结
        • 1. 案例与疑问引入
          • 1.1 同一个业务,产品
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构全景概览
          • 2.1 跨端方案全景图
          • 2.2 每种方案的「JS
        • 3. Electron
          • 3.1 主进程(Node
          • 3.2 为什么 Elec
          • 3.3 preload
          • 3.4 Electron
        • 4. RN新旧架构
          • 4.1 JS Bridge
          • 4.2 JSI + Fa
          • 4.3 Hermes 引擎
        • 5. 小程序双线程
          • 5.1 逻辑层(JS
          • 5.2 Skyline
          • 5.3 小程序与 H5
        • 6. PWA离线缓存
          • 6.1 Service
          • 6.2 Cache-Fi
          • 6.3 Web App
        • 7. WebAssem
          • 7.1 Linear
          • 7.2 JS ↔ WAS
          • 7.3 Rust → w
          • 7.4 图像滤镜的 JS
        • 8. WASI 与 S
          • 8.1 WASI Pre
          • 8.2 WasmEdge
        • 9. 跨端决策矩阵
          • 9.1 性能 / 开发人
          • 9.2 四个典型场景的推
        • 10. 全系列回顾总结
          • 10.1 16 篇全景知识
          • 10.2 JavaScri
          • 10.3 你的下一步:三条
          • 10.4 速查表速览
  • CodeX
  • JavaScript入门
  • 专栏博客
杨充
2026-06-11
目录

跨端架构终局总结

# 16.跨端架构终局

📍 上接第 15 篇《设计模式与函数式哲学》。代码怎么写已经优雅了。但你的 JS 不只跑在浏览器——桌面端(Electron)、移动端(React Native)、小程序、PWA、WASM,五条路摆在面前。本文回答:什么时候该选谁?

# 目录介绍

  • 1. 案例与疑问引入
    • 1.1 同一个业务,产品
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构全景概览
    • 2.1 跨端方案全景图
    • 2.2 每种方案的「JS
  • 3. Electron
    • 3.1 主进程(Node
    • 3.2 为什么 Elec
    • 3.3 preload
    • 3.4 Electron
  • 4. RN新旧架构
    • 4.1 JS Bridge
    • 4.2 JSI + Fa
    • 4.3 Hermes 引擎
  • 5. 小程序双线程
    • 5.1 逻辑层(JS
    • 5.2 Skyline
    • 5.3 小程序与 H5
  • 6. PWA离线缓存
    • 6.1 Service
    • 6.2 Cache-Fi
    • 6.3 Web App
  • 7. WebAssem
    • 7.1 Linear
    • 7.2 JS ↔ WAS
    • 7.3 Rust → w
    • 7.4 图像滤镜的 JS
  • 8. WASI 与 S
    • 8.1 WASI Pre
    • 8.2 WasmEdge
  • 9. 跨端决策矩阵
    • 9.1 性能 / 开发人
    • 9.2 四个典型场景的推
  • 10. 全系列回顾总结
    • 10.1 16 篇全景知识
    • 10.2 JavaScri
    • 10.3 你的下一步:三条
    • 10.4 速查表速览

# 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 脱离浏览器跑在服务端?
1
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 篇知识回扣
1
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】
1
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 ✓    │
│ 文件系统 ✗   │ │ 文件系统 ✓  │ │ 文件系统 ✓  │ │ 文件系统有限│
└──────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
1
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>     │ │
│  │  • 系统通知      │              │                  │ │
│  │  • 自动更新      │              │                  │ │
│  └──────────────────┘              └─────────────────┘ │
│                                                        │
└────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

关键设计:

  1. 主进程是门卫——所有涉及操作系统的事必须通过主进程。渲染进程不知道文件在哪、不知道托盘图标怎么画、不知道系统通知怎么发。它只能通过 IPC 发消息:"主进程,帮我读 /Users/xxx/config.json"。

  2. 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');
});
1
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  └─ 首屏可见 ─────────────────────────────────────
1
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();  // 用户看到的是完整界面,跳过了白屏
    });
});
1
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') │    │                      │
└──────────────────────────────┘    └──────────────────────┘
1
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);
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

contextBridge 的三条安全规则:

  1. 函数不能传引用——exposeInMainWorld 暴露的每个属性都经过 structuredClone,渲染进程 JS 拿不到 preload 里的原始引用。
  2. 不支持原型链——暴露的对象是"平面"的,没有 __proto__ 污染路径。
  3. 不暴露 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
1
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
1
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 创建/更新   │
│                 │  ◄───────────────  │                  │
│                 │   回调 / 事件      │  触摸/滚动事件    │
└─────────────────┘                    └──────────────────┘
                   所有通信走这一条队列
1
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 (一帧时间)
// → 掉帧 → 用户看到 "白屏"(新行还没渲染出来)
1
2
3
4
5
6
7
8

时序图的本质——三线程各自忙什么:

时间 →
主线程(Native): ───[渲染帧1]───[渲染帧2]───[渲染帧3]───
                            ↑              ↑
                            │ 等 JS 结果    │ 等 JS 结果
                            │              │
JS 线程:        ─────[React Diff]──[计算可见行]───
                            ↑
                            │ 等触摸事件
                            │
Shadow 线程:    ─────[Yoga 布局计算]──────────
                  (RN 的布局引擎,用 C++ 写)
1
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      │                 │
│  │  按需加载     │  │  原生渲染     │                 │
│  │  原生模块     │  │  管线        │                 │
│  └──────────────┘  └──────────────┘                 │
└────────────────────────────────────────────────────┘
1
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
1
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── 主线程
                  (共享数据结构,无序列化)
1
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 编译还没生效,应用就进后台了 ──────────────→ 每次都冷启动!
1
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 │
  └─────────────────────┘          └──────────────────────┘
1
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 三步全免)
1
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 层 序列化/转发    │               │
│           └─────────────────────────┘               │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

为什么不能直接操作 DOM?——两条设计考量:

  1. 安全:如果逻辑层能直接操 DOM,就意味着小程序可以执行任意 JS——注入广告、窃取用户数据、XSS 攻击。沙箱把 JS 关在纯逻辑世界,无法触碰任何网页内容。

  2. 管控:微信可以通过 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!
    }
});
1
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 数据)
1
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        │
└────────────────────┘              └────────────────────────┘
1
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
1
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 唤醒  │
              └─────────────────┘
1
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);
        })
    );
});
1
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 (陈旧但可用,后台更新):
┌──────┐   立即返回缓存        ┌──────┐
│ 请求  │───────────────────►│ 返回  │  ← 适合:图片、头像
└──┬───┘                     └──────┘
   │ 同时发起网络请求
   ▼
┌──────┐   更新缓存 (下次生效)
│ 网络  │──────────►          用户看到的是「上次的版本」,
└──────┘                     但缓存已更新为「最新的版本」
1
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
});
1
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 页
1
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 的自定义标题栏
}
1
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       │
                                        └──────────────────────┘
1
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 机制
1
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 是同类设计——栈机器模型让编译器和验证器都更简单。
1
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 栈
1
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
}
1
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
1
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 倍
1
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 快",而是:

  1. 无 GC 暂停:JS 在处理大量图片数据时,频繁分配 Uint8ClampedArray,触发 GC 造成暂停。WASM 在自己的线性内存里管理,永不触发 JS GC。
  2. SIMD:WASM 的 v128 指令直接映射到 CPU 的 SSE/NEON 指令,Rust 编译器自动向量化循环。JS 要手动用 SIMD.js 提案(还不太稳定)。
  3. 编译器优化: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) │
                                  └──────────────────────────┘
1
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" 的硬编码实现
1
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 运行时在内部做了高效桥接
1
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 内存(共用运行时)
1
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   ███   ██   ███   ███   ██   ██   ██
1
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           │
└──────────────────────────────────────────────┘
1
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 完全够用
1
2
3
4
5

场景 C:工具型 App(API 测试/正则调试)

需求:桌面端,轻量、快速、离线 团队:1 人全栈

推荐:Tauri + Vite + React
理由:
  - 安装包 <10 MB
  - 启动 <500ms
  - 不需要复杂 Web 组件
  - Rust 后端处理 HTTP/正则等计算密集任务
1
2
3
4
5
6

场景 D:电商小程序

需求:微信生态内购物、分享裂变 团队:3 前端

推荐:Taro (React) / uni-app (Vue) → 编译到微信小程序
  复杂长列表 → 用 Skyline 渲染引擎
  分享 → 微信原生分享 API
  支付 → wx.requestPayment
理由:小程序是微信生态的入口,不需要 Web/桌面端——专注打通微信能力
1
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 │
│  └──────────────────────────────────────────────┘
└───────────────────────────────────────────────────────────┘
1
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)
1
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/)
1
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)
1
2
3
4

路径 C:深入运行时(如果你想做跨端框架)

1. 读 Hermes 源码: 看字节码解释器怎么做到 <20MB 内存
2. 读 React Native 新架构 (JSI + Fabric + TurboModules)
3. 读 Tauri 源码: Rust 怎么管理 WebView 生命周期
4. 读 wasmtime / WasmEdge: WASI 标准怎么落地
5. 读微信小程序的双线程模型通信协议(逆向分析)
1
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 可以考虑
1
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 的每一层,我们都已经拆开看过了。

上次更新: 2026/06/16, 12:36:20
设计模式函数哲学

← 设计模式函数哲学

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式