forked from RubyLouvre/anu
-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathtwo-threaded.html
133 lines (131 loc) · 23.6 KB
/
two-threaded.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<!DOCTYPE html><html lang="zh-CN"><head><meta charSet="UTF-8"/><meta content="text/html; charset=utf-8" http-equiv="Content-Type"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/><meta name="apple-mobile-web-app-capable" content="yes"/><meta name="apple-mobile-web-app-status-bar-style" content="black"/><link rel="icon" type="image/png" sizes="32x32" href="../images/titleLogo.png"/><link rel="icon" type="image/png" sizes="16x16" href="../images/titleLogo.png"/><meta name="theme-color" content="#ffffff"/><meta http-equiv="Cache-Control" content="no-transform"/><meta http-equiv="Cache-Control" content="no-siteapp"/><title>微信小程序的双线程架构-文档</title><link rel="stylesheet" href="../ydoc/styles/style.css"/><meta name="author" content="司徒正美"/><meta name="keywords"/><meta name="description" content="使用Reac微信小程序"/><meta id="releativePath" content=".."/><link rel="stylesheet" href="../ydoc/ydoc-plugin-search/search.css"/><link rel="stylesheet" href="../style/index.css"/></head><body><div class="g-doc"><div class="m-aside"><div class="m-summary" id="js-menu"><div class="m-summary-content" id="js-menu-content"><div class="m-summary-block"><ul class="m-summary-list"><li class="item"><a href="intro.html#" class="href">介绍</a></li><li class="item"><a href="install.html" class="href">安装与使用cli各种命令</a></li><li class="item"><a href="update.html" class="href">升级</a></li><li class="item"><a href="app.html" class="href">全局对象</a></li><li class="item"><a href="lifetimes.html" class="href">页面组件与生命周期</a></li><li class="item"><a href="page2.html" class="href">页面的各个功能讲解</a></li><li class="item"><a href="component.html" class="href">页面上的子组件使用</a></li><li class="item"><a href="standard.html" class="href">小程序组件规范</a></li><li class="item"><a href="jsx.html" class="href">使用 JSX 的注意事项</a></li><li class="item"><a href="style.html" class="href">使用 CSS 的注意事项</a></li><li class="item"><a href="hooks.html" class="href">React Hooks的支持</a></li><li class="item"><a href="publish.html" class="href">目录结构与输出目录</a></li><li class="item"><a href="import_js.html" class="href">按平台打包代码或样式</a></li><li class="item"><a href="tabBar.html" class="href">据平台设置tabBar</a></li><li class="item"><a href="titleBar.html" class="href">隐藏标题栏</a></li><li class="item"><a href="redux.html" class="href">Redux/Mobx的使用</a></li><li class="item"><a href="webview.html" class="href">智能 webview 化</a></li><li class="item"><a href="customConfig.html" class="href">自定义项目配置</a></li><li class="item"><a href="customBuildConfig.html" class="href">自定义打包配置</a></li><li class="item"><a href="nativeComponents.html" class="href">不转译某些标签名</a></li><li class="item"><a href="chaika.html" class="href">拆库开发</a></li><li class="item"><a href="alias.html" class="href">别名配置</a></li><li class="item"><a href="vscode.html" class="href">vscode插件</a></li><li class="item"><a href="report.html" class="href">日志收集与上传</a></li><li class="item"><a href="npm.html" class="href">npm支持</a></li><li class="item"><a href="wxplugin.html" class="href">微信插件支持</a></li><li class="item"><a href="iconfont.html" class="href">iconfont 支持</a></li><li class="item"><a href="async.html" class="href">async/await 支持</a></li><li class="item"><a href="lesssass.html" class="href">Sass、Less、PostCSS支持</a></li><li class="item"><a href="patchComponent.html" class="href">内置 UI 库</a></li><li class="item"><a href="units.html" class="href">rpx与px互转</a></li><li class="item"><a href="event.html" class="href">事件系统</a></li><li class="item"><a href="size.html" class="href">包大小限制</a></li><li class="item"><a href="subpackages.html" class="href">分包加载</a></li><li class="item"><a href="preload.html" class="href">分包预加载</a></li><li class="item"><a href="debug.html" class="href">真机调试</a></li><li class="item"><a href="bu.html" class="href">百度小程序环境判定</a></li><li class="item"><a href="tt.html" class="href">字节小程序相关</a></li><li class="item"><div class="m-summary-block"><a href="" class="href">快应用相关</a><ul class="m-summary-list indent"><li class="item"><a href="quicklist.html" class="href">快应用的scroll-view兼容</a></li><li class="item"><a href="quickstorage.html" class="href">快应用的同步Storage API兼容</a></li><li class="item"><a href="boxDifferent.html" class="href">快应用与小程序盒子模型的区别</a></li><li class="item"><a href="query.html" class="href">小米快应用获取页面参数</a></li><li class="item"><a href="huaweiQuery.html" class="href">华为快应用获取页面参数</a></li><li class="item"><a href="share.html" class="href">快应用与小程序转发分享</a></li><li class="item"><a href="onblur.html" class="href">快应用onblur</a></li><li class="item"><a href="scene.html" class="href">场景值的兼容</a></li><li class="item"><a href="card.html" class="href">卡片</a></li><li class="item"><a href="movable.html" class="href">movable-area与movable-view的模拟</a></li></ul></div></li><li class="item"><a href="diff.html" class="href">各种小程序的比较</a></li><li class="item"><a href="link.html" class="href">各种小程序的官网</a></li><li class="item"><div class="m-summary-block"><a href="feedback.html" class="href">反馈收集</a><ul class="m-summary-list indent"><li class="item"><a href="" class="href">微信小程序的双线程架构</a></li></ul></div></li><li class="item"><a href="questions.html" class="href">常见问题</a></li><li class="item"><a href="about.html" class="href">关于</a></li></ul></div></div></div><div class="m-summary-switch" id="js-summary-switch"><svg viewBox="0 0 926.23699 573.74994" version="1.1" x="0px" y="0px" width="15" height="15" class="bottom"><g transform="translate(904.92214,-879.1482)"><path d="m -673.67664,1221.6502 -231.2455,-231.24803 55.6165,-55.627 c 30.5891,-30.59485 56.1806,-55.627 56.8701,-55.627 0.6894,0 79.8637,78.60862 175.9427,174.68583 l 174.6892,174.6858 174.6892,-174.6858 c 96.079,-96.07721 175.253196,-174.68583 175.942696,-174.68583 0.6895,0 26.281,25.03215 56.8701,55.627 l 55.6165,55.627 -231.245496,231.24803 c -127.185,127.1864-231.5279,231.248 -231.873,231.248 -0.3451,0 -104.688,-104.0616 -231.873,-231.248 z" fill="#fff"></path></g></svg><svg viewBox="0 0 926.23699 573.74994" version="1.1" x="0px" y="0px" width="15" height="15" class="top"><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="aaa" fill="#fff" fill-rule="nonzero"><path d="M231.2455,342.502 L0,111.25397 L55.6165,55.62697 C86.2056,25.03212 111.7971,-2.99999998e-05 112.4866,-2.99999998e-05 C113.176,-2.99999998e-05 192.3503,78.60859 288.4293,174.6858 L463.1185,349.3716 L637.8077,174.6858 C733.8867,78.60859 813.060896,-2.99999997e-05 813.750396,-2.99999997e-05 C814.439896,-2.99999997e-05 840.031396,25.03212 870.620496,55.62697 L926.236996,111.25397 L694.9915,342.502 C567.8065,469.6884 463.4636,573.75 463.1185,573.75 C462.7734,573.75 358.4305,469.6884 231.2455,342.502 Z" id="Shape" transform="translate(463.118498, 286.874985) scale(1, -1) translate(-463.118498, -286.874985) "></path></g></g></svg></div></div><div class="m-main" id="js-panel"><header class="m-header" id="js-header"><div class="m-header-title js-logo"><a href="../index.html" target="_self"><img class="logo" width="36" src="../images/logo@2x.png"/></a></div><div><div class="m-search">
<div class="icon"></div>
<input type="text" class="input js-input" placeholder="搜索" />
<div class="m-search-result js-search-result"></div>
</div></div><nav class="m-header-nav js-nav"><ul class="m-header-items"><li class="item active"><a class="href" href="intro.html">文档</a></li><li class="item "><a class="href" href="../apis/index.html">已兼容处理的API</a></li><li class="item "><a class="href" href="logo.html">谁在用nanachi</a></li><li class="item "><a class="href" href="about.html">关于</a></li></ul></nav><div id="js-nav-btn" class="m-header-btn ui-font-ydoc"></div></header><div class="m-content" id="js-content"><div id="markdown-body" class="m-content-container markdown-body"><h1>微信小程序的双线程架构</h1>
<blockquote>
<p>节选自微保@梁沛聪 的《 微信小程序渲染性能调优》</p>
</blockquote>
<blockquote>
<p>@梁沛聪,微保技术部-架构平台中心高级前端开发,负责微保小程序基础库搭建、车险业务开发等工作,对小程序性能优化有深入地实践。</p>
</blockquote>
<blockquote>
<p>为了叙述方便,下文会把微信小程序简称为小程序。</p>
</blockquote>
<p>与传统的浏览器Web页面最大区别在于,小程序的是基于 双线程 模型的,在这种架构中,小程序的渲染层使用 WebView 作为渲染载体,而逻辑层则由独立的 JsCore 线程运行 JS 脚本,双方并不具备数据直接共享的通道,因此渲染层和逻辑层的通信要由 Native 的 JSBrigde 做中转。</p>
<p><img src="two-threaded02.png" alt></p>
<h2 id="小程序更新视图数据的通信流程">小程序更新视图数据的通信流程</h2>
<p>每当小程序视图数据需要更新时,逻辑层会调用小程序宿主环境提供的 setData 方法将数据从逻辑层传递到视图层,经过一系列渲染步骤之后完成UI视图更新。完整的通信流程如下:</p>
<ol>
<li>小程序逻辑层调用宿主环境的 setData 方法。</li>
<li>逻辑层执行 JSON.stringify 将待传输数据转换成字符串并拼接到特定的JS脚本,并通过evaluateJavascript 执行脚本将数据传输到渲染层。</li>
<li>渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。</li>
<li>WebView 线程开始执行渲染时,待更新数据会合并到视图层保留的原始 data 数据,并将新数据套用在WXML片段中得到新的虚拟节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。同时,将新的节点树替换旧节点树,用于下一次重渲染。</li>
</ol>
<h2 id="引发渲染性能问题的一些原因">引发渲染性能问题的一些原因</h2>
<p>在上述通信流程中,一些不恰当的操作可能会影响到页面渲染的性能。</p>
<h2 id="setdata传递大量的新数据">setData传递大量的新数据</h2>
<p>数据的传输会经历跨线程传输和脚本编译的过程,当数据量过大,会增加脚本编译的执行时间,占用 WebView JS 线程。</p>
<p>下图是我们做的一组测试统计:在相同网络环境下,各个机型分别对大小为1KB、2KB、3KB的数据执行 setData 操作所消耗的时间。</p>
<p><img src="two-threaded02.png" alt></p>
<p>从图中可以看出, setData 数据传输量越大,数据传输所消耗的时间越大。</p>
<h2 id="频繁的执行setdata操作">频繁的执行setData操作</h2>
<p>频繁的执行 setData 会让 WebView JS 线程一直忙碌于脚本的编译、节点树的对比计算和页面渲染。导致的结果是:</p>
<ul>
<li>页面渲染结果有一定的延时。</li>
<li>用户触发页面事件时,因 WebView JS 线程忙碌,用户事件未能及时的传输到逻辑层而导致反馈延迟。</li>
</ul>
<h2 id="过多的页面节点数">过多的页面节点数</h2>
<ul>
<li>页面初始渲染时,渲染树的构建、计算节点几何信息以及绘制节点到屏幕的时间开销都跟页面节点数量成正相关关系,页面节点数量越多,渲染耗时越长。</li>
<li>每次执行 setData 更新视图,WebView JS 线程都要遍历节点树计算新旧节点数差异部分。当页面节点数量越多,计算的时间开销越大,减少节点树节点数量可以有效降低重渲染的时间开销。</li>
</ul>
<h2 id="渲染性能优化">渲染性能优化</h2>
<p>基于引发渲染性能问题的原因,我们可以制定一些优化策略来避免性能问题的发生。</p>
<h2 id="setdata优化">setData优化</h2>
<p>setData 作为逻辑层与视图层通信的媒介,是最容易造成渲染性能瓶颈的 API 。我们在使用 setData时应该遵循一些规则来尽可能避免性能问题的发生:</p>
<h2 id="减少-setdata-数据传输量">减少 setData 数据传输量</h2>
<ul>
<li>仅传输视图层使用到的数据,其他 JS 环境用到的数据存放到 data 对象外。</li>
<li>合理利用局部更新。setData 是支持使用 数据路径 的方式对对象的局部字段进行更新,我们可能会遇到这样的场景:list 列表是从后台获取的数据,并展示在页面上,当 list 列表的第一项数据的 src 字段需要更新时,一般情况下我们会从后台获取新的 list 列表,执行 setData 更新整个 list 列表。</li>
</ul>
<pre><code class="language-javascript"><span class="token comment">// 后台获取列表数据</span>
<span class="token keyword">const</span> list <span class="token operator">=</span> <span class="token function">requestSync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// 更新整个列表</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setData</span><span class="token punctuation">(</span><span class="token punctuation">{</span> list <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>实际上,只有个别字段需要更新时,我们可以这么写来避免整个 list 列表更新:</p>
<pre><code class="language-javascript"><span class="token comment">// 后台获取列表数据</span>
<span class="token keyword">const</span> list <span class="token operator">=</span> <span class="token function">requestSync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// 局部更新列表</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setData</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token string-property property">'list[0].src'</span><span class="token operator">:</span> list<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>src
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<h2 id="降低-setdata-执行频率">降低 setData 执行频率</h2>
<p>在不影响业务流程的前提下,将多个 setData 调用合并执行,减少线程间通信频次。</p>
<p>当需要在频繁触发的用户事件(如 PageScroll 、 Resize 事件)中调用 setData ,合理的利用 函数防抖(debounce) 和 函数节流(throttle) 可以减少 setData 执行次数。</p>
<blockquote>
<p>函数防抖(debounce):函数在触发n秒后才执行一次,如果在n秒内重复触发函数,则重新计算时间。
函数节流(throttle):单位时间内,只会触发一次函数,如果同一个单位时间内触发多次函数,只会有一次生效。</p>
</blockquote>
<p>除了让开发者自觉遵循规则来减少 setData 数据传输量和执行频率之外,我们还可以自己设计一个 diff 算法,重新对 setData 进行封装,使得在 setData 执行之前,让待更新的数据与原 data 数据做 diff 对比,计算出数据差异 patch 对象,判断 patch 对象是否为空,如果为空则跳过执行更新,否则再将 patch 对象执行 setData 操作,从而达到减少数据传输量和降低执行 setData 频率的目的。</p>
<pre><code class="language-javascript"><span class="token comment">// setData重新封装成新的方法,使得数据更新前先对新旧数据做diff对比,再执行setData方法</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function-variable function">update</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve<span class="token punctuation">,</span> reject</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token function">diff</span><span class="token punctuation">(</span>data<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>result<span class="token punctuation">)</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">resolve</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setData</span><span class="token punctuation">(</span>result<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token function">resolve</span><span class="token punctuation">(</span>result<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>具体流程如下图:</p>
<p><img src="two-threaded03.png" alt></p>
<h2 id="善用自定义组件">善用自定义组件</h2>
<p>小程序自定义组件的实现是由小程序官方设计的 Exparser 框架所支持,框架实现的自定义组件的组件模型与 Web Components 标准的 Shadow DOM 相似:</p>
<p><img src="two-threaded04.png" alt></p>
<p>在页面引用自定义组件后,当初始化页面时,Exparser 会在创建页面实例的同时,也会根据自定义组件的注册信息进行组件实例化,然后根据组件自带的 data 数据和组件WXML,构造出独立的 Shadow Tree ,并追加到页面 Composed Tree 。创建出来的 Shadow Tree 拥有着自己独立的逻辑空间、数据、样式环境及setData调用:</p>
<p><img src="two-threaded05.png" alt></p>
<p>基于自定义组件的 Shadow DOM 模型设计,我们可以将页面中一些需要高频执行 setData 更新的功能模块(如倒计时、进度条等)封装成自定义组件嵌入到页面中。</p>
<p>当这些自定义组件视图需要更新时,执行的是组件自己的 setData ,新旧节点树的对比计算和渲染树的更新都只限于组件内有限的节点数量,有效降低渲染时间开销。</p>
<p>下图是我们在微保小程序WeDrive首页中,将倒计时模块抽取自定义组件前后的setData更新耗时对比:</p>
<p><img src="two-threaded06.png" alt></p>
<p>从图中可以看出,使用自定义组件后,倒计时模块 setData 平均渲染耗时有了非常明显的下降,实际在低端安卓机中体验会感觉明显的更流畅。</p>
<p>当然,并不是使用自定义组件越多会越好,页面每新增一个自定义组件, Exparser 需要多管理一个组件实例,内存消耗会更大,当内存占用上升到一定程度,有可能导致 iOS 将部分 WKWebView 回收,安卓机体验会变得更加卡顿。因此要合理的使用自定义组件,同时页面设计也要注意不滥用标签。</p>
<p>总结</p>
<p>小程序双线程架构决定了数据通信优化会是性能优化中一个比较重要的点。而上述提到的几个优化建议只是优化渲染性能中的一部分,要想让你的页面体验变得更加丝滑,就要熟悉小程序框架的底层原理,根据小程序框架的特点,编写出“合身”的前端代码。</p>
</div><div class="m-content-container m-paging"><div class="m-paging-prev m-paging-item"><a href="feedback.html" class="href"><span class="ui-font-ydoc"></span>反馈收集</a></div><div class="m-paging-next m-paging-item"><a href="questions.html" class="href">常见问题<span class="ui-font-ydoc"></span></a></div></div></div></div></div><div></div><script>
var $content = document.getElementById('js-content');
var $summaryItems = Array.prototype.slice.call(document.querySelectorAll('#js-menu .href'));
var $menu = document.getElementById('js-menu');
if (sessionStorage.prevPathname) {
sessionStorage.setItem('prevPrevPathname', sessionStorage.prevPathname);
sessionStorage.setItem('prevPrevMenuScrollTop', sessionStorage.prevMenuScrollTop);
sessionStorage.setItem('prevPrevContentScrollTop', sessionStorage.prevContentScrollTop);
}
if (sessionStorage.locationPathname) {
sessionStorage.setItem('prevPathname', sessionStorage.locationPathname);
sessionStorage.setItem('prevMenuScrollTop', sessionStorage.menuScrollTop);
sessionStorage.setItem('prevContentScrollTop', sessionStorage.contentScrollTop);
}
if ($menu && sessionStorage.menuScrollTop) {
$menu.scrollTop = sessionStorage.menuScrollTop;
}
// 刷新页面但不切换 pathname 的时候,内容区恢复到记忆的高度
if ($content && sessionStorage.contentScrollTop && window.location.pathname == sessionStorage.locationPathname) {
$content.scrollTop = sessionStorage.contentScrollTop;
} else if (sessionStorage.prevPrevPathname && sessionStorage.prevPrevPathname === window.location.pathname) {
if ($menu && sessionStorage.prevPrevMenuScrollTop) {
$menu.scrollTop = sessionStorage.prevPrevMenuScrollTop;
}
if ($content && sessionStorage.prevPrevContentScrollTop) {
$content.scrollTop = sessionStorage.prevPrevContentScrollTop;
}
}
sessionStorage.setItem('locationPathname', window.location.pathname);
</script><script src="../ydoc/scripts/plugins/dollar.min.js"></script><script src="../ydoc/scripts/plugins/responsive-nav.min.js"></script><script src="../ydoc/scripts/plugins/slideout.min.js"></script><script src="../ydoc/scripts/app.js"></script><script src="../ydoc/ydoc-plugin-search/core.js"></script><script src="../ydoc/ydoc-plugin-search/search.js"></script><script src="../search_json.js"></script></body></html>