第一次接手小程序项目的时候,我是拿写 Vue 的那套心智去套它的,结果很快就在 setData 上撞墙了。长列表一滚就掉帧,第一反应是「模板写得太重」,改了半天结构没什么用,排查了一下午才发现症结在逻辑线程往渲染线程塞的数据量上。小程序看着像个功能受限的 Web 容器,可它的运行架构、登录体系、构建方式、发版机制其实是另外一套东西,照搬 Web 经验大概率会踩坑。这篇是我把小程序从底层模型一路梳理到云开发之后留下的笔记,八个主题相互独立,可以当手册来翻。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 双线程模型到底在解决什么问题,为什么
setData是绕不开的性能瓶颈 - 小程序登录流程里的三个角色、六个术语,以及它和 OAuth 2.0 规范的对应关系
- 自定义组件的构造器、生命周期,还有组件间通信的两条路子
- 微信开发者工具 Audits 面板给出的十条性能建议,逐条讲清背后的原理
- 为什么大家最后都放弃了「构建 npm」,改用 Webpack 自建构建体系
- 性能、用户、异常三类数据怎么建模,怎么用 Proxy 劫持 API 做低侵入埋点
- 小程序发了新版本,用户为什么拿不到,
UpdateManager怎么给更新提速 - 云开发的云函数、云数据库、云存储各自负责哪一块
先说一句时效性上的话。这些笔记整理于 2021 年,小程序基础库、开发者工具的面板位置、公众平台后台的菜单层级这几年都调过好几轮,文中提到的入口路径和版本号只反映当时的情况。原文我一个字没删,但真要动手的时候,请以你手上那版开发者工具和微信官方文档为准,别拿这篇当 API 手册用。架构层面的东西倒是稳定的,双线程、登录时序、更新时机这些结论到现在依然成立。
# 一、双线程模型
这一节是理解小程序其它所有特性的地基。为什么不能操作 DOM、为什么 setData 这么贵、为什么组件通信要绕一圈,答案全在这个模型里。
# 渲染线程和逻辑线程
小程序的双线程指的就是渲染线程和逻辑线程,这两个线程分别承担 UI 的渲染和执行 JavaScript 代码的工作
下面这张图是小程序运行时的整体结构,左边是负责画界面的渲染层,右边是跑业务代码的逻辑层,中间隔着一层 Native。

图里最该留意的是「两个方框中间那道墙」。它不是画着好看的,两侧是真的跑在不同线程、不同 JS 运行环境里。
渲染线程使用 Webview 进行 UI 的渲染呈现。Webview 是一个完整的类浏览器运行环境,本身具备运行 JavaScript 的能力,但是小程序并不是将逻辑脚本放到 Webview 中运行,而是将逻辑层独立为一个与 Webview 平行的线程,使用客户端提供的 JavaScript 引擎运行代码,iOS 的JavaScriptCore、安卓是腾讯 X5 内核提供的 JsCore 环境以及 IDE 工具的 nwjs
并且逻辑线程是一个只能够运行 JavaScript 的沙箱环境,不提供 DOM 操作相关的 API,所以不能直接操作 UI,只能够通过 setData 更新数据的方式异步更新 UI
这里有个坑很多人一开始都会踩。你在小程序里写 document.querySelector 会直接报 document is not defined,不是微信故意阉割,而是逻辑线程压根就不在 Webview 里,它手上没有 document 这个对象。同理,window、localStorage、XMLHttpRequest 这些全局也都不存在,需要什么能力就用 wx. 开头的对应 API 去换。这也是为什么很多 npm 包搬到小程序里跑不起来,它们默认自己活在浏览器里。
顺着上面聊,那些依赖 DOM 尺寸的逻辑怎么办呢?小程序给了 wx.createSelectorQuery() 这类异步查询接口,你拿不到节点对象本身,只能拿到一份节点信息的快照。注意「异步」两个字,跨线程通信没法同步返回,这一点在写滚动加载、吸顶这类交互时会明显感觉到别扭。
# 事件驱动的通信方式
你要注意上图渲染线程和逻辑线程之间的通信方式,与 Vue/React 不同的是,小程序的渲染层与逻辑层之间的通信并不是在两者之间直接传递数据或事件,而是由 Native 作为中间媒介进行转发。
整个过程是典型的事件驱动模式:
- 渲染层(也可以称为视图层)通过与用户的交互触发特定的事件 event;
- 然后 event 被传递给逻辑层;
- 逻辑层继而通过一系列的逻辑处理、数据请求、接口调用等行为将加工好的数据 data 传递给渲染层;
- 最后渲染层将 data 渲染为可视化的 UI。
Vue 的响应式改一个 this.count,同一个线程内 patch 一下就完事了。小程序不行,setData 的数据要先序列化成字符串,交给 Native 转发,渲染层再反序列化,然后才轮到 diff 和更新。一次数据更新至少跨了两次线程边界,中间还夹着两次 JSON 序列化。
所以你传什么进去、传多大,直接决定了这条链路有多慢。
跟浏览器的线程模型相比,小程序的双线程模型解决了或者说规避了
Web Worker堪忧的性能同时又实现了与Web Worker相同的线程安全,从性能和安全两个角度实现了提升。可以概括地说,双线程模式是受限于浏览器现有的进程和线程管理模式之下,在小程序这一具体场景之内的一种改进的架构方案。
注意:浏览器中Worker 内的 JavaScript 代码不能操作 DOM,可以将其理解为线程安全的
微信为什么要这么设计?除了性能,安全是另一个更硬的理由。小程序跑在微信这个超级 App 里,如果开发者的脚本能直接摸到渲染层的 DOM,那就意味着能改页面结构、能注入任意节点,微信的审核形同虚设。把逻辑层关进一个没有 DOM 的沙箱,微信就可以在 Native 这一层完整地管控每一次数据流转。
回到我们要解决的问题上,这套模型对日常开发的直接影响就是下面这几条。
# 性能方面
- 在保证功能的前提下尽量使用结构简单的 UI;
- 尽量降低 JavaScript 逻辑的复杂度;
- 尽量减少 setData 的调用频次和携带的数据体量。
这三条读着像正确的废话,落到代码上其实很具体。第一条对应的是别把整个列表塞进一个 wx:for 里还嵌五层 view;第二条对应的是别在 onPageScroll 这种高频回调里做重计算;第三条最容易出问题,很多人 setData 的时候图省事直接把整个 list 甩过去,其实小程序支持路径式的局部更新,改第 3 条的标题写成 this.setData({ 'list[3].title': '新标题' }) 就行,传输量能小掉一两个数量级。第四节还会把这些点展开讲。
# 二、 小程序的用户体系与 OAuth 规范
登录是小程序里最容易写出安全漏洞的一块。我见过不少项目图省事,直接在小程序端调 auth.code2Session 拿 openid,appsecret 就那么明晃晃地写在前端代码里。小程序包是可以被解出来的,这么写等于把家门钥匙贴在门上。要避开这类问题,得先把整条链路上每一步谁做、为什么必须他做搞清楚。
微信小程序完整的登录流程
下面这张时序图是官方文档里那张的复刻,从 wx.login 开始,到开发者服务器下发 token 结束,中间每一根箭头都有存在的理由。

整个登录流程中描述了三种角色和六个术语,了解它们的定位和作用,是理解小程序登录流程的基础。
把这张图拆开看,参与方一共三个,如下图所示。

登录流程里的三个角色
客户端在整个登录流程中主要承担两种行为:
- 作为整个流程的发起者,获取临时登录凭证 code;
- 作为整个流程的终结者,存储登录态令牌 token。
- 不过客户端的所有信息和网络请求几乎都是可以被破解或拦截的,所以出于安全的考虑,小程序登录流程中的一些接口被限制不能在客户端中直接调用,而是需要在服务端发起,开发者服务的工作便是处理这些安全敏感的网络请求,体现为上图中使用code 获取 openid 和 session_key的请求,这个请求使用了微信提供的 auth.code2Session 接口。
- 而微信接口服务的工作对于开发者来说是不透明的,你需要做的仅仅是根据接口的规范,组装网络请求发送给它,然后根据返回的接口执行分发逻辑。微信服务器会验证网络请求的合法性,对于合法请求下发密钥 session_key 和用户 openid
登录流程的六个术语
这六个词分别是 code、appid、appsecret、openid、session_key 和 token。它们不是并列关系,前三个是输入,中间两个是微信返回的凭据,最后一个是你自己造的。理清这条链,登录流程基本就通了。