# Rust语言实现微信小程序渲染层类似Skyline
**Repository Path**: io123/mini-program-core
## Basic Information
- **Project Name**: Rust语言实现微信小程序渲染层类似Skyline
- **Description**: 源码100%可用,单元测试完善,这不是例子是生产级应用,适配微信小程序源码渲染引擎对齐skyline渲染引擎,可将该跨平台应用集成到你想集成的系统中实现原生渲染,同样体验
包括andriod、ios、macos、windows、linux系统、鸿蒙系统等等中去
仅供学习参考!!!
- **Primary Language**: Rust
- **License**: AGPL-3.0
- **Default Branch**: main
- **Homepage**: https://365code365.github.io/mini-program-core
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2025-12-28
- **Last Updated**: 2026-07-31
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# mini-render
**自绘的小程序渲染引擎。用 Rust 从零实现 —— 不基于 WebView、不映射系统原生控件、不依赖 Skia。**
给它一份微信小程序源码(WXML / WXSS / JS / app.json)和一块可写的像素缓冲,它把界面画出来、
把交互跑起来。对齐目标是微信的 **Skyline**(同样是原生渲染,同样没有 DOM)。
下面四张是引擎自己渲染的输出(纯 WXML + WXSS + 数据,375×667 @2x,无浏览器参与):
| | | | |
|:---:|:---:|:---:|:---:|
| 
电商首页 | 
资讯信息流 | 
外卖点餐 | 
表单控件 |
全部 65 张见 [`doc/场景画廊.md`](doc/场景画廊.md) —— 它们同时是逐字节可复现的回归基线。
---
## 目录
- [核心介绍](#核心介绍)
- [它是什么、不是什么](#它是什么不是什么)
- [一帧是怎么画出来的](#一帧是怎么画出来的)
- [八个关键设计取舍](#八个关键设计取舍)
- [现在到什么程度了(实测)](#现在到什么程度了实测)
- [能力边界:能做什么、明确不做什么](#能力边界能做什么明确不做什么)
- [适合 / 不适合的场景](#适合--不适合的场景)
- [技术栈与依赖](#技术栈与依赖)
- [30 秒跑起来(桌面)](#30-秒跑起来桌面)
- [集成进 App(SDK)](#集成进-appsdk)
- [支持范围](#支持范围)
- [测试与回归](#测试与回归)
- [仓库结构](#仓库结构)
- [文档](#文档)
---
## 核心介绍
### 它是什么、不是什么
一句话:**小程序源码进,像素出**。中间没有浏览器内核、没有系统控件、没有第三方图形库。
```
输入 引擎(纯 Rust 库) 输出
────────────────────────────────────────────────────────────────────────────
my-mini-app/ ┌──────────┐
├── app.json ─┐ │ RGBA │
├── app.wxss │ 解析 → 逻辑层 → 模板求值 → 样式计算 │ 像素 │
├── pages/*.wxml ├─▶ → Flexbox 布局 → 自研 2D 光栅器 ──────▶ │ 缓冲 │
├── pages/*.wxss │ → 分层合成(页面 / tabBar / fixed) └──────────┘
├── pages/*.js ─┘ │
└── 图片等资源 ▼
窗口 / UIView / SurfaceView / PNG
```
产物是一个普通的 Rust 库(`cdylib` / `staticlib` / `rlib`),落地条件只有一条:
**给我一块可写的像素缓冲**。所以同一份代码能跑在 Android、iOS、Windows、macOS、Linux 上,
也能在没有屏幕的 CI 里直接出 PNG。
它和常见方案的区别:
| 方案 | 做法 | 本引擎 |
|---|---|---|
| 微信小程序的 WebView 渲染层 | WXML → DOM,交给浏览器内核排版绘制 | ❌ 不用 DOM、不用浏览器内核 |
| React Native / Weex | JS 描述 → 映射成系统原生控件(`UIView` / `android.view`) | ❌ 不映射系统控件(不受各端控件差异摆布) |
| Flutter | Dart + Skia 自绘 | ✅ 思路最接近,但这里是 **Rust + 自研光栅器**,无 Skia 依赖 |
| Electron / Tauri | 打包/复用一个浏览器 | ❌ 不打包浏览器(产物 34MB 级,不是 100MB 级) |
| 微信 **Skyline** | 原生渲染 + 自绘组件 | ✅ 对齐目标:语义、手感、事件模型都按它对 |
**为什么值得自绘。** WebView 方案的痛点不在「能不能渲染」,而在你**控制不了**:内核版本随
系统走、同一份 CSS 在不同 Android 上排版不同、首屏要等内核起来、滚动与手势的手感由内核决定、
想插一层原生能力就要架桥。自绘把这些变成自己的代码:**布局与绘制逻辑各端只有一份**,帧调度、手势仲裁、内存上限
全都在手里 —— 各端的剩余差异只来自系统字体(要彻底消掉就把字体一起打包,
`assets/` 里放了 Noto Sans SC,`TextRenderer::from_file` 直接加载)。代价也很实:CSS 覆盖面要自己一条一条补
(见[能力边界](#能力边界能做什么明确不做什么)),性能靠 CPU 省着花。
### 一帧是怎么画出来的
八步,每步都能单独打开诊断日志(`MINI_*` 开关见[架构文档](doc/架构与实现原理.md#帧成本是怎么压下来的)):
| # | 阶段 | 做什么 | 关键实现 |
|---|---|---|---|
| ① | **解析** | WXML → 节点树;WXSS → 样式表 | 手写解析器;CSS 选择器引擎(标签/类/#id/`*`/属性/后代/子代 + 特异性排序),选择器在**解析期就编译**好 |
| ② | **逻辑层** | 跑 `App` / `Page` / `Component`,`setData` 产出数据快照 | QuickJS(rquickjs);CommonJS 模块、`Promise`/`async`、定时器、微任务每帧 pump |
| ③ | **模板求值** | `{{ }}` 表达式 + `wx:if` / `wx:for` 展开 → 渲染节点树 | 自研表达式引擎;`class`/`style` 绑定到数组/对象时按 CSS 语义拼接 |
| ④ | **样式计算** | 命中的 CSS + 内联 `style` 合成计算样式 | 继承语义(`color` / `font-size` / `font-weight` / `line-height` / `letter-spacing` …);简写与细项按固定档位排序落地 |
| ⑤ | **布局** | Flexbox 求解盒模型 | taffy 0.12;**文本节点挂自定义度量函数**,换行与 min/max-content 由真实字形宽度决定;布局后有第二遍修正(`reflow`)收拾只有算完才知道的溢出 |
| ⑥ | **绘制** | 逐层画进 RGBA 画布 | 自研 2D 光栅器:扫描线 + even-odd 填充、4× 超采样抗锯齿、圆角三次贝塞尔逼近(K=0.5523)、fontdue 字形 + 多字体回退、Apple `sbix` 彩色 emoji、图片双线性 + 面积两级过滤、线性/径向渐变、掩膜化 `box-shadow`、裁剪栈 |
| ⑥' | **动画** | `@keyframes` / `transition` / `wx.createAnimation` | 在**绘制期**按全局时钟求值:`transform`/`opacity`/颜色只影响绘制,不触发重排 → 动画帧成本≈静态帧 |
| ⑦ | **分层合成** | 正常流 → tabBar → `position:fixed` 覆盖层 | 与浏览器层叠顺序一致(全屏遮罩能压暗 tabBar);覆盖层用视口坐标、独立画布、按需重绘 |
| ⑧ | **上屏** | 贴到窗口 / 交给宿主 / 存 PNG | 桌面走 softbuffer;移动端把 RGBA 交给宿主 View;无头模式直接 `save_png` |
逻辑层与渲染层之间只有**数据快照**这一条通道(`setData` → `Arc`),没有 DOM、没有
虚拟节点 diff、没有 GC 压力。
### 八个关键设计取舍
这些是「为什么这么写」的核心,踩过的坑都写在 [`doc/踩坑记录.md`](doc/踩坑记录.md)。
1. **文本度量驱动布局,不估算宽度。** `TextMeasure` 上下文挂到 taffy 叶子上,由真实字形参与
布局。这样「定宽容器内按容器宽换行」和「收缩容器被内容撑开」两种 CSS 语义能同时成立。
反面教材:曾给每个文本盒 +4px「保险余量」,结果所有按内容定宽的徽标/标签/胶囊都比浏览器宽一圈。
2. **动画只重绘不重排。** `@keyframes` 里改 `width`/`height` 这类会引发重排的属性不生效,
改 `transform`/`opacity`/颜色都生效。换来的是「动画帧和静态帧一样便宜」。
3. **`setData` 只重绘变化的那一块。** 新旧渲染树并行走一遍,收集「文本/属性变了」或「几何变了」
的节点包围盒当裁剪矩形。首页秒杀倒计时的失效范围是 **45×33 像素**,不是整屏。
拿不准就退回整帧 —— 少画一块留下脏像素比多画一次严重得多。
4. **页面滚动不重绘。** 页面画布是整页高的、用内容坐标,滚动只是取不同切片上屏(每帧 2~3ms)。
只有滚出「已绘制条带」那一刻才补画一次。
5. **滚动手感按 iOS/微信那套做。** 橡皮筋衰减 `1 - 1/(x·0.55/d + 1)`、回弹是带初速度的
**临界阻尼弹簧**(不是固定时长缓动)、惯性撞边界不当场停死而是把动量交给弹簧。
6. **手势归属不在按下时决定。** 等第一次明显位移(4px)按主方向锁定,再按「到边界仍在推」
交棒给外层。横向卡片列表里竖着划该滚页面,纵向列表里横着划谁也不动。
7. **宿主层沉进 lib,桌面与移动端共用一份。** 页面栈、覆盖层、触摸状态机、手势仲裁、
picker 面板、像素合成都在 `src/host/`,只有帧调度各自实现。两条链路由
`tools/sdk-parity.sh`(逐像素)+ `src/tests/engine_input_tests.rs`(输入行为)双重守着。
8. **不引入 GPU、不做元素级位图缓存。** 定位就是「给我一块像素缓冲我就能画」;位图缓存与
动画/裁剪栈的交互复杂度不值当。抗锯齿也没换成子采样 —— 实测与 Chrome 的差异反而从
4.7% 涨到 5.3%。
### 现在到什么程度了(实测)
**性能**(375×667 @2x,即 750×1334 物理像素,CPU 逐像素写出):
| 场景 | 优化前 | 现在 |
|---|---|---|
| 商城首页整帧(轮播 + 每秒倒计时 + 骨架动画) | 49.6 ms | **6.1 ms** |
| 商城首页稳态帧间隔(144Hz 屏,节拍 6.94ms) | 6.9~25 ms 抖动 | **6.7~7.2 ms** |
| 首页最慢一帧 / 帧间隔峰值 | 17.9 / 24.9 ms | **11.4 / 11.4 ms** |
| 一次 `setData` 的重建成本 | 32 ms | **4 ms** |
| 拖动单帧(tea-app 首页,60 帧平均) | 7.17 ms | **3.74 ms** |
| 远程图片二次打开(磁盘缓存) | 6132 ms | **11 ms** |
| 首帧「建树+样式」(三条中文字体栈的应用) | 3093 ms | **955 ms** |
**双端一致性**(同一份源码:一边原生渲染,一边编译成 HTML 用 Chrome 截图,逐像素比):
| 示例 | 页数 | 与 Chrome 的变化像素比 |
|---|---|---|
| `sample-app`(商城) | 15 | **4.55%** |
| `news-app`(资讯) | 6 | **6.33%** |
剩余差异集中在粗体字形与亚像素文本位置。这个数字是**回归判据**,不是宣传语 ——
任何渲染改动都要先看它有没有变坏。
**跑得起来的真实工程**(不是 demo 级的自造样板):
| 小程序 | 规模 | 说明 |
|---|---|---|
| `sample/tea-app` | 36 页 | **uni-app 编译到 mp-weixin 的产物**:259KB Vue 3 运行时 + 60 个 CommonJS 模块,用 `Component()` 构造器定义页面 |
| `sample/real-sample` | 8 页 | 微信官方 demo |
| `sample/sample-app` | 15 页 | 商城闭环:购物车 → 确认订单 → 下单 → 订单/物流,跨页状态走 `wx.storage` |
| `sample/news-app` | 6 页 | 资讯闭环:频道横滑、正文字号即时生效、评论、收藏持久化 |
**回归规模**(每次改动都要全绿,命令见[测试与回归](#测试与回归)):
| 判据 | 规模 |
|---|---|
| 单元测试 | lib **471** + bin **9**;其中宿主纯逻辑 41 条(手势仲裁 13 / 侧滑返回 12 / picker 面板 10 / 触摸状态机 6)、SDK 指针链路 7 条 |
| 场景画廊 | **65** 张,固定动画时钟下逐字节可复现 |
| 局部重绘校验 | 增量重绘 vs 强制整帧,5 个场景**逐字节相同** |
| SDK 与桌面一致性 | sample 15/15、news 6/6 **逐像素相同** |
| 指针层与覆盖层 | 6 张确定性交互快照 + 2 条手势断言 + **空转守卫** |
| 编译警告 | **0**(`cargo check --all-targets`) |
### 能力边界:能做什么、明确不做什么
**已实现**(详表见[支持范围](#支持范围)):
- **24 个组件标签**(容器 / 文本媒体 / 表单三类,含 `swiper`、`scroll-view`、`picker-view`);
- **事件**:六种绑定前缀(`bind` / `catch` / `capture-bind` / `capture-catch` / `mut-bind` / `bind:`)
与完整的捕获-冒泡链,触摸序列按微信语义产出(slop 取消 tap、350ms longpress、被滚动接管补 `touchcancel`);
- **模板**:`wx:if` / `wx:elif` / `wx:else` / `wx:for`、`block`、表达式引擎、`class`/`style` 绑定、
页面 json 的 `usingComponents`(自定义组件含样式隔离与独立数据作用域);
- **样式**:选择器与特异性层叠、继承、`@import`、CSS 变量、`rpx`、`calc()`、渐变、阴影、
`transform` / `transition` / `@keyframes`;
- **API**:39 个 `wx.*`(`request` 含 `abort`、storage 全套且跨启动持久化、Toast/Loading/Modal、
五个路由 API、设备信息全套、`createAnimation`、`createCanvasContext`);
- **生命周期**:三级共 28 个钩子(App 7 / Page 13 / Component 5 + `pageLifetimes` 3),
页面 `onShow/onHide/onResize` 会联动页面内组件;
- **交互**:惯性滚动与临界阻尼回弹、手势仲裁、侧滑返回、下拉刷新、picker 底部面板、局部重绘;
- **媒体**:Canvas 2D(完整 2D 上下文 + 设备分辨率后备缓冲)、`