小程序刚发布没多久(2017年1月),最近把开发文档和一些内部分享资料翻了一遍,发现它的渲染架构设计挺有意思的,跟普通套个 WebView 的混合应用完全不同。这里整理一下我的理解。
为什么不直接用 WebView
微信早有 JS-SDK,网页套个 WebView 就能运行,为什么还要搞小程序这一套?
官方给过两个理由:
- 性能:WebView 里网页初次加载慢,白屏时间长
- 管控:微信无法审核网页内容,容易被用来做钓鱼、诱导分享等违规操作
这两个需求催生了小程序的双线程架构。
双线程模型
普通网页:JS 和 DOM 渲染在同一个线程里,JS 执行时 UI 会卡(这也是为什么 JS 长时间计算会让页面卡住)。
小程序把它拆成两个线程:
1 | ┌────────────────────────────────┐ |
渲染层:每个页面一个独立 WebView,负责渲染 WXML 和 WXSS,不直接运行开发者 JS。
逻辑层:跑在一个独立的 JavaScript 引擎里(iOS 上是 JavaScriptCore,Android 上是 V8),运行所有的 Page()、Component() 代码。
两个线程之间不共享任何内存,通信完全通过微信客户端的 JSBridge 进行,数据要序列化成 JSON 字符串传过去。
setData 为什么有性能限制
理解了双线程,setData 的性能问题就说得通了:
1 | // 每次 setData,数据流向是: |
这条路每走一次都有序列化/反序列化开销,加上跨线程通信本身的延迟。如果:
setData的数据量很大(传整个长列表)setData调用频率很高(比如onScroll里每帧都调)
就会导致明显卡顿。所以小程序文档里特别强调”精简 setData 数据”和”避免频繁 setData”。
实际上官方给了一个”路径更新”的方式专门解决精细更新:
1 | // 整个对象更新 vs 精确路径更新 |
渲染层为什么用多个 WebView
每个页面用独立的 WebView,好处是页面切换时可以做原生级别的转场动画——实际上是两个 WebView 在做 translate 动画,新页面从右边滑进来,旧页面往左推出去。这个动画由客户端原生层控制,跟 JS 执行无关,所以非常流畅,这是纯 WebView H5 页面很难做到的效果。
代价是内存占用高,页面栈最多 10 层也有这方面的考虑。
逻辑层不能操作 DOM
因为逻辑层是纯 JS 引擎,不存在 DOM API,所以 document、window、navigator 全都没有。这直接挡掉了大量依赖 DOM 的 npm 包。
反过来,这也是微信的”管控”意图体现:小程序不能动态注入 HTML,不能通过 eval 改变页面结构,内容在发布时就被审核锁定了。
WXML 不是 HTML
WXML 是微信自己定义的模板语言,虽然长得像 HTML,但本质上是描述式的数据绑定模板:
1 | <view wx:for="{{ list }}" wx:key="id" wx:for-item="item"> |
渲染层拿到 WXML + 数据之后,会构建一棵 Virtual DOM 树(类似 React 的那套),数据更新时做 Diff 再 patch 到真实 DOM,而不是全量重建。
所以小程序的 Diff 算法、Virtual DOM 一直在那,只是封装在客户端里看不到源码。
性能体验为什么比 H5 好
综合来看,小程序在以下几个点上比内嵌 WebView 的 H5 明显更好:
- 首屏速度:小程序的 WXML 预先编译,WebView 启动时直接渲染,不像 H5 还要等 JS 下载执行
- 页面切换:客户端原生层控制多 WebView 切换动画
- 与原生融合:摄像头、蓝牙、地理位置等直接调 Native API,不用走 H5 的受限接口
代价是:开发者自由度大幅降低,任何 npm 包能不能用都要看有没有依赖浏览器 API,踩坑机会很多。
这套架构设计挺有意思的,用受限的技术换来了更好的性能和更强的平台管控。从工程角度看是个合理的权衡,只是对开发者不那么友好。
评论加载中…