DaraW

Code is Poetry

GMTC 北京 2021 小程序开发实践专场所见所想

前言

七月初有幸参加了 GMTC 北京 2021 小程序开发实践专场,一共听了四个演讲主题,分别是:

  • 京喜跨端小程序开发实践
  • 智能生成云端一体代码,提升小程序开发效率
  • 滴滴出行基于 Mpx 的复杂小程序解决方案
  • 砥砺前行—抖音小程序前端渲染框架演进

在密集的听完一下午的分享后,感觉信息量还是比较大的,引发的思考更是不少,下面就慢慢道来。

京喜跨端小程序开发实践

第一场是京东旗下京喜产品的前端工程师带来的分享,主题围绕小程序和 H5 跨端开发展开,恰巧的是我们目前也在做类似的事情,所以这一场听的格外仔细。

项目背景

京喜作为京东的社交电商产品(对标拼多多),由于又得到了微信的「发现页」流量入口,所以业务体量比较大:

  • 整包体积 20M
  • 100+ 开发者,且分了不同的团队
  • 峰值亿级请求量
  • 跨多端:多个平台的小程序 + 多个容器内的 H5

跨端实践

整个京喜小程序跨端方面基于京东自研的 Taro 框架,比较值得关注的是京喜由于体积大、存在历史遗留问题,每个页面就是一个仓库,可以针对单页进行编译开发、多端调试,单页构建完成后最终会再合并构建到整个项目中,相当于会做二次构建。另外针对这个架构,整个京喜小程序内存在着多种技术栈。

在跨端语法这块,基于 Taro 的跨端开发能力,详细可以参考 https://taro-docs.jd.com/taro/docs/envs ,这里不再赘述:

  • 内置环境变量
  • 条件编译
  • 多端文件后缀

首页性能优化

讲师经历了首页性能优化项目并取得了一定成果,在这之前京喜首页面临了一些性能瓶颈:低端机不流畅、机器发热等问题。
首页性能优化一共从三点展开来做:虚拟列表、低端机降级、体验评分。

虚拟列表

虚拟列表的落地场景是首页的商品列表,商品列表的特点是长列表、功能复杂,这就导致了滚动两屏后容易卡顿,所以做了虚拟列表的落地:

  • 结构改造
    虚拟列表常见的一个场景是整个页面长列表,但这里比较特殊,不是整个页面都是列表,而是只有页面的局部。所以这里组件本身不太好去监听页面的 scroll 事件,只能基于 Taro 的事件中心从组件外传进来滚动事件。
  • 卡片高度计算
    商品卡片的一个特点是不定高,直观的思路就是通过 DOM API 去获取卡片 DOM 的高度,但这里有个问题是小程序中的 DOM API(SelectorQuery)性能不太好,在这样的列表场景下大量去调用更加容易遇到性能问题。
    既然 DOM API 不可取,那可以从业务入手,可以发现卡片的高度可以根据内容计算出来,把各个组成部分的高度加起来即可。这个思路确实可行而且性能很好,但还是遇到了一个小问题:小程序计算出来的高度和实际 DOM 高度不一致,经过排查后发现是 rpx 换算规则导致高度不符合预期,1rpx 在绘制的时候会转换成 0.42px,而在计算的时候会转换成 0.426667px,积少成多这里就可能会带来偏差。解决办法也很简单,计算逻辑针对小程序平台做一下区分即可。


  • 白屏优化
    至于白屏优化则比较简单,是常见的骨架图,这里不再赘述。

低端机降级

低端机降级首先是划分标准,这里一开始研发先定了一个标准,根据年份、系统、上市时间来判断是否是高/低端机。但落地后发现高端机标准定低了,有些高端机也会卡顿,以及高端机的比例和产品渠道的用户划分比例也不符合。在重新校准参数后,最终大约 2/3 为高端机,1/3 为低端+未知,比较符合事实。
在明确低端机的标准后,针对低端机就可以做一些优化了:

  • 轮播图改单图
  • 动图改静图
  • 大图改小图
  • 去除跨模块/时间关联(简化弹窗等逻辑)

体验评分

小程序 IDE 自带了一个体验评分,其指标主要由以下几点组成:

  • 首屏 DOM 数(控制在 1000)
  • 图片大小
  • setData 调用频率、内容大小
    京喜针对这些优化项,将体验评分做到了全 100 分。

代码体积优化

小程序有着包体积限制,超出则无法发版,所以需要优化代码体积。
常见的收益较大的方式:

  • 分包
  • 依赖分析,移除未使用的内容
  • 避免使用本地资源尤其是图片
  • 所有类型都进行压缩和注释清理

除此之外,京喜首页还针对 CSS 做了一些更加精细化的优化:

  • 继承属性:
    有些 CSS 属性只要在全局定义过之后,里面是可以不用再重复定义的,例如 color line-height font-family font-size 以及 @font-face 字体定义。这里讲师举了个例子,在他们的首页项目中 @font-face 重复定义了 231 次。经过了这一系列优化后,首页模块体积一共减少了 10K,占项目总量 630K 的 1.6%。

  • 样式补全:
    例如 display: flex 在最终页面中往往会通过各种方式再加上一行 display: -webkit-flex 以便支持多个宿主环境。
    在小程序 IDE 中,在上传代码的时候支持选择样式自动补全,当然框架一般也会通过 PostCSS 在编译阶段补全。那么到底该选择哪个呢?经过京喜的对比尝试,发现小程序 IDE 自带的补全在使用上没啥问题,同时还可以避免 PostCSS 编译加入的样式补全代码的算进小程序的体积里,所以最终关闭了框架的补全,选择使用了小程序 IDE 的补全,这样使得包体积减少了 58K,占项目总量 630K 的 9%。

  • 样式命名优化
    JS 代码在编译的过程中 UglifyJS 可以将函数名简化成 a b c 这样;那么同理我们可以把组件内的 classname 进行优化。
    默认情况下 CSS 和 JSX 分离,很难把 JSX 中的 classname 和 CSS 中的 classname 建立关联。虽然可以做,但比较麻烦。

所以京喜选择了 CSS Module 来建立样式和 JS 关联:

这样优化后,体积又减少了 50K,占项目总量 630K 的 7.9%。

这一系列的优化使得体积从 631K 减少到了 465K,在小程序包体积寸土寸金的背景下,效果是比较显著的。

系统监控

京喜小程序借助了 Badjs 来做质量数据收集,但会遇到一些问题,线上质量数据数量大、模块划分不明显、实时反馈信息少。因此建立了一些可用率监控平台,配合数据,建立环比、同比等规则进行报警处理。

Q&A 环节

  • 为什么首页用 Taro 其他页面可以用其他的技术栈
    • 首页是实验田,其他业务会有自己的选型或者历史遗留问题
    • 首页单页编译,避免其他的业务影响,只对首页编译
  • 评分卡中什么页面是小程序评分比较好的,依据来源
    • 参考的 Google Chrome 的
  • 灰度策略
    • 开发者灰度体验包
    • 逐步放量

思考

业务

这场演讲是业务角度的分享,目前我们所做的某个细分方向业务中,主推的也是一个跨多端(微信内 H5 和微信小程序)、包含多个业务模块的产品,京喜的分享给我们指明了一些未来发展的道路。

跨端

在跨端实践这块,Taro 提供的方式已经足够我们很好的去做多端开发与调试,以及代码的维护。
虽然 Taro 跨端开发的学习成本很低,但特别是新同学加入后,还是会有一个短暂的适应期,早期在人不多的时候,我们往往会在 CodeReview 阶段去避免不符合跨端最佳实践的代码进入最终的 codebase,但现在团队人员规模逐渐增加,我们尝试通过 Linter Plugin 来沉淀我们在过去的 CodeReview 中积累的跨端经验,目前已经初步在项目中落地了,且确实有所见效,统计 CodeReview 数据发现落地后跨端语法类的问题下降了 90+%,对于这块未来我们会做更多的详细的分享。
因此在这点上,我们可以比较自信的说做的是比京喜好的,至少我们 CodeReview 比较严格,没有京喜首页模块重复定义了 200+ 次字体文件这样的问题。未来我们也会继续迭代 Linter Plugin,将更多的实践沉淀到其中,将问题扼杀在编写代码的过程中。

构建

在小程序包体积的方面,我们已经即将超出 2M 的单包体积限制。这块京喜给我们指了一条道路,也是 Q&A 环节大家都比较感兴趣的一点,京喜在构建上做了类似客户端团队的拆分工作。
像今日头条、抖音、支付宝等这类包含了多个业务逻辑的巨型 App,往往会跨团队研发,在开发中启动整个 App 的所有模块肯定是不现实的,所以一般都会去做 App 功能模块的独立开发与构建。京喜小程序应该是将项目分成了多个独立分包,所以可以实现单个模块的独立开发和构建,最终再将各个模块再合并构建一次产出最终的小程序。
不同于京喜各个页面的跨端是独立的,H5 应用各个页面是分离的,我们整个应用也提供了 H5 形式的 SPA,所以我们也不能完全照搬京喜的构建经验。下半年我们将重点探索 Taro 框架下完美支持跨端的分仓库构建,提升开发体验和效率。

性能

上半年我们业务上经历了从无到有的过程,业务的起量也是最近几个月刚开始爆发,所以在过去的半年内,我们主要围绕着研发提效等角度去做技术规划,性能方面没有遇到比较大的问题,所以还未曾特别关注过性能数据。预计下半年业务稳定后,我们将会在性能方面去做一些优化工作,京喜的经验将是我们的重点参考资料,例如低端机降级等。

监控

小程序像 Web、却也像 App,字节跳动内端端监控目前针对小程序的支持其实是不够完善的,和 Web + App 相比不少监控指标缺失。做一做基本的监控和报警还可以,但想细化数据却比较难。下半年我们在性能优化的工作中,将尝试能不能积累一些跨端的性能优化经验,并帮助端监控完善这块。

总结

这场分享是四场中和业务开发最贴近的一场,也是四场中引发我的思考最多的一场,会后和讲师做了一些简单的交流,并加了讲师的微信,后续也会和讲师做进一步的技术交流,学习友商团队的更多经验。

智能生成云端一体代码,提升小程序开发效率

这一场是 DCloud 的 CTO 做的分享,主要讲小程序 PaaS 相关的内容。

前/后端协同模式演变

讲师将前后端协同模式划分了几个阶段:

  • 第一阶段:1990~2005 年,HTML 诞生后,页面主要由 ASP/JSP/PHP 等模板序言实现的动态网页+ JS + CSS 组成
  • 第二阶段:2005~2018 年,AJAX 的出现和移动互联网的成熟,使得各个端合在一起变成了大前端,WebApp、原生 App、小程序等提供了较好的用户体验,但开发效率比较低。在这个阶段,跨端逐渐成为主流,例如 ReactNative、Weex、Taro、uni-app、Flutter 等技术方案,跨 H5、iOS、Android、小程序多端。
  • 第三阶段:2018~今天,Serverless 的出现,使得开发者可以不需要搭建服务器,在端上直接操作数据库,在保证用户体验的同时提升了开发效率,演变成了重前端、轻后端的架构,业务逻辑迁移,前端承担更多的责任。

小程序云开发的先进性和局限性

  • 表级权限,场景受限制,稍微复杂一点的规则就没法做了
    • 积分大于50的注册用户,才能评论文章
    • 仅管理员可设置精华帖,阅读量系统自增
  • 参数合法:落库的时候难做数据校验
  • 数据割裂:小程序厂商支持程度和标准都有差别,用了多家的云就会导致数据分散在各个云数据库中,难以一起统计

uniCloud 的改进探索

  • 跨云跨端:uniCloud SDK + uni-app runtime

  • 规范制定:借助 JSON Schema 支持权限配置、字段校验、组件绑定,设计了一套 DSL 来做权限控制和字段校验

  • clientDB 云端一体化:前端 DB SDK + 数据组件

智能生成小程序代码(Schema2Code)

大家大多接触过类似的 LowCode 方案,这里不详细讲了,只贴一张图,更多的可以看下 PPT。

未来的轮子生态

在当下前后端分离导致前后端轮子生态割裂,前端框架和后端框架之间有着很大的沟壑;在前端承载越来越多职责的背景下,未来可能会往云端一体化的方向去发展。
但目前的云开发虽然高效,但局限很大,难以满足复杂的业务,未来探索的方向包含两个:对复杂业务场景的更好支持(跨云跨端、精细化权限、严格字段校验、前端数据组件)和基于云开发模式下的代码生成思路。

Q&A 环节

  • 云端 MongoDB 数据库可能是低版本不支持事务,那么怎么实现事务
    • 云端厂商版本选用高版本
    • 云端做事务,前端做不了事务

思考

注意 DCloud 同时也是开源 uni-app 的公司,uni-app + uniCloud 这种云端一体化的发展方向也算是 DCloud 这种 PaaS 云服务厂商在当下的最佳方案。
在这块由于我没有怎么关注过,也没有落地的经验,所以不献丑了。公司内对应的最适合做这种方案的团队应该就是轻服务了,轻服务和 uniCloud 端 SDK 的使用方式也确实很相似,只是轻服务是 Web 体系内的,uniCloud 是小程序体系内的。

滴滴出行基于 Mpx 的复杂小程序解决方案

这一场是滴滴的高级专家工程师分享他们开源的 Mpx 小程序框架。
滴滴出行小程序复杂性介绍

  • 复杂的业务逻辑(多场景/多状态/重地图)
  • 庞大的业务体量(18+业务线/营销业务闭环/总包 体积20M)
  • 跨团队合作开发(10+业务团队)
  • 跨多端投放(微信/支付宝/QQ/字节/Web)
  • 多语言支持(支持中英文切换)

Mpx 小程序框架概览

小程序优先的增强型跨端开发框架

不同于业内主流小程序框架追求将 Web MVVM 框架(React/Vue)运行在小程序环境中 的思路,Mpx以小程序原生的语法和技术能力为基础,借鉴参考了 Vue 框架中的优良语法设计,通过编译和运行时手段对其进行增强扩展,让用户能够以接近 Vue 的体验和方式来进行小程序开发,并保持与原生小程序一致甚至更优的性能与包体积。

业内小程序框架异同对比 - 性能与效率的权衡


目前业界比较常见的两种框架类型是静态编译型和动态渲染型:

  • 静态编译型以 uni-app 和 Taro2 为代表:
    • DSL 层 uni-app 为 Vue,Taro2 为 React
    • 技术方案则是重编译期,将 Vue / React 直接编译到小程序原生
    • 优点是性能比较好,因为编译后的代码接近原生写的小程序;且能够具备 Web 迁移能力
    • 缺点也比较明显,因为通过编译实现,Web 框架使用能力受限,例如 Taro2 中 JSX 循环限制很大,容易写出 bug
  • 动态渲染型,运行时框架,目前的一种趋势,以 Taro3 为代表:
    • DSL 层可以随意使用任何 Web 框架
    • 技术方案是重运行时,借助递归动态模板模拟 DOM 环境,所以可以直接将 Vue / React 等视图层的框架直接跑在小程序上
    • 优点和静态编译型相反:Web 框架的使用不受限制,可以自由使用几乎是所有的语法,因此迁移 Web 时也很容易
    • 缺点也和静态编译型相反:性能相对来说差一些,setData 发送数据的开销和渲染的开销都相对大些

这两类实现方式因为以 Web 的方式去做小程序开发,使用部分小程序原生特性也会有些麻烦。

Mpx 则走了第三条路,原生增强型:

  • DSL 层基于小程序的语法去做拓展,因为本身语法就类似 Vue,所以拓展后是类 Vue 的语法,所以编译
  • 技术方案也和 Vue2 相似,编译+运行时
  • 优点时性能可以做到极佳,因为 Vue 在编译期可以做 AOT,而运行时又有精细化的依赖追踪,所以可以保证 setData 时的更新细粒度几乎是最佳;同时由于是小程序的语法拓展,所以和原生小程序开发体验也比较接近,原生语法的使用不是很麻烦
  • 缺点则是和小程序靠拢过紧,不是完整的 Vue,Web 迁移能力比较弱

总结起来就是 Mpx 在设计的时候做了一些权衡,牺牲了一些 Web 开发的效能,换取更高效的小程序性能。

Mpx 全局架构

和业界常见的框架类似,分为编译层和运行时。编译层完全基于 Webpack,通过深度定制的 Webpack Plugin 来实现编译;在运行时核心模块包含小程序平台差异抹平、响应式逻辑等,同时还包含了一些周边库,例如 Mock 等。

Mpx 支持能力概览

  • 开发体验与效率
    • 数据响应
    • 跨端开发
    • 模板指令增强
    • CSS 预处理
    • ES6+ 支持
    • TypeScript 支持
    • i18n支持
  • 质量与性能
    • setData 优化
    • 按需构建
    • 分包资源处理
    • 包体积优化
    • 包体积分析
    • 单元测试支持
    • SourceMap 支持
  • 跨团队开发
    • npm 支持
    • 多实例 Store
    • Packages 规范
    • 原生渐进迁移

Mpx 使用情况

Mpx 支持了滴滴内部几乎所有的小程序业务,例如滴滴、青桔、橙心优选等,并在 GitHub 上获得了 2.8K Stars。

数据响应与性能优化

数据响应编程

类似 Vue 的语法,这里不再赘述。

Mpx 数据响应实现

Mpx 参考了 Vue2 的实现,基于 defineProperty 实现了数据响应式。其中和 Vue 的区别在于,Vue 因为是 Web 平台,可以直接操作 DOM,在数据发生变化后最终驱动了 DOM 的变动;而小程序中无法直接操作 DOM,所以 Mpx 中数据发生变动后驱动的是小程序的 setData,由小程序去负责渲染。

未来当小程序宿主环境兼容性变好了之后,Mpx 也会考虑更换为 Vue3 的 Proxy 来实现。

数据响应下的 setData 优化

小程序 setData 性能优化建议一般是三条:避免 setData 的数据过大、避免 setData 的调用过于频繁,和避免将模板中未绑定的数据传入 setData。
一般框架的实现有三种:

  • 全量设置:将整个模板对应的数据传入,这样实现最简单粗暴,性能也是最差的
  • 深度 diff 设置:将上次和这次的数据做一个深度 diff,进行增量的 setData,这种实现方式可以做到避免 setData 数据过大和调用频繁,但做不到避免将没有用到的数据传入,因为这种情况下数据和模板没有去做关联,是不知道模板里真正需要的数据到底是哪部分的
  • 只设置模板上使用且发生变化的最小数据:这是 Mpx 的做法,也是性能最好的实现方式

基于 render 函数的 setData 优化方案

在 Vue 中,Vue 通过依赖收集和追踪,最终能够做到精细化的 DOM 更新,那么在 Mpx 中类似的,通过依赖收集和追踪,将数据变动改为和 setData 进行关联,可以做到精细化的 setData 更新。
同时 Mpx 可以和 Vue 一样通过 nexttick 来合并渲染。

render 函数生成流程

第一次编译将模板编译为原始的渲染函数,但这个原始的渲染函数没法直接在运行时使用,所以会基于 Babel 再做二次编译,去关联 this 上下文,便于之后可以直接在运行时使用。

Mpx setData 优化效果

在滴滴使用 Mpx 的项目中,没有手动去做 setData 优化,但性能一直很优秀。Mpx 的性能在 benchmark 场景下也是最优。

编译构建与包体积优化

Mpx 编译构建

由于 Mpx 编译构建基于 Webpack,所以整个依赖分析、编译构建的流程和普通的 Vue 项目相似。

小程序资源依赖树

小程序项目虽然看起来资源比较离散,但实际是通过 app.json page.json 来描述了资源依赖关系,在做依赖分析的时候可以从这点入手。

npm 支持

微信小程序官方的 npm 支持比较差,有诸多问题例如全量复制 npm 包,Mpx 做了优化,解决了这些问题。

分包构建

根据用户分包配置,串行对主包和各个分包进行构建。同时还能自动生成 SplitChunksPlugin 配置,将项目中的公共模块输出到主包或分包 bundle 中。
对于独立分包也做了支持,独立分包可以在没有主包的情况下运行,所以在独立分包的场景下会给独立的 runtime 和相关资源,保证其能够独立运行。

小程序包体积分析

Webpack 生态中,包体积分析工具都是针对 Web 来设计的,在小程序场景下有一些缺点,拿 webpack-bundle-analyzer 来举例子:

  • 只能统计分析 js 模块体积,而小程序输出中包含大量非 js 资源
  • 因为 Web 体系内没有分包的概念,所以无法以分包维度进行体积统计分析,但小程序的体积限制是设置在每个分包上的
  • 体积来源难以追溯
    针对这些问题,Mpx 内置了包体积分析工具,支持以下特性:
  • 全量资源(js 与非 js)体积统计分析
  • 支持以分包维度进行体积统计分析
  • 支持通过来源配置分组进行体积统计分析
  • 支持按照分组和分包维度配置体积阈值进行体积管控

在滴滴的业务场景下,各个业务方会在一个小程序里进行开发,那么去分析每个业务的体积就非常重要。
因此 Mpx 包体积分析设定了 entry 的概念,根据 entry 进行分组。接着从 app.json 开始,对整个依赖树做一次深度优先遍历,将他们的资源大小信息根据分组进行归类。如果这个资源都是这个分组使用的,那就是这个分组的 self size,否则算 shared size。除此之外,滴滴还做了一套可视化的页面,用来展示上面包体积分析得到的数据。

小程序跨端

Mpx 1.0 针对各个平台都做了增强,到了 2.0 则以微信小程序作为标准,由框架来进行跨平台转换支持支付宝、百度等小程序平台。

在设计和实现的时候,针对静态的部分主要通过编译转换来做,针对动态的部分主要通过运行时来抹平差异,对于框架无法处理的差异提供了条件编译的能力,这个和 Taro 等框架类似,有几种写法:

  • 文件维度条件编译,通过文件后缀来标识平台,例如 card.wx.mpx card.ali.mpx
  • 区块维度条件编译,在单文件组件中,script 标签可以配置 mode 属性,来指定这块逻辑适配某个平台
  • 代码维度条件编译,通过环境变量来区分 if else,并支持编译时移除条件死分支
  • 属性维度条件编译,当平台差异只存在于节点属性维度时,使用代码维度条件编译较为冗余,子树难以维护: <view class@wx="wx-container" class@ali="ali-container"></view>

在输出 Web 平台方面,因为语法借鉴了 Vue,所以主要还是基于 Vue 来实现。在编译时将小程序基础能力例如页面路由能力的实现注入进去。其中组件这块,Mpx 直接将 view、text 等标签换成了 HTML 中的基础组件,例如 view 换成了 div。

Mpx 多语言支持

由于小程序是双线程架构,应该尽量减少 setData 数据传输,所以 Mpx i18n 方案有两种,一种是 wxs 模式,把多种语言文案放在模板中,这种方案没有额外的通信开销,性能比较好,但问题是会导致包体积增大,语言包同时存在于 wxs 和 js 中,且因为文案在编译期就决定了,后续无法异步加载;另一种方案是 computed 模式,优缺点与 wxs 模式刚好相反,通信开销较大,但包体积占用较小,且可以实现异步加载语言包。

展望与未来

  • Webpack5 构建升级,大幅提升构建速度
  • Vue3 数据响应升级,支持 composition api
  • 单元测试进一步完善,完整支持 jest mock
  • E2E 自动化测试支持
  • Mpx-cube-ui 跨端组件库开源
  • 支持局部组件运行时渲染

Q&A 环节

抱歉这里记不太清了。

思考

在分享我的思考之前,先声明下利益相关,我是 Vue Contributor & Taro Contributor,所以不保证接下来的内容不会夹杂私货
说实话刚开始听还是有些失望的,本来预期是和第一场类似,主要从业务落地角度讲滴滴出行的小程序实践,没想到没讲几分钟,就变成了一个纯 Mpx 技术细节的分享。纯技术细节的分享更适合以文章的形式展开,以演讲的形式提供很容易导致听众跟不上思路,好在 PPT 内容做的还是比较详细的,倒也不是太大的问题。
在听完整个分享后,虽然根据我们的业务背景和团队相关同学的背景,我依然不会去选择使用 Mpx,但 Mpx 的一些工作也给我们指引了一些未来的道路,所以这场倒也不算白听了。

小程序框架设计漫谈

分享中虽然针对业内的小程序框架设计做了不少点评,但我这边还是想去展开聊聊。
Web 发展至今,HTML、CSS、JS 基本已经成了事实汇编语言,只要是构建稍微有一定复杂度的页面应用,几乎不会再有人去拿三剑客从头写了,大家都会选择一个像 Vue、React 之类的 Web 框架。
小程序中三剑客的设计尽管和 Vue 极其相似,但还是逃不掉成为事实汇编语言的命运,当然这背后的原因是很多的,DSL 设计的合理性暂且不谈,程序员永远都是懒惰的,如果能用熟悉的 Web 框架去开发,谁又愿意去学习一套新的语法呢?这也是小程序框架例如 uni-app、Taro 出现的背景,甚至连微信小程序官方都在推荐 Kbone 这样的框架。

重编译型框架

在第一代小程序框架中,大家的设计思路都是既然小程序语法和 Vue、React 像是吧,那就干脆把 Vue、React 编译到小程序吧。
这种方案最直接的问题就是 Vue、React 的语法是针对 Web 平台的,在小程序中很多使用会受限,例如以 Vue 语法为代表的 mpvue 中无法使用复杂的 JS 表达式,以 React 为代表的 Taro 1/2 中 JSX 里无法使用复杂的 map 循环。
导致这些问题的原因基本都是编译成本的问题,简单场景下的 Vue template 和 React JSX 都可以对等的编译到小程序的模板,但复杂语法的支持工作量是非常爆炸的,特别是 JSX 灵活性本质就是 JS,将标准的 JSX 编译到 template 几乎就是在对 JS 做 AOT 的工作量。
这里多提一嘴,有一个叫 solid.js 的框架,对 JSX 做了 AOT,当然它的 JSX 的语法使用是受限的。感兴趣的同学可以自行阅读其文档。
在这类框架中,最终编译出来的小程序代码和原来写的 Vue、React 代码都是很相似的,自然的性能就和小程序原生很贴近。

重运行时框架

大家不想再使用受限的 Web 框架语法,但小程序又不让直接碰 DOM,那怎么办呢?
这时有人天才般的发现了「动态递归模板」技术,最早是谁想出来的这里不去追溯了,意义不大,但这个技术本身比较有意思,这里展开聊下。
小程序模板往往支持 template 递归引用,例如用下面这段伪代码就可以做到动态的 view 里面嵌套 view:

1
2
3
4
5
<template name="view">
<block a:for="{{item.children}}" key="{{item.id}}">
<template is="view" data="{{item: item}}" />
</block>
</template>

那么就有人想到了,是不是可以做一个万能模板,这个万能模板能够描述一切的页面,模板和 JS 之前的 data 就是 VDOM 的 AST?
如果大家有使用 Taro3 的项目,可以看下编译产物的目录,base.wxml 就是这个万能模板,而小程序 IDE 中的 AppData 就是页面的 AST:

这就很舒服了,可以模拟出一切 DOM 操作。
这种方案很多人都会说性能不太好,这里也详细聊下,为什么会说性能不好:

  1. data 是整个页面的 AST,而小程序是双线程模型,分为逻辑层和渲染层(详细可以阅读这个系列的文章 https://zhaomenghuan.js.org/blog/wechat-miniprogram-principle-analysis.html),也正是这种模型限制了 DOM 操作,只允许将可序列化的页面数据从逻辑层发给渲染层,也就是小程序中的 setData。大家应该都知道,线程间的通信是有开销的,那么 data 越大开销就越大。尽管这种实现方式可以做到合并更新,但 data 包含了 AST,data 大小必定不会小。
  2. 不管用不用到那么多节点,base.xml 永远都是那么大,就像打车的起步价一样,对于轻量级的场景,这是比较吃亏的,这个模板就已经 63K 了。
    综上,尤其是 benchmark 场景下,动态模板的框架很难在重编译的框架面前占优势。

但注意,业务往往和 benchmark 不一样,除了框架开发者和新手之外很少有人没事去写 Todo MVC。尽管动态模板的方案有起步价,但大家再去观察各个页面编译出的模板文件,会发现里面都是直接引用的 base.xml,新增一个页面模板这块的体积增加的开销几乎为 0(在打包的时候还能被 Gzip 压缩掉),因此只要你的页面复杂度和数量达到一定阈值,体积上的缺点会反过来变成优点,就像我们的产品目前页面的数量已经差不多上百了,还有大量的组件,在体积上选择运行时的框架不一定比重编译的框架差。

再谈 Mpx

这里不去评价 Mpx,毕竟人家已经说了,是根据他们的业务背景而设计的,他们有一些业务是原生的小程序,Mpx 作为增强型的框架,迁移成本必定比前两者小,但我也很难去称它为第三代的框架。
Mpx 给我们的思考主要是两点:
一是造轮子要考虑业务背景,这也是讲师作为 Mpx 核心开发者强调的一点。
就像去年我们团队内做了一个叫 RingJS 的 Node.js 框架并顺利落地了(这点未来有机会再详细谈谈),其中一个重要的点在于 RingJS 在我们中后台都是裸 Koa Node.js 应用的背景下,迁移成本极低,如果我们强行使用 Nest.js 等框架,恐怕要么迁移工作量爆炸,要么落地遥遥无期。

二是在市场上已经有几个框架霸占了几乎是所有的市场时,我们怎么去找自己设计的框架的定位?
在 Web 世界是 React、Vue、Angular 三大框架霸占了所有的市场,尤雨溪也曾在知乎的一个回答里(见 https://www.zhihu.com/question/332293687/answer/738922582 )说过这件事,「在三大已经几乎霸占市场的情况下怎么推一个新框架,可以看看 Rich Harris 怎么推 Svelte 3 的」。
同样的,从 GitHub Star 来看重编译类框架中 uni-app、mpvue、Taro 1/2 和重运行时类框架中的 Taro3 已经占了几乎是所有小程序框架的市场,再做一个 mpvue 或者是 Taro3 并没有意义,你无法说服用户放弃使用更知名、更稳定、生态更好的现有框架来用你的框架。
我感觉在这一点上,Mpx 找到一个比较好的点,和 Svelte 一样,从性能、轻量作为入手点,贴近 Vue 的语法、使用增强型设计减少学习和落地的成本,是能够吸引一部分原来是原生小程序的开发者试水 Mpx 的。

包体积分析

Mpx 中谈到的小程序包体积分析工具的局限,其实在各个框架中都有,例如 Taro 等,在我们接下来的性能优化工作中,如果能有一个配套 Taro 的包体积分析工具,一定能事成功倍。补齐 Taro 生态下的包体积分析工具,将成为我们的性能优化工作中的重要一环。目前在这块我们也已经初步有产出出来了,未来再做相关的分享。

总结

纵然 Mpx 并不适合我们的业务场景和背景,但 Mpx 做的很多尝试还是能够引发我们的思考,启发我们的工作。

抖音小程序前端渲染框架

最后一场是来自我们字节同学的分享,主要是小程序厂商内部的视角。比较有趣的是,因为讲师刚去深圳出差过,所以当时还在居家隔离中,是远程接入的。
另外讲师也是我的好朋友,私下也有过很多的技术交流,演讲的部分背景和内容以前也曾经聊过,所以这场听起来还是比较轻松的。

小程序渲染背景介绍

抖音小程序是什么?

这里想必不用再介绍了,目前已经接入了不少业务,其中就包含教育,例如瓜瓜龙和清北。

小程序运行时架构

首先最底层是接入宿主 App,提供小程序运行环境和管理能力;在这之上是 Native 运行时,可能是常见的 Android 和 iOS,也可以是 IoT 设备,提供页面堆栈和端能力;再上面是跨端信道,支持多端通信;最上层就是 JSSDK,也就是小程序的 JS 运行时,提供组件、API、渲染等核心能力,这也是接下来要重点关注的内容。

小程序运行流程

用户的小程序会经过四个流程,最终被运行起来:

  • 编译:小程序代码会经过一层转义,转成 JS 引擎能够识别的代码,并进行打包
  • 下发:在打开小程序的时候由端来下发小程序
  • 加载:下发完成后由端来加载小程序
  • 运行:加载完成后小程序开始运行
    接下来关注的就是编译和运行两个流程。

小程序渲染背景

小程序渲染要同时服务内部和外部开发者,内部主要是 view、text 等内置组件的渲染,外部则是开发者需要在页面上使用的自定义组件等渲染,这两者是很紧密的,但诉求又不一样,在一些方面是完全相反的。

在内部,会比较重视性能,对于高抽象程度的设计一般是难以接受的,因为往往抽象会带来更大的开销;内部需要更能接触底层和本质,开发的学习成本会较高。
对于外部开发者来说:

  • 对小程序语法一致性要求比较高,不能因为换了一个平台和渲染框架,就要重新学习和开发一遍
  • 小程序的双线程渲染模型决定了线程间通信的数据必须要是可序列化的,所以类似 Proxy、Observable 等思路是行不通的
  • 对性能有要求
  • 对可测试性也会有要求

总结来说,开发者会关注开发的限制,又关注学习成本,不接受较高的学习成本。根据上述的背景,小程序第一版的渲染框架做了两套分离的组件设计。

这样一套分离的方案在早期带来了一些好处:

  • 抽象不一样,可以并行去开发
  • 分离的设计和实现更容易做单独的测试

但这种分离的方案却可能带来更多的问题。如图所示,内部组件是基于 Polymer 实现的 web component,使用原生事件避免合成事件带来的开销,并借助 DOM 生命周期去做了组件的生命周期;自定义组件完全不一样,因为有高定制化的需求,所以基于 VDOM 的方案去做了组件,事件也是合成事件,并且生命周期也支持自定义生命周期。

这样分离的方案给后续的开发和维护带来了非常多的问题,比如在两端都需要做一些开发工作的时候,渲染层的开发同学必须要同时维护两套,并且要保证两边是对等的;如果开发者同时依赖了内部组件和外部组件,也会发现生命周期是不对齐的。

针对这些问题,因此需要用一套统一的组件化机制,来将两者统一起来。

框架设计与思考

为什么不用XXX?

为什么不用现有的开源方案例如 React、Vue、Svelte?因为应用场景不一样。
当下的诉求是:

  • 轻量,可插拔

  • 独立绘制:小程序有多个平台,给多个平台提供统一的方案
    对于这两点,React 看起来都满足要求,React 也支持 custom render,但从 16 开始 Fiber 和 Concurrent Mode 已经很复杂很重了,React 代码不是很容易搞懂。业界里支付宝用了 React,现在已经比较难维护了,并且现在在细节上和微信是不对齐的。

  • 高度可定制性:不希望到处去打补丁

  • 性能优化空间:和第三点相似
    根据这四点,会发现现有的方案都很难去完全满足现有的需求,所以觉得自己去做一套框架。

框架设计迷思

框架的设计需要看场景和需求,这里从三点来看框架设计:

  • 能力提供:框架到底要提供什么样的能力,组件抽象?可插拔渲染 API?
  • 渲染机制:目前主流是 JSX vs template,两者各有优缺点,但目前业界的趋势是往静态方向发展,template 能在固定的场景下提供一些信息,便于去做 AOT
  • 状态管理:业界方案也很多
    • 变更监听
      • defineProperty / Proxy
        • defineProperty 有数组监听的缺陷
        • Proxy 在 C 端兼容性得不到保证,端上不一定支持
    • set-data
      • 例如 React 的 setState,只能走数据 diff,不知道精准的页面更新位置
    • 流式更新
      • 例如 Cyclejs,流式的更新可以做到精准的更新,但是因为 JS 原生不支持 lazy eval(惰性求值),一般都是依赖闭包实现,JS 中这种实现虽然性能好,但是内存压力大,在 IoT 场景下受限设备性能
    • 变更发现
      • 例如 Svelte,通过固定的模板语法,这种方案的问题是源代码和编译后的代码差异较大,调试难度增大

我们的选择

对于需要的特性,全部都要:渐进式编译优化 + set-data + 运行时配置。

在外部开发能力这块,页面的模板会被编译成 render 函数,并在 render 函数中做了一些插值,用来做性能优化,最终的外部组件内会调用 render 函数来做渲染。
在内部类似,原生组件通过 JSX 来描述,这里不用模板的原因是因为内部组件有很多对于 VDOM 的依赖,例如 Swiper 组件,Swiper 内部需要有 SwiperItem,如果用模板的话,很难对模板去做校验。

渲染优化技巧

ChildFlag

结构的稳定性对于性能优化很重要,如图所示的一段结构,第一部分 hello 很明显固定的永远是一个字符串节点,第二部分则会在 booleanVNode 两种节点之间不断切换。

在之前 React 对于这种场景,只会在运行时不断地 typeof 来做判断,这给运行时带来了极大的压力,现在 React 会针对这种场景去给节点做 memorize,类似 V8 中 hidden class 的优化,如果传递的参数是稳定的,V8 会跳过参数的校验直接运行。
通过给节点加各种标识符,线上 diff 的压力减少了 70%,也就是 diff 性能提升了三倍:

当然这种标识符让用户手写肯定不现实,但由于有编译期的存在,可以在编译期去自动加标识符。

VNode Immutability

例如图中的代码,如果 children 一直不会发生变化,就给 children 标记为不可变的节点,这样在后续的 diff 中就可以跳过它,减少 diff 工作量。

除了这些优化外,还做了很多的优化,例如 V8 core dump,去观察性能是否有劣化。

Benchmark

当然空口无凭,也有对应的 benchmark 来证明。benchmark 中抖音小程序前端渲染框架的名字叫 yaw,意为 yet another web-component。
yaw 目前还有一个未上线的优化版本,其中模仿 vue-next 和 Svelte 做了 PatchFlag 等优化,没有上线是因为发现这个特性会破坏 VNode 结构,导致 VNode 残缺,Vue 可以做是因为 Vue 没有很多对于 VNode 操作的依赖,这里不再展开去聊这个问题。

性能工具与调优

一共有三种工具,第一种是客户端性能分析工具,在客户端可以打开显示性能数据,可以看到 CPU、内存等实际的占用信息;第二种是 IDE 性能 trace 工具,可以细粒度的去看性能数据;第三种是 IDE 性能评分工具,可以粗略的去看性能优化点。三种工具详细的对比如下图:

思考

这次分享更多的是 inside-out,将小程序厂商内部的细节展示出来,让外部的开发者了解其设计和实现。
如果对各个 Web 前端框架设计与实现感兴趣的话,框架设计部分应该都比较了解了,如果不了解也没关系,核心点这块讲师讲的很不错,可以多研究一下 PPT。
在渲染优化技巧这块,如果比较关注 Vue3 的话,会发现 yaw 中的优化技巧不少和 Vue3 相似,重点的思路就是将固定的部分跳过,做更加精细化的更新(参考 VueConf 2019 SH 尤雨溪的分享 https://img.w3ctech.com/VueConf2019SH_Evan.pdf )。

写在结尾

最后感谢公司,能给我这次去 GMTC 2021 的机会,这里将我的所见所想总结成文,希望大家也能和我一样有所收获,也欢迎大家和我做进一步的交流和分享,谢谢大家。

Proudly powered by Hexo and Theme by Hacker
© 2022 DaraW