Day08:跨端技术概述 | 青训营笔记

82 阅读11分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 8 天。

什么是跨端

跨端技术的产生背景

随着业务的发展,产生了越来越多的业务场景,同时随着技术的发展,产生了越来越多的应用界面:PC端(Windows、Mac)、移动端(安卓、iOS)、Web端、IoT设备(车载设备、手表)等。

【常见痛点】

  • 各端功能几乎一致,各端需要单独配置研发人员
  • 开发、维护成本高
  • 安卓、iOS发版周期长

跨端技术方案的目标

  • 研发效率高
    • 学习成本低
    • 多端一致性高
  • 用户体验好
    • 稳定性高
    • 性能体验好
  • 动态化:支持动态化下发,满足日益增长的业务需求

跨端技术方案介绍

Hybrid方案

基于WebView容器渲染,通过JS Bridge把一部分系统能力开放给JS调用。

WebView容器的工作原理是基于Web技术来实现界面和功能,通过将原生的接口封装、暴露给JavaScript调用,JavaScript编写的页面可以运行在系统自带的WebView中。

【优势】

  • 对于前端开发者比较友好,可以很快地实现页面跨端
  • 保留调用原生的能力,通过搭建桥接层和原生能力打通

【缺点】

  • 跨端的能力受限于桥接层,当调用之前没有的原生能力时,就需要增加桥
  • 浏览器内核的渲染独立于系统组件,无法保证原生体验,渲染的效果会差不少

原生渲染方案

使用JS开发,通过中间层桥接后使用原生组件(而不是WebView)来渲染UI界面。

【优势】

  • 原生组件的渲染效率更高,而WebView容易出现卡顿问题

【缺点】

  • 相较于WebView而言,原生组件难以定制化实现部分样式,不如CSS灵活

常见实现方案:React Native

React Native是一个由Facebook于2015年9月发布的一款开源的JavaScript框架,它可以让开发者使用JavaScript和React来开发跨平台的移动应用。

  • JSI(JavaScript Interface)使得JS可以直接操作C/C++语言编写的对象,从而避免采用Socket通信而反复序列化造成的性能损失
  • Fabric:处理映射为OEM Widget的部分
  • Turbo Module:处理映射系统能力的的部分

自渲染方案

自行重新实现渲染管线,不依赖原生组件。

常见实现方案:Flutter

Flutter是Google 在2018年世界移动大会公布的开源应用开发框架,仅通过一套代码库,就能构建精美的、原生平台编译的多平台应用。

Flutter采用三层架构,Framework和Engine都是Hackable的:

绝大多数Flutter的使用者直接利用Framework层暴露的接口,采用Dart语言进行开发。

小程序方案

使用小程序DSL+JS开发,通过中间层桥接后调用原生能力,使用webview来渲染UI界面。这种方案和Hybrid方案类似,但是针对所在平台还做了特殊的优化。

常见实现方案:字节小程序

字节小程序是一种全新的连接用户与服务的方式,它可以在宿主(抖音、头条等App)内被便捷地获取和传播,同时具有出色的使用体验。

跨端技术方案对比

技术方案视图层逻辑层优点缺点
Hybrid方案WebViewWebView JS Thread开发成本低;
CSS 全集;
一致性好
性能中等
原生渲染方案原生组件JS Engine性能好可实现样式有限;
一致性一般
自渲染方案SkiaDart VM性能最好;
一致性好
可实现样式有限;
Dart生态不足;
开发成本较高
小程序方案WebView+原生组件JS Engine开发成本低;
CSS 全集;
一致性好
性能较好

基于小程序跨端实践

开发抖音小程序的步骤

  1. 下载小程序开发者工具
  2. 小程序开发、调试(开发文档
  3. 小程序上传

小程序的优化

优化的意义:

  • 留住用户
  • 提升转化率
  • 提升用户体验

性质指标

从小程序启动到完成分为以下几个关键环节:

  1. 小程序启动后会显示一个加载视图,在这个阶段会初始化小程序环境
  2. 初始化完成后开始加载小程序,然后开始第二个阶段“首次绘制”(Fist Painting,FP),这个时机开始渲染小程序的首屏页面
  3. 第三个阶段是“最大内容绘制”(Largest Content Painting),在这时已经展示页面中的最大元素,也是比较接近用户视觉感知效果
  4. 最后是主要元素加载完成,并且达到了可交互的状态

因此可以优化的性能指标之一就是从加载开始到LCP显示的耗时。

当然,启动环节耗时并不能完全真实反映用户体验,我们也借助实际用户行为,异常监控,作为辅助指标,包含:

  • 取消率:用户在加载过程中点击返回或右上角关闭的占比,用于描述小程序的启动性能,用户取消率越低,启动性能越好。
  • 白屏率:从启动到退出白屏的PV占比,出现白屏表示页面渲染失败,我们目标是尽可能降低白屏率
  • LCP到达率:如果较多用户在 LCP 到达前离开,也表示小程序可能出现异常,或用户体验较差

借助启动阶段指标及辅助指标,我们可以较为准确的描绘出小程序的体验感知。

优化手段

  • 启动性能体验
    • 较少包体积
      • 合理使用分包:使用分包加载是优化小程序启动耗时效果最明显的手段。建议开发者按照功能划分,将小程序的功能按使用频率和场景拆分成分包,实现代码包的按需加载。同时需要注意控制分包数量,避免过多拆包。
      • 移除无用文件:目前小程序打包是会将工程下所有文件都打入代码包内,在开发迭代过程中,如果不及时清理无用的资源,会使得包体积越来越大,开发过程中要养成良好的习惯,及时清理没有使用到的资源,防止资源冗余。
      • 控制包内静态资源:避免在代码包中包含或在 ttss 中内联过多、过大的代码包内的图片,应尽量采用网络图片。代码包内的图片一般应只包含一些体积较小的图标。声音、视频等其他类型的资源应尽量避免放到代码包中。小程序代码包在下载时会使用 Gzip 算法进行压缩,降低下载时传输的数据量。这些资源文件会占用大量代码包体积,并且通常难以进一步被压缩,对于下载耗时的影响比代码文件要大很多。
    • 减少同步逻辑
      • 优先使用异步API:在小程序启动流程中,会注入开发者代码并顺序同步执行App.onLaunch, App.onShow, Page.onLoad, Page.onShow。在小程序初始化代码(Page,App 定义之外的内容)和启动相关的几个生命周期中,应避免过度使用 Sync 结尾的同步 API。
      • 避免启动时运行过多同步代码:在小程序初始化代码(Page,App 定义之外的内容)和启动相关的几个生命周期中,应避免执行复杂的计算逻辑。
    • 更早的展示首屏数据
      • 尽早调用关键API和请求:首屏绘制可能会依赖API数据和网络请求,尽早的调用相关API,发送相关网络请求,能提前数据准备时间。
      • 接入数据预取:大部分小程序在渲染首页时,需要依赖服务端的接口数据,小程序为开发者提供了数据预取,方便开发者在小程序冷启动时提前发起请求,并缓存请求内容。
      • 避免非必要的reLaunchreLaunch会先关闭所有页面,非必要的reLaunch会导致首页白屏时间明显增长。
    • 合理缓存数据
      • 网络数据缓存:小程序提供了tt.getStoragett.setStorage等读写本地缓存的能力,数据存储在本地,返回的会比网络请求快。如果开发者基于某些原因无法采用数据预拉取,我们推荐优先从缓存中获取数据来渲染视图,等待网络请求返回后进行更新。
      • API数据缓存:对调用频次高的方法的结果进行缓存,例如对于 tt.getSystemInfo, tt.getSystemInfoSync的结果应进行缓存,避免重复调用。
    • 图片优化
      • 选择合适的图片格式:对于不需要透明格式的图片,推荐采用 jpeg 格式来代替 png 格式。如果有条件,尽可能使用webp格式图片,能大幅缩小图片体积。
      • 进行合理的压缩:图片尽可能压缩到200kb以下,压缩的同时也需要兼顾图片的质量。
      • 使用CDN并开启缓存:使用CDN,能大幅减少图片资源的下载速度。开启 HTTP 缓存控制后,下一次加载同样的图片,会直接从缓存读取,大大提升图片加载速度。
    • 更多优化手段
      • 框架骨架屏:通过小程序框架提供骨架屏机制,能比业务中创建的骨架屏加载时机更靠前,使用这一机制,可以减少用户的白屏等待时长,给用户带来更好的体验。
      • 占位组件:为自定义组件配置占位组件,可以指定该组件不在小程序启动时立即注入,而是等到页面中其他元素渲染完成后才注入。通过占位组件,能减少启动耗时。
  • 运行时性能体验
    • 合理使用setData
      • 减少发送频率
      • 动画不使用setData
    • 合理使用自定义组件
      • 合理的拆分组件数量:Page 中的setData会触发渲染层以页面级别进行 diff 操作,如果页面比较复杂且没有使用自定义组件,那么 diff 的成本会很高,导致体验比较差(更新卡顿、不粘手等感受)。如果页面转换为若干个组件,如果在组件中setData, 只会触发渲染层对应组件的 diff 操作,diff 成本会降低很多,使用体验也会提升很多。
      • 只注册当前使用的组件:同时在usingComponents建议只注册当前页面有使用到的自定义组件,在小程序框架会根据usingComponents中的自定义组件注册(无论开发者在运行时是否有使用)。
      • 同步修改初始data:在app.json中增加配置component2: true后,支持在created生命周期中修改自定义组件初始数据,自定义组件将在created生命周期执行完成后开始渲染。合理的使用方式能进一步提升用户体验。开启后有以下优化点:
        • 能够很好的解决依赖计算逻辑导致的 data 频繁变更。避免初始数据依赖大量计算逻辑时,由于 data 变化导致页面渲染内容闪动或频繁变动问题
        • 优化 observer 触发方式,减少因数据变更导致的通信,提升小程序性能体验
    • 合理监听处理事件
      • 合理监听处理 scroll 事件:避免在scroll事件中高频执行耗时操作,会明显降低FPS
      • 去掉不必要的事件绑定(ttml 中的 bind 和 catch),从而减少通信的数据量和次数
    • 内存优化
      • 及时解绑事件监听:事件监听结束后,应及时解绑监听器
      • 及时清理定时器:开发者在开发如「秒杀倒计时」等功能时,可能会使用setInterval设置定时器,页面或组件销毁前,需要调用clearInterval方法取消定时器。
    • 导航栏适配
      • 适当开启自定义导航栏 避免在app.json中全局开启动态导航栏,仅在需要的页面中配置,降低适配成本。
      • 关键信息避开状态栏和胶囊按钮 如果开启自定义导航栏,需要通过tt.getCustomButtonBoundingClientRect获取自定义导航栏下不可改变的元素来进行导航栏的适配
    • X分屏适配
      • 通过onResize监听显示区域变化 小程序支持组件和页面的生命周期函数onResize用于在显示区域的尺寸发生变化的时候返回当前页面的信息。其中组件需要作为页面配置到app.json中触发事件。
      • 不使用JS设置ScrollView高度 scroll-view中分屏变为全屏时需要重新设置scroll-view高度,可能会出现空白区域问题,建议使用CSS(vh)完成自适应布局。

可以使用字节提供的性能评分/分析工具进行具体分析来针对性的优化。

随着端越来越多,跨端的需求会越来越强烈,跨端技术方案也会不断迭代,核心目标仍然是“write once,run anywhere”,未来自渲染可能会出现更多机会。