本文阅览时长约15分钟。京喜小程序开发团队核心成员倾力之作,都是干货,读完必定会收获满满,请大家耐性阅览~

背景

京喜小程序自去年双十一上线微信购物一级进口后,时刻{ d t Q迎接着亿级用户量的挑战,细微的体会细节都有或许被无限放大,为此,“极致的页面功能”、“友爱的产品体会” 和 “安稳的体系服务” 成为了咱们开发团队的最基本履行准则。

主页w [ ; . m / b作为小程序的门户,其功能体现和用户留存率息息相关。因而,咱们对京喜主页进行了一次全方位的晋级改造,从加载、烘托和感知体会几大维度深挖小程序的功能可塑性。

除此之外,京喜主页在微信小程序、H5、G 5 U ? 0 7APP 三端都有落地场景,为了提高研制功率,咱们运用了 Taro 结构完结多端共同,因而下文中有部分内# R y 3 N容是和 Taro 结构息息相关的。

怎么界说高功能?

提起互联网运用功能这个词,许多人在脑海中的词法解析便是,“是否足够快?”,似乎加载v j % W { Q p F速度成为衡量体系功能的唯一目标。但这其实F 7 b 1 } Q是不行准确的,试想一下,假如一个小程序加载速度十分快,用: } j 3 =户花费很短时刻就能看到页面的主体内容,但此刻查找框却无法输入内容,o 4 o A %功能无a y O N 2 { r ` p法被流通运用,用户或许就不会关心页面烘托有多快了。所以,咱们不应该单纯考虑速度目标而疏忽用户的感* 7 Z ] ; A Y P ;知体会,而应该全B J R g L O X A方位衡量用户在运用进程中能感知到的与运用加载相关的每个节点。

谷歌X d 8 w K a R % ^为 Web 运用界说了以用户为中心的功能目标体系,每个目标都与用户体会节点息息相关:

体会 目标
页面能否正常拜访? 初次内容制作 (First Contentful Paint, FCP)
页面内容是否有用? 初次有用制作 (First Meaningful Paint, FMP)
页面功能是否可用? 可交互时刻 (Time to InteractiveV i Y, TTI)

其间,“是否有用?” 这个问题是十分片面的,关于不同场景的体系~ n F或许会有彻底不一样的答复,所以 FMP 是一个比较含糊的概念目标,不存在规范化的数值衡量。

小程序作为一个新的内容载体,衡量目标跟 Web 运用是十分相似的。关于大多数小程序而言,上述目标对应的意义为:

  • FCP:白屏加载完毕;
  • FMP:首屏烘托完结;
  • TTI:一切内容加载完结;

综上,咱们已基本确定了高功能的概念目标,接下来便是怎么运用数值目标来描绘功能| = S / R x M U v体现。

小程序官方功能( l m J { X * [ ;目标

小程序官方针对小程t V h U 4 ^ ^序功能体现制定了威望的数值目标,首要环绕 烘托体现sR t l g - ~ 9etData 数据量元素节点数网络恳求延时 这几个维度来给予6 n L ~ S界说(下面只列出部分要害目标):

  • 首屏时刻不超越 5 秒;
  • 烘托时刻不超越 500ms;
  • 每秒调用 setData 的次数不超越 20 次;
  • setDa] R 3 w 8ta 的数据在 JSON.stringify 后不超越 256kb;
  • 页面 WXML 节点少于[ 6 ] L @ i ` t 1000 个,节点树深度少于 30 层,子节点数不大于 60 个;
  • 一切网络恳求都在 1 秒内回来成果;

详见 小程序% x V x : 5功能评分规矩

咱们应该把这一系列的官方目标作为e D } . f ` Y小程序的( Y $功能及格~ i ` U M , w 0 g线,不断地打磨和提高小程序的全体体会,下降用户流失率。别的,这些l N H 3目标会直接作为小程序体会评分东西的功能评分规矩(体会评分东西会依据这些规0 7 c ~ %矩的权重和求& m }和公式核算出体会得分)。

咱们团队内部在官方功能目标的根底上,进一步浓缩优化目标系数,旨在对产品体会更高要求:

  • 首屏时刻不超越 2.5 秒;
  • setData 的数据量不超越 100kb;
  • 一切网络恳求都6 y k A . 6 T C {在 1 秒内回来成果;
  • 组件滑动、长列表D [ I翻滚无卡顿感;

体会评分东西

小程序供给了 体会评分东西(Audits 面板) 来丈量上述的目标数据,其集成在开发者东西中,在小程序运转i b d C $ m h时实时检查K V ^ .相关问题点,并为开发者给出优化建议。

京喜小程序的高性能打造之路

以上截图均来自小程序官方文档

体会评分东西是现在检测小程序G q i功能问题最直接有用的途径,咱们团队现已把体会评分作为页面/7 D $ Z m 6 G组件是否能达到精品门槛的重要考量手法之一。

小程序后台功能剖析

咱们知道,体会评分东西是在本地运转小程4 e e序代码时进行剖析,但功能数据往往需求在实在环境和大数据量下才更有说服力。恰巧,小程序办理渠道小程序帮手 为开发者供给了许多的实在数据核算。其间,功能剖析面B $ | y 8 ] u n /板从 发动功能运转功能网络功能 这三个维度剖析数据,开发者能够依据客户端体系、机型T 7 N K Y、网络环境和拜访来源等条件做精细化剖析,十分具有考量价值。

京喜小程序的高性能打造之路

其间,发动总耗6 X S / 4 e时 = 小程序环境初始化 + 代码包加载 + 代码履行 + 烘托耗时

第三方测速体系

许多时分,微观的耗时核算关于功E y { / & ^ 4 d能瓶颈点剖析往往是杯水车薪,作用甚少,咱们需求更细致地针对某个页面某些要害节点作测速核算,排p . f查出露出功能问题的代W c * _ g p码区块,才能更有用地针对性优化。京喜小程序运用的是内部u X * + x 4自研的测速体j _ K t L ?系,N p 6 c 6 r 7 KE = 1 a 2 b l :撑对区域、运营商、网络、客户端体系等多条件挑选,一起也支撑数据可视化、同比剖析数据等才能。京L } + q L喜主页首要环绕 页面 onLoado 1 ` _ ]nReady数据加载完结首屏烘托完结各事务组件初次烘托完结 等几个要害节点& 3 J _ 3 u 9 Y核算U z 0 x # t测速上报,旨在全链路监控功能体现。

京喜小程序的高性能打造之路

别的,Q W * o a微信为开发者供给了 测速体系,也支撑针对客户端体系、网+ B A % k络类型、用户区域等维度核算数据,有爱好的能够测验。

了解小程序底层架构

为了更好地$ C f – A为小程序制定功能优化办法,咱们有必要先了解小程序的底层架构,以及与 web 浏览器的差异性。

微信小程序是大前端跨渠道技能的其间一种产品,与当下其他抢手的技能 React Native、Weex、Fluts d W 1 @ Kter 等不同,小程序的终究烘托载体仍然是浏览器内核,而不是原生客户端。

而关于传统的网页来说,UI 烘托和 JS 脚W 6 E F b , f l本是在同一个线程中履行,所以经常会出现 “堵塞” 行为。微信小程序依据功能的考虑,启用了双线程模型

  • 视图层:也便是 webf ~ z { Mview 线程,担任启用不同的 webview 来烘托不同的小程序页面;
  • 逻辑层:一个独自的线程履行 JS 代码,能够操控视图层的逻辑;
京喜小程序的高性能打造之路

上图来自小程序官方开发攻略

但是,任何线程间的数据传输都是有延时的,这意味着逻辑层和视图层间通讯是异步行为。除此之外,微信为小程序供给了许多客户端原生才能,在调用客户端原生才能的进程中,微信主线程u J d _ / 3 J和小程序双线程之间u M F x 7 –也会发作通讯,这也是一种异步B b E – d p N行为# S n u K。这种异步延时的特性会使运转环境复杂化,稍不注意,就会产出功h y – / F 率低下的编码。

作为小程序开发者,c , [ G y ` U咱们常常会被下面几个问题所困@ M % ]扰:

  • 小程序发动慢;
  • 白屏时刻长;
  • 页面烘托慢;
  • 运转内存不$ c W _足;

接下来,咱们会结合小程序的底层架构剖分出这些问题的根本原因,并针对性地给出解决计划。

小程l ` W Y N –序发动太慢?

小程序发动阶段,也便是如下图所示的展现加载界面的阶段。

京喜小程序的高性能打造之路

在这个阶段中(包含发动前后的机遇)1 R 7 { ] : !,微信会默默完结下面几项工作:

1. 预备运转环境:

在小程序发动前,微信会先发动双线程环境,并在线程中完结小程序根底库的初始化和预履行。

小程序| g 5根底库包含 WebView 根底库和 AppServic! g q pe 根底库,前者注入到视图层中,后者注入到逻辑层中,分别为所在层级供给其运转所需的根底结构才能。

2.C 9 7 + f ? L { : 下载小程序代码包:

在小程序初q _ f W 0 /次发动时,需求下载编a b z ? {译后的代码包到本地。假如发动了小程序分包,则只有主包的内容会被下载。别的,代码包会保留在缓6 G g h存中,后续发9 F J C {动会优先读取缓存。w 5 % h J

3. 加载小程序代码d V z c – S包:

小程序代码包下载好之后,会被加载到恰当的线程中履行,根底库会完结一切页面的注册。

在此阶段,主包内的f K D $ y u一切页面 JS 文件及其依靠文件都会被主动履行。

在页面注册进程中K b u H j x y,根底库会调用页面 JS 文件的 Page 结构g d / _ 1 L a R器办法,r O x | = – L {来记录页面的根底信息(包含初始数据、办法) E _ [ |等)。

4. 初始化小n L T ] – u程序主页:

在小程序代码包加载完之后,根b % N R I L 4 { P底库会依据发动途径n ; # x : 8 a # l找到主页,依据主页的根底信息初始化一个页面实例,并把信息传递给视图p a + ` v c W v ~层,视图层会结合 WXML 结构、WXSS 款式和初始数据来烘托界面。

综合考虑,为了节约小程序的“点点点”时刻(小程[ & ! Q N H序的发动动画是三个圆点循环跑马灯),除了给每位用户发一台高配 5G 手机并顺带供给千兆宽带网络之外,还能够尽量 操控代码包巨细,缩小代码包的下载时H H –刻。

无用文件、函数、款式剔除

经过屡次事务迭代,无可防止的会存在一些弃用的B Q k ; d组件/页面,以及不被调用的函数、款式规矩,这些冗余代码会白白占有宝贵的代码包空间。而且,现在小程序的打包会将工程下一切文t K ) a件都打入代码包内,并没有| [ $ | z z l做依靠剖析。

因而,咱们需求及时地剔除不再运用的模块,以保证代码包空间运用率坚持在Y { M 1 q T较高水平。经过一些东西化手法能够有用地辅助完结这一工作。

  • k + m I U y a ) d件依靠剖析

在小程序中,一切页面的途径都需求在小程序代码根目录 app.json 中被声明,相似地,自界说组件也需求在页面装备文件 page.json 中被声明。别的,WXML、WXSS 和 JS 的模块化都需求特定的要6 Q | / u M e d c害字来声明依靠引证联系。

WXML 中的 imE Q 7 K Q u xportinclude

&l` f i D  %t;!-- A.wxml -->
<template name='U G * NA'>
<text>{{text}}&lN p / O - 3t;/text>
</template>: , 7 T &
<!-- BX 6 %.wxml -->
<import src="https://juejin.im/post/5e7S R K p *d4487e51db N e e p4546d83af560/A.wxml"/m p d g>
<template in : Y 2 / X ~ :s="AP ) N v %" data="{{text: 'B'}}"/>
<!-- A.wxml -->
<texl ; ~ z 9 dt> A </text>
<!-- B.wxml -->
<W q J ?include src="https://juejin.im/post/5e7d4487e51d4546d83af560/A.wx` # # 4 L hml"/>
<text> B </text>

We $ J CXSS 中的 @import

@import './A.wxss'

JS 中的 require/import

const A = require('./A')

所以,能够说小程序里的一切依靠模块都是有迹可循的,咱们只需求运用这些要害字信息递归查找,遍历出文件依靠树,然后把没用的模块剔除* b M ( M M 6去。s [ h E s g

  • JS、CSS Tree-Shaking

JS Tree-Shaking 的原理便是借助 Babel 把代码编译成笼统语法h F / n d n Z树(AST),经过 AST 获取到函数的调用联系,然后把未被调用的函数办法剔除去。不过这需求依靠 ES module,而小程 P @ N . G e d序最开始是遵循 CommonJSP G = R , P p 规范的,这意味着是时^ : C v * ! & (分来一波“痛并快乐着”的改造了。

而 CSS 的 Tree-Shaking 能够I & v e P { q j运用 PurifyCSS 插件来完结。关于这两项技能,有爱好的能够“谷歌一下”,这儿就不铺开细讲了。

题外,京东的小程序团队现已把这一系列工程化才能集成在一套 CLI 东西中,有爱好的能够看看这篇分享:小程序工程化探索。

削减代码包中的静o w E G态资源文件

小程序代码包终究会经过 GZIP 紧缩放在f ~ 8 CDN 上,但 GZIP 紧缩关于图片资源来说作用十分低。如 JPGPNG 等格局文件,本身现已被紧缩过了,再运用 GZIP 紧缩有或d _ $ z $许体积更大,因小失大。所以,除了部分用于容错的图片有必要放在代码包(比如网络异常提示)之外,建议开发者把图片、视频等静态资源都放在 CDN 上。

需求注意,Base64 格局本质上是长# – v H – ] 4 o I字符串,和 CDN 地址比起来也会更占空+ * 4 E v间。

逻辑后移,精简事务逻辑

这是一个 “痛并快乐着” 的优化办法。“痛” 是由于需求给后台同学提改造需求,分分钟被打;“快乐” 则是由于享用删代码的进程,而且R ~ F 7 3 A u % j万一出 Bug 也不用背锅了…(开个打趣)

经过让后台承当更多的事务逻辑,能够L R *节约小程序前端代码量,一起线上问题还支撑M $ 9 $ a e # O紧急修正,不需求阅历小程序的提审、发布上线等繁琐进Q y 9 N } Z D N程。

总结得出,一般不涉及前端核算的a t B ; 5 w x N展现类逻辑,都能够恰当做后移。比如京喜主页中的幕帘弹窗(如下图)逻辑,这儿共有 10+ 种弹窗类型,曾经的做法是前端从接口拉取 10+# } l 个不同字段,依据优先级和 “是否已展现”(该状态存储在本地缓存) 来决议展 Q h现哪一种,终究代码大约是这样的:

//u s  [ l ; P 检查每种弹窗类* + 3 L 7 Q型是否已展现
Prom1 s Cise.all([
check(popup_1),
check(popup_2),
// ...
check(popup_n)
]).then(result => {
// 优先级排序
const queue = [{
show: result.popup_1
datu H 7 6 [ + q z Da: data.popup_1
}, {
show: result.popA m z E C j 9 e #uX ` p_2
data: data.popup_2
},
// ...
{
show: re# / Rsult.popup_n
data: data.popup_n
}]
})

逻辑后移之后,前端只需担任拿幕帘字段做展现就能够了,代码变成这样:

this.setData({
popup: data.popup
})
京喜小程序的高性能打造之路

复用模板插件

京喜主页作为电商体系的门户N U D,需求应Y h T e ? z L对各类频频的营销活动、晋级改版等,一起也 g ] 0 m 4 v要满足不同用户特点的界面个性化需求(俗称 “千人千面”)。怎么既能削减为应对多样化场景而发作的代码量,又能够提高研制功率d H 8 i z,成为燃眉之急。

相似于组件复用的理念,咱们需求供给更丰富的可装备才% z 6 e能,完结更高的代码复用度。参阅小时分很喜欢玩的 “l = & @ r + C D 6乐高” 积木玩具,咱们把主页模块的模板元素作颗粒度更细的划分,依据款式和功能笼统出一块块“积e # I X木”质料(称为插件元素)。当主页模块在处理接口数据时,[ M 0 s S * ( .会发动插件引擎逐一装载插件,终究输出个性化的模板款式,整个流程就比如堆积木。当后续产品/运营需求新增模板时,只要在插件库中挑选插件j ) { ) 1 G A 1排列组合即可3 . ` 6 0 } ( { T,不需求额定新增/修改组件内容,也更不会发作难以保护的 if / else 逻辑,so8 p W easy~

当然,要完结这样的插件化改造免不了几个先决条件:

  • 用户体会规划的共同。假如规划风格总是天差地别的,强行插件化只会成为累赘。
  • 服务端接口的共同。同上,假如得糟蹋许多的精力来兼容不同模块间的接口字段差异,将会十分蛋疼。

下面为大家供给部分例程来辅助了解。其间,use 办法会承受各类处理钩子终究拼接出一个 Function,在对N c | y P = S –应模块处理数据时会被调用。

// bi.helper.js
/**
* 插件引擎
* @param {function} options.formatd # W 7 _ O }Name 标题处理钩子
* @param {function} optionN D l T Es.validList 数据校验器钩子
*/
const use = options => data => format(data)
/**
* 预置插件库
*/
nV n ^ N x W OameHelpers = {
text: data => data.text,
icon: data => data.icon
}
liY ) L . j w ?stHelpers = {
single: lik ) v % _ Cst => list.slice(0, 1),
double: list => list.slice(0, 2)
}
/**
* “堆积木”
*/
export default {
1000: use({
formatName: nameHelpers.text,
validList: listHelpers.singlY  t G a w #e
}),
1001: use({
formatName: nameHelpers.icon,
vali~ # ? PdList: listHelpers.double
})
}
<!-- bi.wxml -->
<!-- 各模板节点完结 -->
<template name="renderName">
<view wx:if="{{type === 'text'}}"> text </view>
<view wx:elif="{{type === 'icon'}}"> icon </view>
</templaf N L & A mte>? % o M = y / C
<view class="bi__name">
<template is="renderName" data="{{...data.name}"/>
<w M l/viU @ r / 0 V G ]ew>
// bi.js
Component({D [ N V ^ R h z E
ready() {
// 依据 tpl 值挑选解析函数
con| . E rst formatData2 , } 1 - U n = helper[data.tpl]
this.s9 | C VetData(M + | 0 9 Q f{
dae B L b uta: formatData(data)
})
}
})

分包加载

小程序发动时只会下载主包/独立分包,启用分包能够有用削减下载时刻。(独立)分包需求遵循一些准则,详细的能够看官方文档:

  • 运用分包
  • 独立分包

部分页面 h5 化

小程序供给了 web-view 组件,支撑在小程序环s $ 8 o o } b K S境内拜访网页。当实在无法在小程序代码包中腾出多余空间时,能够考虑降级计划 —— 把部分页面 h5– p M 6 A ~ t 化。

小程序和 h5 的通讯能够经过 JSSDK 或 postMessage 通道来完结,详见 小程序开发文档。

白屏时刻过长?

白屏阶段,是指小程序代码包下载完(也便是发动界面完毕)之后,页面完结首屏烘托的这一阶段,也便是 FMPP F d (初次有用制作)。

FMh V RP 没法用规范化的目标界A F h说,但关于大部分小程o ` C序来说,页面首屏展现的内容都需求依靠服务端的接口数据,那么影响白屏加载时刻的首要由这两个元素构成:

  • 网络资源加载时刻
  • 烘托时刻

启用本地缓存

小程序供给了读写本地缓存的接口,数据存储在设备硬盘上。由于本地 I/O 读写z 4 ] p &(毫秒级)会比网络恳求(秒级)要快许多,所以在用户g # r拜访页面时,能够优– = k先从缓存中取上一次接口调用成功的数据来烘托视图,待网络恳求成功后再覆盖最新数据从头烘托。除此之外,缓存数据还能够作为兜底数据,防止出现接口恳求失败时页面空窗,M 9 : k : i 6 o 8一石二鸟。

但并非一切场景都合适缓存战略,比如对数据即时性7 y I = Y [ } |要求十分高的j 5 # P , 8场景(如抢购X { F O e进口)来说,展现老数据或许会引发一些问题。

小程序默许会依照 不同小程序不同微信用户 这两个维度对缓存空间进行阻隔。诸如京喜小程序主页也采用了缓存战略,会进一步依照 数据版本号用户特点 来对缓存进行再阻隔,9 p . [ *防止信息误展现。

数据预拉取

小程序官方为开发者供i 1 = – , _ D o给了一个在小程序冷发动时提早拉取第三L D j |方接口的才能:数据F q ( D ( :预拉取。

关于冷发动和热发动的界说能够看 这儿

数据预拉取的原理其实1 & ! _ I h 8很简略,便是在小程序发动时,微信服务器署理小程序客户端建议一个 HTTP 恳求到第三方服务器来获取数据,并且把呼应数据存N 1 4 . k ,储在本地客户端供小程序前端调取。当小程序加载完结后,只需调用微信供给的 API wx.getBackgroundFe: N 0 # a 8 $tchData 从本地缓存获取数据即可。这种做法能够充分运用小程序发i P 0 ? I g 0动和初始化阶段的等待时刻,使/ Y ( ! 更快地完结页面烘托。

京喜小程序主页现已在出产环境实践过这个才能,从每日千万级的数据剖析得出,预拉取使冷发动时获取到接口数据的时刻节K D @ m ~点从 2.5s 加速到 1s(提速了 60%)。虽然提高作用十分显着,但这个才能仍然存在一些不成熟的当地:

  • # 1 + 6 % s + k拉取的数据会被强缓存

    由于预拉取的恳求U ; 4 P U终究是由微信的服务器建议的,也许是出于服务器资源约束的考虑,预拉取的数据会缓存在微信本地一段时刻,缓存失i M 0效后才会从头建议恳求。经过真机实测,在微信购物进口冷发动京喜小程序的场景下,预拉取缓存存活了 30 分钟以上,这关于数据实时性要求比较高的体系来6 Y 4 m C v 6 R说是十分致命的。

  • 恳求体和呼应体都无法被阻拦

    由于恳求第三方服务器是从微信的服务器建议的,而不9 v m S O R I 5 v是从小程序客户端建议的,所以本地署理无法阻E T C P & { D h I拦到这一次实在恳求,这会导致开发者无法经过阻拦恳求的方法来差异获取线上环境和开发环境的数据] ^ 0 Z Y I,给开发调试带来麻烦。

    小程序内部接口的呼应体类型都是 application/ow 3 q q : Zctet-strE V @ & @ P ieam,即数据格局未知,使本地署理无法正确解析。

  • 微信服务器建议的恳求没有供给差异线上版和开发版的参数,且没有供给用户 IP 等信息

假如这几个问题点都不会影响) d : o到你的场景,那么能够测验开启预拉取才能,这关于小程序首屏烘托速度是质的提高。

跳转时预拉取

为了赶W i F B快获取到服务端数据,比较常见的做法是在页面 onLoad 钩子被A f ,触发时建议网络恳求,但其实这并A : k R : x不是最快的方法。从建议页面跳转,到下一个页面 onLoad 的进程中,小程序需求完H 0 + G t s结一些环境初始化及页面实例化的工作,耗时大约为 300 ~ 400 毫秒。. . x

实际上,咱们能够在建议跳转前(如 wx.navigateTo 调用前),提早恳求下一个页面的主接口并存储在大局 Promise 目标中,待下个页面加载完结后从 Promise 目标中读取数据即可。

这也d ; = 8是双线程模型所带来的优势之一,不同于多页面 web 运用在页面跳转/刷新时就毁^ N _ ! 0 r T掉掉 window 目标。

分包预下载

假如开启了分包加载才能,在用户拜访到分包内某个页面时,小程序才会1 M W g %开始下载对应的分包。当处于分包下载阶段时,页面会维持在 “白屏” 的发动态,这用户体会是比较糟糕的。

幸亏,小程序供给了 分X l J x _ ^ A , L包预下载 才能,开发者能够装备进入某个页面时预下载或许会用到的分包,防止在页面切换时相持在 “白屏” 态。

非要害烘托数据推迟恳求

] _ ) – , K是要害烘托途径优化的其间一个思路,从缩短网络恳求时延的角度加速首屏烘托完结时刻。

要害烘托途径(Critical Rendering Path) 是指在完结首屏烘托的进程中有必要发作的事情。

以京喜小程序如此巨大的小程序项目为例,每个模块背面都或许有着海量的后台服务作支撑,而这些后台服务间的V B – G 2 ? [ : 8通讯和数据交互都会存在必定的时延。咱们依据京喜主页的页面结构,把一切模块划分红两类:主体模块(导航、产品轮播、产品豆腐块等)和 非主体模块(幕帘弹窗、右侧挂件等)。

在初始化主K R m x $页时,小程序会建议一个聚合接| F 9口恳求来获取主体模块的数据,而非主体模块的数据则从另一个接口获取,经过拆分的手法来J & 0 = $ * C G下降主接口的调用时延,一起削减呼应体的数据量,缩减网络传输时刻。

京喜小程序的高性能打造之路

分屏烘托

这也是要害烘托途径优化思路之一,经过推迟非要害元素的烘托机遇,为要害烘托途径腾出资源。

相似上一条办法,继续以京喜小程序主页为例,咱们在 主体模块 的根底上再度划分出 首屏模D f N d : y G Z(产品豆腐H Z + N o ? T =块以上部分) 和 非首屏模块# O D 0 { @产品豆腐块及以下部分)。当小程序获取到主体模块的数据后,会^ n Z I = x i 0优先烘托首屏模块,在一切首屏模3 _ u r E块都烘托完结后才会烘托非首屏模块和非主体模块,以此保证首屏内容以最快速度出现。

京喜小程序的高性能打造之路

为了更好地出现作用,上面 gif 做了降速处理

接口聚合,恳求兼并

在小程序中,建议网络恳求是经过 wx.request 这个 API。咱们知道,在 web 浏览) – / r器中,针对同一域名j E ; p $ I的 HTTP 并发恳求数是有约束的;在小程序中也有相似G 9 . d D C ~ ! )的约束,但差异在于不是针3 : ^ h ) R `对域名约束,而是针对 API 调用:

  • wx.request (HTTP 衔接)的最大并发约束是 10 个;
  • wx.v J r D 6coi K q o n OnnectSocket (WebSocket 衔接)的最大并发约束是 5 个;

超出并发约束数目的 HTTP 恳求将会被堵塞,需求在行列中等待前面的恳求完结,然后必定程度上增加了恳求时延。因而,关于职责相似的网络恳求,最好采用节省的方法,先在必定时刻间隔内搜集数据,再兼并到一个恳求体中发送V u ]给服务端。

图片资源优化

图片资源一直是移动端体系中抢占大流量的部分,尤其是关于电商体系。优化图片资源的加载能够有用地加速页面呼应时刻,提高首屏烘托速度。

  • 运用 WebP 格局

WebP 是 Google 推出的一种支撑有损/无损紧缩的图片文件格7 @ 9 F局,得益于9 y g 4 :更优的图像数据紧缩算法,其与 JPG、PNG 等格局比较,在肉眼无差别的图片质量前提下具有更小@ Q ~的图片I ^ u x体积(据官方说明,WebP 无损紧缩体积比 PNG 小 26%,有损紧缩体积比 JPEG 小 25-34%)。

小程序的 imagT 1 z S Ge 组件 支撑 JPG、PNG、SVG、WEBP、GIF 等格局。

  • 图片裁剪&ampM d # v;降质

鉴于移动端设备的分辨率是有上限的,许多图片的? ` 8 L 1 v g尺度常常远大于页面元素尺度,这十分糟蹋网络资源M ] 7 9 ; R(一般图片尺度 2 倍于页面元素实在尺度比较合适)。得益于京东内部强大的图片处理服务,咱们能够经过资源的命名规矩和恳求参数来获取服务端优化后的图片:

裁剪成 100×100 的图片:https://] v ( V 6 E S c{host}/s100x100_jfs/{file_path3 } W Q j}

降质 70%:J i & O q #https://{href}!q70

  • 图片懒加载、^ # J M I Z雪碧图(CSS Sprite)j h t k e l H )优化

这两者都是比较陈词滥调的图片优化技能,这儿就不打算细讲了。

小程序的 image 组件 自带 lazy-load 懒加载支撑。雪碧图技能(CSS Sprite)能够参阅 w3schools 的教程。

  • 降级加载大图资源

在不得不运用大图资源的场景下,咱们能够恰当运用 “体s z o K会换速度” 的办法来提高烘托功能。

小程序会把已加载的静态资源缓存在本地,当短时刻内再次建议恳求时会直接从缓存中取资源(与浏览器行r S 2为共同)。因而,关于大图资源,咱们能够先出现高度紧缩的– , { 0 S b t ! 1含糊图片,一起运用一个隐藏的 <image> 节点来加载原图,待原图加载完结后再转移到实在节– g a Y r } D h 3点上烘托。整个流程,从视觉上会感知到图片从含糊到高清的进程,但与对首屏烘托的提高作用比较,这点体会落差是能够承受的。

下面为大家供给部分例程:! | & J n r

<!-- banner.wxml -->
<image src="https://juejin.im/post/5e7d4487e51d4546d83af560/{{url}}" />
<!-- 图片加载器 -->
<image
styl! l q ?e="width:0;height:0;display:none"
src="- U b - 9 a | ^ Hhttps://juejin.im/pos, C : Ct/5e7d44k & J 5 v M k87e51d4546d83af560/{{preloaV L ] } _ $ 4dUrl}}"
bindload="onImgLoad"
binderror="onErrorLoad"
/>
// banner.js
Component({
ready() {
this.originUrl = 'https://path/P ~ W . 9 , v 8 bto/picture'  // 图片源地址
this.setData({
url: compress(this.originUrl)             // 加载紧缩降质的图片
preloadUrl: this.originU@ 4  frl                // 预加载原图
})
},
methods: {
onImgLoad{ o G F 8 | R & B() {
this.setData({
url: this.originUrl                       // 加载原图
})
}
}
})

注意,具有 display: none 款式的 <image> 标签只会加载图片资源,但不烘托。

京喜主页的产品轮播模块也V C a U z 7 h –采用了这种降级加载计划,在首屏烘托时只会加载榜首帧降质图片。以每帧原图 20~50kb 的巨细核算,这一办法能够在初始化阶段节约掉几百 kb 的网络资v r N d – H Y . g源恳求。

京喜小程序的高性能打造之路

为了更好地出现作用,上面 gif 做了降速处理

骨架屏

一方面,咱们能够从下降网络恳求时延、削减要害烘托的节点数这两个角度出发,缩短完$ A ^ [ & P D Z +结 FMP(初次有用制作 b B 9 D H j K 3)的时刻。另一方面,咱们5 4 I 9 e w B y7 4 T 8 _ x V H需求从用户感知的角度优化加载体会。

“白屏” 的加载体会关于初次拜访的用户来说是O U p P ? c难以承受的,咱们能够运用尺度安稳的骨架屏i P G,来辅助完结实在模块K 6 K ] : H占位和瞬间加载。

骨架屏现在在业界被广泛运用,京喜主页挑选运用灰色豆腐块作为骨架屏的主元素,大致^ k # ~ r ~ I 4 0勾勒出各模块主体内容的款式布局7 h 5 D ~。由于微信小程序不支撑 SSR(服务端烘V 8 L H } +托),使动态烘托骨架屏的e 4 2 : ! f % A计划难以完结,因而京喜主页的骨架屏是经过 WXSS 款式静态烘托的。

有趣的是,京喜主页的骨架屏计划阅历了 “共同m . M g办理”V P ( w 4 @ ; H(组件)独立办理” 两个阶段。出于防止对组件的侵入性考虑,开始的骨架屏是由一个完好的骨架屏组件共同办理的:

<!-- index.wxml -->
<, f f;skeleton wx:if="{{isLoading}}"></skeleton>
<block wx:else>
页面主体
<y Z ] ] )/block>

但这种做法的保护本钱比较高,每次页面主体模块更新迭代,都需求在骨架屏组件中的对应节点同步更新(比如某个模块的尺度} S ^被调整)。除此之外,感官上从骨架屏到实在模块的切换是跳跃式的,这是由于骨架屏组件和页面主体节点之间的联系是全体条7 ] X v e w p o C件互斥的,只有当页面主体数据 Ready(或烘托完毕)时9 H S 5 v = &才会把骨架屏组件毁掉,烘托(或展现)9 4 L主体内容。

为了运用户感知体会愈加丝滑,咱们把骨架屏元素拆分放到各个事务组件中,骨架屏元素的显示/隐藏逻I n p辑由事务组件内部独立办理,这就能够轻松完结 “谁跑得快,谁先出来” 的并行加载作用。除此之外,骨架屏元素与事务组件共用一套 WXML 节点,且相关款式由公共的 sass 模块集中办G Q T理,事务组件只需求在恰当的节点挂上 skeletonskeleton__block 款式块即可,极大地下降了保护本钱。

<; W ( 6 3;!-- banner.wxml -->
<view classw t R : l  (="{{isLoading ? 'banner--skeleton' : ''}}">
<view class="ba| o h 0 ;nn& t $ ) 7er_wrapper"></view. t ) 6>
</view>
// banner.scss
.banner--skeleton {
@include skeleton;
.banner_wrapper {
@include skeleton__block;
}
}
京喜小程序的高性能打造之路

上面的 gif 在紧缩进程有些小问题,大家能够直接拜访【京喜】小程序体会骨架屏作用。

怎么提高烘托功能?

当调用 wx.navigateTo 翻开一r ^ Z Z – ]个新的小程序页面时,P X * j y I = C小程序结构会完结这几步工作:

1. 预备新的 webview 线程环境,包含根底库的初始化;

2. 从逻辑层到视图层的初始数据通讯;

3. 视图层依据逻辑层的数据,结合 WXML 片段构建出节点树(包含节点特点、事情绑定等信息),终究与 WXSS 结合完结页面烘托;

由于g + l t m y o 3微信会提早开始预备 webview 线程环境,所以小程序的烘托损耗首要在后两者& m C 数据通讯节点树创建/更新 的流程中。相对应的,比较有用的烘托功能优化方向便是:

  • 下降线程间通讯频次;
  • 削减线程间通讯的数据量;
  • 削减 WXML 节点数量;

兼并 setData 调用

尽或许地把屡次 s= N V 8etData 调用兼并成一次。

咱们除了要从编码规范上践行这个准则,还能够经过一些技能手法下降 setData 的调用频次。比如,把同一个时刻片(事情循环)内的 setData 调用兼并在一起,T s aro 结构就运用了这个优化手法。

在 Tare { $ 2 R u ~ Do 结构下,调用 setState 时供给的目标会被加入到一个数组中,% 8 q _当下一次事情A J e w I循环履行的时分再把这些目标兼并一起,经过 setData 传递给原生小程序。

// 小程序里的时刻片 APIC O K
const nextTick = wx.nextTickL V v : s q g @ M ? wx.nextTick : setTimeout;

只把与界面烘托相关的数据放在 data

不难得出,setData 传输的数据量越= [ k ` q 4 B多,线程间通讯的耗时越长,烘托速度就越慢。依据微信官方测得的数据,传输时刻和数据量大体上呈正相关联系:

京喜小程序的高性能打造之路

上图来自小程序官方开发攻略

所以,与视图层烘托无关的数B 0 / C据尽量不要放在 data 中,能够放在页面(组件)类的其他字段下。

运用层的数据 diff

每当调用 setData 更新数据时,会引起视图层的从U | | % [ p头烘托,小程P E g ` 0 n序会结合新的 daU # ; a Rta 数据和 WXML 片段构建出新的节点树,并与当时节点树进行比较得出终究需求更新_ H % P W i x ? D的节点(特点)。| ^ / 5 L X

即便小程序在底层P U k Y : : r Y结构层面现已对节点树更新进行了 di. : m : z } cff,但咱们依旧能够优化这次 diff 的功能。比J 6 C C如,在调用 setDataE l # ~ 时,提早P ^ | – D 6保证传递的一切新数据都是有变化$ N a 1 Z u的,也F I – G , a 3便是针对 data 提早做一次 diff。

Taro 结构内部做了这一层优化。在每次调用原生小程序的 setData 之前,Taro 会把最新的 state 和当时页面实例的 data 做一次 diff,挑选出有必要更新的数据再履) b ( / 6 v YsetData

附 Taro 结构的 数据 diff 规矩

去掉不必要的事情绑定

当用户事情(如 CliP K M + ( v j pck4 | , 9 ?Touch 事情等)被触发时,视图层会把事情信息反馈给逻辑层,这也是一个线程间通讯的进程。但,假如没有在逻辑层中绑定事情的回调函数,通讯将不会被触发。

所以,尽量削减不必要的事情绑定G ( 7 – 5 c 7,尤其是像 onPagq . P + q ,eScroll 这种会被频频触发的用户事情,会使通讯进程频频发作。

去掉不必要的节点特点

组件节点支撑附加自界说数据 dataset(见下面比如),当用户事情被触发时,视` e ! B图层会把事情 taru X ^getdataset 数据传输给逻辑层。那么,当自界说数据量越大,事情通讯的耗时就会越长,所以应该防止在自界说数据中设置太多数据。

<!-- wxml -->
<view
data-a='A'
dat) 2 | P pa-b='B'
bindtap='bindViewTap'
>
Click Me!
</view>
// js
Page({
bindViewTap(e) {
console.log(e6 3 9 m { H u L.currentTarget.dataset)
}
})

恰当的组件颗粒度

小程序的L L e P | | r l 5组件模型与 Web Components 规范中的P * { Q S ShadowDOM 十分相似,每个组件都有独立的节点树,具有各自独立的逻辑空间(包含独立的数据、setData 调用、cx i w k , C F Kreatv ` 3 v x v K , aeSelectorQJ # : # Cuery 履行域等)。

不难得出,假如自T t 8界说组件的颗粒度太粗,组件逻辑过重,会影响节点树构建和新/旧节点树 diff 的功率,然后影响到组件内 setData 的功能。别的,假如组件内运用了 createSelectorQuery 来查找节点,过于巨大的节点树结构也会影响查找功率。

咱们来看一个场景,京喜主页的 “京东秒杀” 模块涉及到一个倒计时特性,是经过 setInterval 每秒调用 setData 来更新表盘时刻。咱们经过把倒计时抽离出一个根底组件,能: k ^ 8 N够有用下降频频 setData 时的功能影响。

京喜小程序的高性能打造之路

恰当的组件化,既能够减小数据更新时的影响规模,又能支撑复用,何乐而不为?诚然,并非组w 8 Y * = x y件颗粒度越细越好,组件数量和小程序代码包巨细是正相关的。尤其是关于运用编译型结构(如 Taro)的项目,每个组件编译后都会发作额定的运转时代码和环境 polyfill,so,为了代码包空间,请坚持沉着…

事情总线,替代组件间数据绑定的通讯方法

WXML 数据绑定是小程序中父组件向子组件传递动态数据的较为常见的方法,如下面例程所示:Component A 组件中的变量 ab 经过组件特点传递给 Component B 组件。在此进程中,不可防止地需} _ & n求阅历一次 Component A 组件的 setData 调用方可完结任务,这就会发作线程间的通讯。“合情合理”,但,假如传递给子组件的数据只有一部分是与视图烘托有关呢?

<!-- Component A -->
<component-b prop-a="{{a}}" prop-b="{{b}}" />
// Component B
Component({
properties: {
propA: String,
propB: String,
},
methods: {
onLoadk h E: function() {
this.data.propA
thi4 / ys.dat- _ pan b C 1.propB
}
}
})

引荐一种特定场景下十分快捷的做法:经过事情总线(EventBus),: y也便是发布/订阅形式,来完结由父向子的数据传递。其构成十分简略(例程只供给要害代码…):

  • 一个大局的事情调度中心

    class EventBus {
    constructor() {
    this.events = {}
    }
    on(key, cb) { this.events[key].push(cb) }
    t5 j ] 6 / S irig4 {  | Zger(b  Xkey, args) {
    this.events[key].forEach(function (cb) {
    cb.call(this, ...args)
    })
    }
    remove() {}
    }
    const event = new EventBus()
    
  • 事情订阅者

    // 子组件
    Component({
    created() {
    event- ) ;.on('dataY C f H k _ c-ready', (data) => { th2 y & 8 = ]is.} x + } k w e Q /setData({ data }) })
    }
    })
    
  • 事情发布者

    // Parent
    Componen[  _t({
    ready() {
    event.trigger('data-ready', datg 9 2a)
    }
    })
    

子组件被创建时事先监听数据下发事情,当父组; E ( O件获取到C n c ~ u数据后触发事情把数据传递给子组件,这整个进程都是在小程序的逻辑层里同步2 9 l +履行,比数据绑定的方法速度更快。

但并非一切场景都合适这种做法。像京喜主页这种具有 “数据单向传递”“展现型交互” 特性、且 一级子组件数量巨大 的场景,运用事情总线的效益将会十分高;但若是频频 “双向数据流“ 的场景,用这种方法会导致事情交织难以保护。

题外话,TaL r yro 结构在处理父子组件间2 D % w数据传递时运用的是观察者形式,经过 Object.definePropert4 + H ; Qy 绑定父子组件联d E /系,当父组件数据发作变化时,会递归告诉一切* ? y G [ . 9子孙组件检查并更新数据。这个告诉的进程会同步触发数据 diff 和一些校验逻辑,每个组件跑一遍大约需求 5 ~ 10 ms 的时刻。所以,假如组件量级比较大,整个流程下来时刻损耗仍是不小的,咱们依旧能够测验事情总线的计划。

组件层面的 dif; 9 ! r p ^f

咱们或许会遇到这样的需求,多个组件之间位置不固定,支Y o 0撑随时随地灵敏装备,京喜主页也存在相似的诉求。

京喜主页主体可被划分为若干个事务组件(如查找框、导航栏、产品轮播等),这些事务组件的次序是不固定的,今天是查找框在最顶部,明日有或许变成导航栏在顶部了(夸张了…)。咱们不或许针对多种次序或许性供给多套完T w e V N结,这就需求用到小程序的自界Q O M t X e @ M n– n M ; ~模板 <template>

完结一个支撑调度一切事务组件的模板,依据后台下发的模块数组按序循环烘托模板,如下面例程所示。

<!-- index.wxml -->
<template name="render-component">
<search-bar wx:if="{{compId === 'R ] rSearchBar'}}" floor-id="{{index}}" />
<nav-bar wx:if="{{compId === 'NavBar'}}" floor-id="{{index}}"0 } . I k l ^ ? />
<banner wx:if="{{compId === 'Banner'}}" floor-i4 V K [ d="{{index}}" />
<icon-nav wx:if="{{compId === 'IconNav'}}" floor-id="G &  * p 4 N S{{index}}"] V ) }  0 : h />
</templateJ g T g l d u G N>M E & 1 | ( d C m
<view
class="component-wrapper"
wx:for="{{comps}}"
wx:for-item="comp"
>
<template is="render-component" data="{{...comp}}"/>
</view>
// search-bar.js
Component({
properties: {
floorId: Number,
},
created() {
event.on('data-ready', (comps) =&- 7 Tgt; {
const data = comps[this.data.floorId] // 依据楼层位置取数据
})
}
})

貌似十分轻松地完结需求,但值得考虑的是:假如组件次序调整了,一切@ K I v组件的生命周期会发作什么变化?

假定,U ] n +上一次烘托的组件次序是 ['search1 H ! N z _ ^ f-bar','nav-bar','banner', 'icon-nav'],现在需k – W 7 = * Y –求把 nav-bar 组件去掉,调整为 ['search-s I . {bar','banner', 'icon-nav']。经实验得出,当某个组件节点发作变化时,P H @ 6 Y S j B其前面的组件不受影响,其后面的组件都会被X E j H L #毁掉从头挂载。

原理很简略,每个组件都有各自阻隔的节O b x – % B 7 v点树(ShadowTree),页面 body 也是一个节点树。在调整组件次序时,小程序结构会遍历比较新/旧节点树的差异,所以发现新b H U G , . S t节点树的 nav-bar 组件节点不见了,就C ? /以为该(树)分支下从 nav-bar 节点起发作了变化,往后节点都需求重烘托。

但实际上,这儿的组j z 0件次序是没有变化的,丢掉的组件按道理不应该影响到其他组件的正常烘托。所以,咱们在 setData( A & 前先进行了新旧组t 6 m w # V件列表 diff:假如 newList 里边的组件是 oldList 的子集,且相对次序没有发作变化,则一切组件不从头挂载。除此之外,咱们还要在接口数据的z ~ ~ # V y相应位置填充上空数据,把该组件O 1 * – q i – $隐藏掉,done。

经过组件 diff 的手法,能够有用下Z N k n *降视图层的烘托压力,假如有相似场景的朋友,也能够参阅这种计划。

内存占用过高?

想必没有什么会比小程序 Crash 更影响用户体会了。

当小程序占用体系资源过高,就有或许会被体系毁掉或被微信客户` w d J R端主动收回! H h K : E Z O W应对这种尴尬场景,除了提示用户提高硬件功能之外(比如来京东商城买新手机),还能够经过一系列W D l – 5的优化手法下降小程序的内存损耗。

京喜小程序的高性能打造之路

内存预警

小程序供给了监听内存不足告警事情的 API:wx.onMemoryWarning,旨在让开发者收到告警时及时开释内存资源防止小程序 Crash。但是关于小程序开发者来说,内存资源现在是无法直接触碰的,最多便是调用 wx.reLaunch 清理一切页面栈,重载当时页面,来下: l 5降内存负荷(此计划过于粗犷,别冲动,想想就好…)。

不过内存告警的信息搜集却是有意义的,咱们能够把内存~ o ! B = R ! %告警信息(包含页面途径、客户端版本、终端手机型号等)上报到日志体系,剖分出哪些页面 Crash 率比较高,然4 Z ^后针对性地做优化,下降页面复杂度等等。

收回后台页面P F V 7 O D S计时器

依据双线程模型,小程序每一个页面都会独立一个 webview 线程,但逻辑层是单线程的,也便是一切的 webview 线程共享一个 JS 线程。以至于当页面切换到后台态时,仍然有或许抢占到逻辑层的资源,比如没有毁掉的 setIntervalsetU K W O V u . fTimeout 定时器:

// Page A
Page({
onLoad() {
let i = 0
setInterval(() => { i++ }, 100)
}
})

即便如小程序的 <swiper> 组件,在页面进入后台态时仍然是会继续轮播的。

正确的做法是,在页面 onHide 的时分手动把定q P L时器清理掉,有必要时再在 onShow 阶段恢复定时器。坦白讲,区区一个定时器回调函数的履行,关于体系的影呼应该是微不足道的,但不容忽视的是回调函数里的代码逻辑,比如在定时器回调里继续 setData 许多数据,* _ q kc | c U V就十分难受了…

防止频发事情中的重度内存操作

咱们经常会遇到这样的需求:广告曝光、图片懒加载、导航栏吸顶等等,r a y A X g这些都需求咱们在页面翻滚事情触发时实G 5 ` 1 f ( w a时监听元素位置或更新视图。在了解小程序的双线程模+ t x 2 K p 型之后不难, A P ( y 7 _ 8 f发现,页面翻滚时 onPageScroll 被频发触发,会使逻辑层和视图层发作继续通讯,若这时分再 “火上浇油” 调用 setData 传输许多数据,会导致内存运用率快速上升,使页面卡顿甚至 “假死”。所以,针对频发事情的监听,咱们最好遵循以下准则:

  • onPageScroll 事情回调运用节省;
  • 防止 CPU 密集型操作,比如复杂的核算;
  • 防止调用 setData,或减小 setData 的数据量;
  • 尽量运用 IntersectionObservey H Yr 来替代 SelectorQuen c ( w q % R qry,前者对功能~ d 6影响更小;

大图、长列表优化

据 小程序官方文档 描述,大图片和长列表图片在 iOS 中会引起 WKWebView 的收回,导致小程序 Crash。

关于大图片资源(比如满屏的 gif 图)来说,咱们只能尽或l d n W * W 1许对图片进行降P Z 2 M B I质或裁剪,P z i ,当然不运用是最好的。

关于长列表,比如瀑布流,这儿供给一种思路:咱们能够运用 Inp E 4tersectionObserver 监听长列表内/ X k n x U * M组件与视窗之间的相交状态,当组件间隔视窗大于某个临界点时,毁掉该组件开释内存空间,并用等尺度的骨架图占坑;当间隔小于临界r C ! 4 u点时,再取缓存~ ^ : : A z G数据从头加载该组件。

但是无可防止地,当用户快速翻滚长列表时,被毁掉的组件或许来不及加载? Q t O # ]完,视觉上就会出现短暂的白屏。咱们能够恰当地调整毁掉阈值,或者优化骨架图的款式来尽或许提高体会| – / 1 1 – 7 V @感。{ % j

小程序官方供给了一个 长列表组件,能够经过 npm 包的方法引入,有爱好的能够测验。

总结

结合上述的种种办法论,京喜小程序主页进行全方位晋级改造之后给出了答卷:

1. Audits 审计东西的功能得分 86

2. 优化后的首屏烘托完结时刻F v s . K 2 E(FMP):

京喜小程序的高性能打造之路

3. 优化前后的测速数据比照:

京喜小程序的高性能打造之路

但是,事务迭代在继续推动,多样化的用户– D 7 ) #场景徒增不减,功能优化将成为咱们日常开发中挥之不去的准则和主题。本文以微信小程序开发中与功能相关的问题为出发点,依据小程序的底层结构原理,探求小程序D * K功能体会提} K ! r X Z /高的各种或许性,期望能为各位小程序开发者带来参阅价值。

参阅

  • User-centric Performance Metrics
  • Reduce Ja& c { O ZvaScript Pa% w U _ R . 8yloads with Tree Shakinv N pg
  • 小程序开发攻略
  • 小程1 A $序官方文档
  • Taro 官方文档
  • 探求Z h ; ! H & UWebP一些事儿
  • 京喜主页(微信购物进口)跨端开发与优化实践

欢迎重视K M & 5 h } { d凹凸实验室博客:aotu.io

或者重视凹凸实验室公众号(AOTULabs),不定时推送文章:

京喜小程序的高性能打造之路