轮播图
<p>[TOC]</p>
<h5>简要描述</h5>
<ul>
<li>轮播图</li>
</ul>
<h5>请求URL</h5>
<ul>
<li><code>https://creationapi.shbwyz.com/api/article/all/recommend</code></li>
</ul>
<h5>请求方式</h5>
<ul>
<li>GET </li>
</ul>
<h5>参数</h5>
<table>
<thead>
<tr>
<th style="text-align: left;">参数名</th>
<th style="text-align: left;">必选</th>
<th style="text-align: left;">类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">statusCode</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">string</td>
<td>返回状态码</td>
</tr>
<tr>
<td style="text-align: left;">msg</td>
<td style="text-align: left;">否</td>
<td style="text-align: left;">string</td>
<td>状态提示</td>
</tr>
<tr>
<td style="text-align: left;">success</td>
<td style="text-align: left;">否</td>
<td style="text-align: left;">boolean</td>
<td>真假</td>
</tr>
<tr>
<td style="text-align: left;">data</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">Object</td>
<td>地址信息</td>
</tr>
</tbody>
</table>
<h5>返回示例</h5>
<pre><code>{
"statusCode": 200,
"msg": null,
"success": true,
"data": [
{
"id": "dfe7861c-c184-4126-86ee-ee4549e9f714",
"title": "微信小程序首屏性能优化",
"cover": "https://wipi.oss-cn-shanghai.aliyuncs.com/2021-12-11/IMG_031434567.jpeg",
"summary": "老生常谈的性能优化,在微信小程序中又应该如何去实践?",
"content": "## 首屏时间定义\n\n小程序启动流程:\n\n点击小程序图标 → 小程序启动 → 页面开始加载 → 页面主要框架加载完成 → 图片及资源下载 → 页面完全加载\n\n其中,从点击小程序图标开始到页面主要框架加载完成是首屏启动时间。\n\n## 优化治理\n\n首屏启动时间分为小程序启动和页面加载完成两阶段,需要分开治理优化。\n\n![首屏加载时机](https://wipi.oss-cn-shanghai.aliyuncs.com/2021-12-08/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%A6%96%E5%B1%8F%E5%8A%A0%E8%BD%BD.png)\n\n### 小程序启动阶段\n\n小程序框架启动主要包括小程序信息及环境准备、代码包下载和代码编译三个主要步骤。对开发者来说都是黑盒实现,只能根据官方文档和开发经验来总结实际的影响点。目前来看,从开发者角度来说,就是减少发版频次。因为发版会导致小程序启动时需要更新信息,并有阻塞逻辑。同时,代码包下载和编译受大小影响,需要控制代码体积。\n\n### 首屏渲染阶段\n\n#### 1. 初始执行优化\n\n根据启动阶段的必要性,建立优先级机制:\n\n- 阻塞首屏渲染的逻辑同步执行\n- 不影响首屏渲染的逻辑延迟执行\n\n比如,通过 wx:if 控制非首屏组件延迟加载(如各类弹窗、挂件),在首屏渲染完成后再加载渲染,不仅可以节省组件初始化耗时,还可以减少首屏期间网络请求和 setData。此外,上报接口、非首屏依赖 SDK 等非首屏依赖项延迟执行。\n\n#### 2. 数据加载优化\n\n##### 1. 缓存数据读取\n\n在组件 attached 阶段通过 wx.getStorageSync 读取并使用缓存进行渲染\n\n##### 2. 接口请求优化\n\n在 app 的 onLaunch 阶段对数据进行预加载,因为从 app onLaunch 到页面 onLoad 会需要几百毫秒耗时\n\n##### 3. 接口耗时优化\n\n主要通过接口合并、数据精简、域名合并等操作提升端到端耗时\n\n#### 3. 页面渲染优化\n\n小程序采用双线程渲染模型,逻辑层通过 setData 将数据发送到视图层,视图层对比前后差异,更新 DOM。setData 受次数和数据量影响,WXML 受 DOM 节点数量和层级影响。\n\n##### 1. setData\n\n控制数据量(非渲染数据不要放到 data 里),延迟执行非首屏的 setData,合并 setData 来优化次数等。\n\n##### 2. WXML\n\n删除冗余节点,优化节点层级。\n\n##### 3. 图片\n\nC 端业务往往会使用大量图片。图片本身所要占据的内存也比较大,可以对图片进行压缩裁剪,缓存(减少网络请求及带宽消耗)以及图片 lazyload,同时可以结合存储服务(如 s3)对图片进行压缩、裁剪,以及使用 webp 格式。\n\n## 技术手段\n\n### setData 的优化\n\nsetData 方面的优化只能从次数和数据量来入手。次数优化的手段:收集一段时间内的 setData 合并成一次 setData;数据大小的优化:对 setData 提交的数据和之前的数据进行 diff,diff 成形如:`this.setData({ 'a.b.c': 'newVal' })` 进行精准更新。为此,可以劫持微信的 `setData`,为其加上 `diff` 和调度更新的能力。\n\n#### 调度手段\n\n横向调研了几个开源框架的调度方案:\n\n- Taro3:setTimeout 0 进行合并 setData\n- Wepy2 和 Mpvue:使用 throttle 50 进行合并 setData\n- MPX:使用 Promise.then 进行合并 setData\n\n结合真机测试数据,最终选择了 setTimeout 的合并调度 setData 策略。\n\n#### 编码实现\n\n1. 对 data 进行 diff,控制数据量\n2. 减少 setData 次数,作合并\n3. onReady 之前的 setData 统一合并\n\n\n##### diff\n\n```js\nconst OBJECT_TYPE = '[object Object]';\nconst ARRAY_TYPE = '[object Array]';\n\nconst getType = obj => Object.prototype.toString.call(obj);\n\nconst initPath = data => {\n if (getType(data) !== OBJECT_TYPE) return;\n for (const item in data) {\n if (/\\w+\\.\\w+/g.test(item) && item.indexOf('[') === -1) {\n const arr = item.split('.');\n let result = data;\n const len = arr.length;\n for (let i = 0; i < len - 1; i++) {\n const arrItem = arr[i];\n if (getType(result[arrItem]) !== OBJECT_TYPE) {\n result[arrItem] = {};\n }\n result = result[arrItem];\n }\n result[arr[len - 1]] = data[item];\n delete data[item];\n }\n }\n};\n\nconst initData = (cur, pre, root = false) => {\n if (cur === pre) return;\n const curType = getType(cur);\n const preType = getType(pre);\n if (curType !== preType) return;\n if (curType === ARRAY_TYPE && cur.length >= pre.length) {\n for (let i = 0; i < pre.length; i++) {\n initData(cur[i], pre[i]);\n }\n } else if (curType === OBJECT_TYPE && Object.keys(cur).length >= Object.keys(pre).length) {\n for (const key in pre) {\n if (!root && cur[key] === undefined) {\n cur[key] = null;\n } else {\n initData(cur[key], pre[key]);\n }\n }\n }\n};\n\nconst doDiff = (cur, pre, target, path = '', root = false) => {\n if (cur === pre) return;\n const curRootType = getType(cur);\n const preRootType = getType(pre);\n if (curRootType === ARRAY_TYPE && preRootType === curRootType && cur.length >= pre.length) {\n for (let i = 0; i < cur.length; i++) {\n doDiff(cur[i], pre[i], target, `${path}[${i}]`);\n }\n return;\n }\n if (\n curRootType === OBJECT_TYPE &&\n preRootType === curRootType &&\n (root || Object.keys(cur).length >= Object.keys(pre).length)\n ) {\n const keys = Object.keys(cur);\n for (const key of keys) {\n const curVal = cur[key];\n const preVal = pre[key];\n const curType = getType(curVal);\n const preType = getType(preVal);\n if (curVal === preVal) continue;\n if (curType === ARRAY_TYPE && preType === curType && curVal.length >= preVal.length) {\n for (let i = 0; i < curVal.length; i++) {\n doDiff(curVal[i], preVal[i], target, `${path ? `${path}.` : ''}${key}[${i}]`);\n }\n continue;\n }\n if (\n curType === OBJECT_TYPE &&\n preType === curType &&\n Object.keys(curVal).length >= Object.keys(preVal).length\n ) {\n for (const sKey in curVal) {\n doDiff(curVal[sKey], preVal[sKey], target, `${path ? `${path}.` : ''}${key}.${sKey}`);\n }\n continue;\n }\n target[`${path ? `${path}.` : ''}${key}`] = curVal;\n }\n return;\n }\n target[path] = cur;\n};\n\nexport function diffData(data, prevData) {\n const target = {};\n initPath(data);\n initData(data, prevData, true);\n doDiff(data, prevData, target, '', true);\n return target;\n}\n```\n\n##### reconcile\n\n```js\nexport function noop() {}\n\n\nexport function shape(source, target) {\n const accumulator = {};\n const keys = Object.keys(target);\n\n for (const key of keys) {\n accumulator[key] = source[key];\n }\n\n return accumulator;\n}\n\nfunction _basePath(path) {\n if (Array.isArray(path)) return path;\n return path.replace(/\\[/g, '.').replace(/\\]/g, '').split('.')\n}\n\nfunction set(object, path, value) {\n if (typeof object !== 'object') return object;\n\n _basePath(path).reduce((o, k, i, _) => {\n if (i === _.length - 1) {\n o[k] = value\n return null\n } else if (k in o) {\n return o[k]\n } else {\n o[k] = /^[0-9]{1,}$/.test(_[i + 1]) ? [] : {}\n return o[k]\n }\n }, object)\n\n return object;\n}\n\nexport function syncData(ctx, data) {\n Object.keys(data).forEach(key => {\n set(ctx.data, key, data[key])\n })\n}\n\nfunction getTick() {\n return fn => setTimeout(fn, 0);\n}\n\nexport const tick = getTick();\n\nexport class Stream {\n data: Array<unknown> = [];\n listeners = {};\n hasEnd: boolean = false;\n\n constructor () {\n this.data = [];\n this.listeners = {};\n this.hasEnd = false;\n }\n\n on(key, callback) {\n if (typeof callback !== 'function') return;\n\n this.listeners[key] = this.listeners[key] || [];\n this.listeners[key].push(callback);\n }\n\n emit(key, ...args) {\n const callbacks = this.listeners[key] || [];\n\n for (const cb of callbacks) {\n cb(...args);\n }\n }\n\n add(data) {\n this.data.push(data);\n this.emit('data', data)\n }\n\n end() {\n if (this.hasEnd) return;\n this.hasEnd = true;\n this.emit('end', this.data);\n }\n\n getData() {\n if (!this.data.length) return null;\n\n const ret = Object.create(null);\n\n while (this.data.length > 0) {\n const unit = this.data.shift();\n Object.assign(ret, unit);\n }\n\n return ret;\n }\n\n destroy() {\n this.data = [];\n this.listeners = {};\n this.hasEnd = false;\n }\n}\n\n\nexport function getReconciler() {\n let onLoadTime = 0;\n let mounted = null;\n let perf = null;\n let ctx = null;\n let workingDiffs = [];\n let workingCallbacks = [];\n let pendingUpdate = false;\n let pendingFlush = false;\n let beforeMountedSetData = [];\n let beforeMountedCallback = [];\n\n let stream = null;\n\n function enqueueUpdate(data) {\n workingDiffs.push(data);\n\n if (!pendingUpdate) {\n performUpdate();\n }\n }\n\n function enqueueUpdateCallback(callback) {\n workingCallbacks.push(() => {\n callback();\n });\n }\n\n function performUpdate() {\n pendingUpdate = true;\n\n const render = () => {\n perf && perf.start();\n const data = Object.create(null);\n\n while (workingDiffs.length > 0) {\n const diff = workingDiffs.shift();\n Object.assign(data, diff);\n }\n\n pendingUpdate = false;\n perf && perf.record(data);\n\n ctx && ctx.originalSetData(data, () => {\n perf && perf.stop();\n if (!pendingFlush) {\n flushUpdateCallback();\n }\n });\n }\n\n tick(render);\n }\n\n function flushUpdateCallback() {\n pendingFlush = true;\n const cbs = workingCallbacks.slice(0);\n workingCallbacks.length = 0;\n for (const cb of cbs) {\n cb();\n }\n pendingFlush = false;\n }\n\n function update(this: any, data, callback = noop) {\n if (!ctx) ctx = this;\n if (!perf) perf = getPerf({ ctx: this, onLoadTime, name: this.tag || this.data.tag || 'unknown' });\n if (mounted === null) {\n syncData(ctx, data)\n stream?.add(data);\n beforeMountedCallback.push(callback);\n return;\n }\n if (mounted === false) return; // 已卸载\n\n stream?.end();\n perf?.count();\n\n const previous = shape(ctx.data, data);\n const diff = diffData(data, previous);\n // 微信行为:同步更新数据\n syncData(ctx, data);\n\n if (!Object.keys(diff).length) return;\n\n enqueueUpdate(diff);\n enqueueUpdateCallback(callback);\n }\n\n function flushBeforeMountedSetDataCallbacks() {\n const cbs = beforeMountedCallback.slice(0);\n beforeMountedCallback.length = 0;\n for (const cb of cbs) {\n cb();\n }\n }\n\n function init(this: any) {\n onLoadTime = Date.now();\n this.originalSetData = this.setData.bind(this);\n this.setData = update.bind(this);\n stream = new Stream();\n stream?.on('end', () => {\n const data = stream.getData();\n if (!data) return;\n this.originalSetData(data, flushBeforeMountedSetDataCallbacks);\n })\n }\n\n function mount(this: any) {\n mounted = true;\n stream?.end();\n }\n\n function unmount() {\n stream = null;\n mounted = false;\n perf = null;\n ctx = null;\n workingDiffs = [];\n workingCallbacks = [];\n pendingUpdate = false;\n pendingFlush = false;\n beforeMountedSetData = [];\n beforeMountedCallback= [];\n }\n\n return { init, mount, unmount };\n}\n```\n\n##### wrap\n\n```js\nexport function reconcileComponent(component) {\n component.lifetimes = component.lifetimes || {};\n const originalCreated = component.created || component.lifetimes.created || noop;\n const originalDetached = component.detached || component.lifetimes.detached || noop;\n\n let reconciler = null;\n\n function created(this: any, ...args) {\n reconciler = getReconciler();\n reconciler.init.apply(this, args);\n reconciler.mount.apply(this, args);\n originalCreated.apply(this, args);\n }\n\n function detached(this: any, ...args) {\n reconciler.unmount.apply(this, args);\n originalDetached.apply(this, args);\n }\n\n component.created = created;\n component.lifetimes.created = created;\n component.detached = detached;\n component.lifetimes.detached = detached;\n\n return component;\n}\n\nexport function reconcilePage(page) {\n const originalOnLoad = page.onLoad || noop;\n const originalOnReady = page.onReady || noop;\n const originalOnUnload = page.onUnload || noop;\n\n let reconciler = null;\n\n page.onLoad = function (...args) {\n reconciler = getReconciler();\n reconciler.init.apply(this, args);\n originalOnLoad.apply(this, args);\n };\n\n page.onReady = function (...args) {\n originalOnReady.apply(this, args);\n reconciler.mount.apply(this, args);\n };\n\n page.onUnload = function (...args) {\n reconciler.unmount.apply(this, args);\n originalOnUnload.apply(this, args);\n };\n\n return page;\n}\n```\n\n\n## 总结\n\n小程序的首屏性能治理涉及到各阶段各个方面,需要各个监控埋点,分析优化。\n\n### 1. 启动优化\n\n - 减少发版频次、控制代码包体积\n\n### 2. 渲染优化\n\n - 延迟执行非首屏逻辑、延迟加载非首屏组件\n - 接口请求:接口合并、数据精简、域名收敛、预加载\n - 数据缓存\n - setData:合并 setData 减少次数、控制数据量大小、数据 diff 精准更新\n - WXML:控制节点数、层级\n - 图片:压缩裁剪、懒加载、选择合适的格式(webp)、图片缓存\n\n",
"html": "<h2 id=\"首屏时间定义\">首屏时间定义</h2>\n<p>小程序启动流程:</p>\n<p>点击小程序图标 → 小程序启动 → 页面开始加载 → 页面主要框架加载完成 → 图片及资源下载 → 页面完全加载</p>\n<p>其中,从点击小程序图标开始到页面主要框架加载完成是首屏启动时间。</p>\n<h2 id=\"优化治理\">优化治理</h2>\n<p>首屏启动时间分为小程序启动和页面加载完成两阶段,需要分开治理优化。</p>\n<p><img src=\"https://wipi.oss-cn-shanghai.aliyuncs.com/2021-12-08/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%A6%96%E5%B1%8F%E5%8A%A0%E8%BD%BD.png\" alt=\"首屏加载时机\" /></p>\n<h3 id=\"小程序启动阶段\">小程序启动阶段</h3>\n<p>小程序框架启动主要包括小程序信息及环境准备、代码包下载和代码编译三个主要步骤。对开发者来说都是黑盒实现,只能根据官方文档和开发经验来总结实际的影响点。目前来看,从开发者角度来说,就是减少发版频次。因为发版会导致小程序启动时需要更新信息,并有阻塞逻辑。同时,代码包下载和编译受大小影响,需要控制代码体积。</p>\n<h3 id=\"首屏渲染阶段\">首屏渲染阶段</h3>\n<h4 id=\"1-初始执行优化\">1. 初始执行优化</h4>\n<p>根据启动阶段的必要性,建立优先级机制:</p>\n<ul>\n<li>阻塞首屏渲染的逻辑同步执行</li>\n<li>不影响首屏渲染的逻辑延迟执行</li>\n</ul>\n<p>比如,通过 wx:if 控制非首屏组件延迟加载(如各类弹窗、挂件),在首屏渲染完成后再加载渲染,不仅可以节省组件初始化耗时,还可以减少首屏期间网络请求和 setData。此外,上报接口、非首屏依赖 SDK 等非首屏依赖项延迟执行。</p>\n<h4 id=\"2-数据加载优化\">2. 数据加载优化</h4>\n<h5 id=\"1-缓存数据读取\">1. 缓存数据读取</h5>\n<p>在组件 attached 阶段通过 wx.getStorageSync 读取并使用缓存进行渲染</p>\n<h5 id=\"2-接口请求优化\">2. 接口请求优化</h5>\n<p>在 app 的 onLaunch 阶段对数据进行预加载,因为从 app onLaunch 到页面 onLoad 会需要几百毫秒耗时</p>\n<h5 id=\"3-接口耗时优化\">3. 接口耗时优化</h5>\n<p>主要通过接口合并、数据精简、域名合并等操作提升端到端耗时</p>\n<h4 id=\"3-页面渲染优化\">3. 页面渲染优化</h4>\n<p>小程序采用双线程渲染模型,逻辑层通过 setData 将数据发送到视图层,视图层对比前后差异,更新 DOM。setData 受次数和数据量影响,WXML 受 DOM 节点数量和层级影响。</p>\n<h5 id=\"1-setdata\">1. setData</h5>\n<p>控制数据量(非渲染数据不要放到 data 里),延迟执行非首屏的 setData,合并 setData 来优化次数等。</p>\n<h5 id=\"2-wxml\">2. WXML</h5>\n<p>删除冗余节点,优化节点层级。</p>\n<h5 id=\"3-图片\">3. 图片</h5>\n<p>C 端业务往往会使用大量图片。图片本身所要占据的内存也比较大,可以对图片进行压缩裁剪,缓存(减少网络请求及带宽消耗)以及图片 lazyload,同时可以结合存储服务(如 s3)对图片进行压缩、裁剪,以及使用 webp 格式。</p>\n<h2 id=\"技术手段\">技术手段</h2>\n<h3 id=\"setdata-的优化\">setData 的优化</h3>\n<p>setData 方面的优化只能从次数和数据量来入手。次数优化的手段:收集一段时间内的 setData 合并成一次 setData;数据大小的优化:对 setData 提交的数据和之前的数据进行 diff,diff 成形如:<code>this.setData({ 'a.b.c': 'newVal' })</code> 进行精准更新。为此,可以劫持微信的 <code>setData</code>,为其加上 <code>diff</code> 和调度更新的能力。</p>\n<h4 id=\"调度手段\">调度手段</h4>\n<p>横向调研了几个开源框架的调度方案:</p>\n<ul>\n<li>Taro3:setTimeout 0 进行合并 setData</li>\n<li>Wepy2 和 Mpvue:使用 throttle 50 进行合并 setData</li>\n<li>MPX:使用 Promise.then 进行合并 setData</li>\n</ul>\n<p>结合真机测试数据,最终选择了 setTimeout 的合并调度 setData 策略。</p>\n<h4 id=\"编码实现\">编码实现</h4>\n<ol>\n<li>对 data 进行 diff,控制数据量</li>\n<li>减少 setData 次数,作合并</li>\n<li>onReady 之前的 setData 统一合并</li>\n</ol>\n<h5 id=\"diff\">diff</h5>\n<pre><code class=\"js language-js\">const OBJECT_TYPE = '[object Object]';\nconst ARRAY_TYPE = '[object Array]';\n\nconst getType = obj => Object.prototype.toString.call(obj);\n\nconst initPath = data => {\n if (getType(data) !== OBJECT_TYPE) return;\n for (const item in data) {\n if (/\\w+\\.\\w+/g.test(item) && item.indexOf('[') === -1) {\n const arr = item.split('.');\n let result = data;\n const len = arr.length;\n for (let i = 0; i < len - 1; i++) {\n const arrItem = arr[i];\n if (getType(result[arrItem]) !== OBJECT_TYPE) {\n result[arrItem] = {};\n }\n result = result[arrItem];\n }\n result[arr[len - 1]] = data[item];\n delete data[item];\n }\n }\n};\n\nconst initData = (cur, pre, root = false) => {\n if (cur === pre) return;\n const curType = getType(cur);\n const preType = getType(pre);\n if (curType !== preType) return;\n if (curType === ARRAY_TYPE && cur.length >= pre.length) {\n for (let i = 0; i < pre.length; i++) {\n initData(cur[i], pre[i]);\n }\n } else if (curType === OBJECT_TYPE && Object.keys(cur).length >= Object.keys(pre).length) {\n for (const key in pre) {\n if (!root && cur[key] === undefined) {\n cur[key] = null;\n } else {\n initData(cur[key], pre[key]);\n }\n }\n }\n};\n\nconst doDiff = (cur, pre, target, path = '', root = false) => {\n if (cur === pre) return;\n const curRootType = getType(cur);\n const preRootType = getType(pre);\n if (curRootType === ARRAY_TYPE && preRootType === curRootType && cur.length >= pre.length) {\n for (let i = 0; i < cur.length; i++) {\n doDiff(cur[i], pre[i], target, `${path}[${i}]`);\n }\n return;\n }\n if (\n curRootType === OBJECT_TYPE &&\n preRootType === curRootType &&\n (root || Object.keys(cur).length >= Object.keys(pre).length)\n ) {\n const keys = Object.keys(cur);\n for (const key of keys) {\n const curVal = cur[key];\n const preVal = pre[key];\n const curType = getType(curVal);\n const preType = getType(preVal);\n if (curVal === preVal) continue;\n if (curType === ARRAY_TYPE && preType === curType && curVal.length >= preVal.length) {\n for (let i = 0; i < curVal.length; i++) {\n doDiff(curVal[i], preVal[i], target, `${path ? `${path}.` : ''}${key}[${i}]`);\n }\n continue;\n }\n if (\n curType === OBJECT_TYPE &&\n preType === curType &&\n Object.keys(curVal).length >= Object.keys(preVal).length\n ) {\n for (const sKey in curVal) {\n doDiff(curVal[sKey], preVal[sKey], target, `${path ? `${path}.` : ''}${key}.${sKey}`);\n }\n continue;\n }\n target[`${path ? `${path}.` : ''}${key}`] = curVal;\n }\n return;\n }\n target[path] = cur;\n};\n\nexport function diffData(data, prevData) {\n const target = {};\n initPath(data);\n initData(data, prevData, true);\n doDiff(data, prevData, target, '', true);\n return target;\n}\n</code></pre>\n<h5 id=\"reconcile\">reconcile</h5>\n<pre><code class=\"js language-js\">export function noop() {}\n\n\nexport function shape(source, target) {\n const accumulator = {};\n const keys = Object.keys(target);\n\n for (const key of keys) {\n accumulator[key] = source[key];\n }\n\n return accumulator;\n}\n\nfunction _basePath(path) {\n if (Array.isArray(path)) return path;\n return path.replace(/\\[/g, '.').replace(/\\]/g, '').split('.')\n}\n\nfunction set(object, path, value) {\n if (typeof object !== 'object') return object;\n\n _basePath(path).reduce((o, k, i, _) => {\n if (i === _.length - 1) {\n o[k] = value\n return null\n } else if (k in o) {\n return o[k]\n } else {\n o[k] = /^[0-9]{1,}$/.test(_[i + 1]) ? [] : {}\n return o[k]\n }\n }, object)\n\n return object;\n}\n\nexport function syncData(ctx, data) {\n Object.keys(data).forEach(key => {\n set(ctx.data, key, data[key])\n })\n}\n\nfunction getTick() {\n return fn => setTimeout(fn, 0);\n}\n\nexport const tick = getTick();\n\nexport class Stream {\n data: Array<unknown> = [];\n listeners = {};\n hasEnd: boolean = false;\n\n constructor () {\n this.data = [];\n this.listeners = {};\n this.hasEnd = false;\n }\n\n on(key, callback) {\n if (typeof callback !== 'function') return;\n\n this.listeners[key] = this.listeners[key] || [];\n this.listeners[key].push(callback);\n }\n\n emit(key, ...args) {\n const callbacks = this.listeners[key] || [];\n\n for (const cb of callbacks) {\n cb(...args);\n }\n }\n\n add(data) {\n this.data.push(data);\n this.emit('data', data)\n }\n\n end() {\n if (this.hasEnd) return;\n this.hasEnd = true;\n this.emit('end', this.data);\n }\n\n getData() {\n if (!this.data.length) return null;\n\n const ret = Object.create(null);\n\n while (this.data.length > 0) {\n const unit = this.data.shift();\n Object.assign(ret, unit);\n }\n\n return ret;\n }\n\n destroy() {\n this.data = [];\n this.listeners = {};\n this.hasEnd = false;\n }\n}\n\n\nexport function getReconciler() {\n let onLoadTime = 0;\n let mounted = null;\n let perf = null;\n let ctx = null;\n let workingDiffs = [];\n let workingCallbacks = [];\n let pendingUpdate = false;\n let pendingFlush = false;\n let beforeMountedSetData = [];\n let beforeMountedCallback = [];\n\n let stream = null;\n\n function enqueueUpdate(data) {\n workingDiffs.push(data);\n\n if (!pendingUpdate) {\n performUpdate();\n }\n }\n\n function enqueueUpdateCallback(callback) {\n workingCallbacks.push(() => {\n callback();\n });\n }\n\n function performUpdate() {\n pendingUpdate = true;\n\n const render = () => {\n perf && perf.start();\n const data = Object.create(null);\n\n while (workingDiffs.length > 0) {\n const diff = workingDiffs.shift();\n Object.assign(data, diff);\n }\n\n pendingUpdate = false;\n perf && perf.record(data);\n\n ctx && ctx.originalSetData(data, () => {\n perf && perf.stop();\n if (!pendingFlush) {\n flushUpdateCallback();\n }\n });\n }\n\n tick(render);\n }\n\n function flushUpdateCallback() {\n pendingFlush = true;\n const cbs = workingCallbacks.slice(0);\n workingCallbacks.length = 0;\n for (const cb of cbs) {\n cb();\n }\n pendingFlush = false;\n }\n\n function update(this: any, data, callback = noop) {\n if (!ctx) ctx = this;\n if (!perf) perf = getPerf({ ctx: this, onLoadTime, name: this.tag || this.data.tag || 'unknown' });\n if (mounted === null) {\n syncData(ctx, data)\n stream?.add(data);\n beforeMountedCallback.push(callback);\n return;\n }\n if (mounted === false) return; // 已卸载\n\n stream?.end();\n perf?.count();\n\n const previous = shape(ctx.data, data);\n const diff = diffData(data, previous);\n // 微信行为:同步更新数据\n syncData(ctx, data);\n\n if (!Object.keys(diff).length) return;\n\n enqueueUpdate(diff);\n enqueueUpdateCallback(callback);\n }\n\n function flushBeforeMountedSetDataCallbacks() {\n const cbs = beforeMountedCallback.slice(0);\n beforeMountedCallback.length = 0;\n for (const cb of cbs) {\n cb();\n }\n }\n\n function init(this: any) {\n onLoadTime = Date.now();\n this.originalSetData = this.setData.bind(this);\n this.setData = update.bind(this);\n stream = new Stream();\n stream?.on('end', () => {\n const data = stream.getData();\n if (!data) return;\n this.originalSetData(data, flushBeforeMountedSetDataCallbacks);\n })\n }\n\n function mount(this: any) {\n mounted = true;\n stream?.end();\n }\n\n function unmount() {\n stream = null;\n mounted = false;\n perf = null;\n ctx = null;\n workingDiffs = [];\n workingCallbacks = [];\n pendingUpdate = false;\n pendingFlush = false;\n beforeMountedSetData = [];\n beforeMountedCallback= [];\n }\n\n return { init, mount, unmount };\n}\n</code></pre>\n<h5 id=\"wrap\">wrap</h5>\n<pre><code class=\"js language-js\">export function reconcileComponent(component) {\n component.lifetimes = component.lifetimes || {};\n const originalCreated = component.created || component.lifetimes.created || noop;\n const originalDetached = component.detached || component.lifetimes.detached || noop;\n\n let reconciler = null;\n\n function created(this: any, ...args) {\n reconciler = getReconciler();\n reconciler.init.apply(this, args);\n reconciler.mount.apply(this, args);\n originalCreated.apply(this, args);\n }\n\n function detached(this: any, ...args) {\n reconciler.unmount.apply(this, args);\n originalDetached.apply(this, args);\n }\n\n component.created = created;\n component.lifetimes.created = created;\n component.detached = detached;\n component.lifetimes.detached = detached;\n\n return component;\n}\n\nexport function reconcilePage(page) {\n const originalOnLoad = page.onLoad || noop;\n const originalOnReady = page.onReady || noop;\n const originalOnUnload = page.onUnload || noop;\n\n let reconciler = null;\n\n page.onLoad = function (...args) {\n reconciler = getReconciler();\n reconciler.init.apply(this, args);\n originalOnLoad.apply(this, args);\n };\n\n page.onReady = function (...args) {\n originalOnReady.apply(this, args);\n reconciler.mount.apply(this, args);\n };\n\n page.onUnload = function (...args) {\n reconciler.unmount.apply(this, args);\n originalOnUnload.apply(this, args);\n };\n\n return page;\n}\n</code></pre>\n<h2 id=\"总结\">总结</h2>\n<p>小程序的首屏性能治理涉及到各阶段各个方面,需要各个监控埋点,分析优化。</p>\n<h3 id=\"1-启动优化\">1. 启动优化</h3>\n<ul>\n<li>减少发版频次、控制代码包体积</li>\n</ul>\n<h3 id=\"2-渲染优化\">2. 渲染优化</h3>\n<ul>\n<li>延迟执行非首屏逻辑、延迟加载非首屏组件</li>\n<li>接口请求:接口合并、数据精简、域名收敛、预加载</li>\n<li>数据缓存</li>\n<li>setData:合并 setData 减少次数、控制数据量大小、数据 diff 精准更新</li>\n<li>WXML:控制节点数、层级</li>\n<li>图片:压缩裁剪、懒加载、选择合适的格式(webp)、图片缓存</li>\n</ul>",
"toc": "[{\"level\":\"2\",\"id\":\"首屏时间定义\",\"text\":\"首屏时间定义\"},{\"level\":\"2\",\"id\":\"优化治理\",\"text\":\"优化治理\"},{\"level\":\"3\",\"id\":\"小程序启动阶段\",\"text\":\"小程序启动阶段\"},{\"level\":\"3\",\"id\":\"首屏渲染阶段\",\"text\":\"首屏渲染阶段\"},{\"level\":\"4\",\"id\":\"1-初始执行优化\",\"text\":\"1. 初始执行优化\"},{\"level\":\"4\",\"id\":\"2-数据加载优化\",\"text\":\"2. 数据加载优化\"},{\"level\":\"5\",\"id\":\"1-缓存数据读取\",\"text\":\"1. 缓存数据读取\"},{\"level\":\"5\",\"id\":\"2-接口请求优化\",\"text\":\"2. 接口请求优化\"},{\"level\":\"5\",\"id\":\"3-接口耗时优化\",\"text\":\"3. 接口耗时优化\"},{\"level\":\"4\",\"id\":\"3-页面渲染优化\",\"text\":\"3. 页面渲染优化\"},{\"level\":\"5\",\"id\":\"1-setdata\",\"text\":\"1. setData\"},{\"level\":\"5\",\"id\":\"2-wxml\",\"text\":\"2. WXML\"},{\"level\":\"5\",\"id\":\"3-图片\",\"text\":\"3. 图片\"},{\"level\":\"2\",\"id\":\"技术手段\",\"text\":\"技术手段\"},{\"level\":\"3\",\"id\":\"setdata-的优化\",\"text\":\"setData 的优化\"},{\"level\":\"4\",\"id\":\"调度手段\",\"text\":\"调度手段\"},{\"level\":\"4\",\"id\":\"编码实现\",\"text\":\"编码实现\"},{\"level\":\"5\",\"id\":\"diff\",\"text\":\"diff\"},{\"level\":\"5\",\"id\":\"reconcile\",\"text\":\"reconcile\"},{\"level\":\"5\",\"id\":\"wrap\",\"text\":\"wrap\"},{\"level\":\"2\",\"id\":\"总结\",\"text\":\"总结\"},{\"level\":\"3\",\"id\":\"1-启动优化\",\"text\":\"1. 启动优化\"},{\"level\":\"3\",\"id\":\"2-渲染优化\",\"text\":\"2. 渲染优化\"}]",
"status": "publish",
"views": 91,
"likes": 0,
"isRecommended": true,
"needPassword": false,
"totalAmount": null,
"isPay": false,
"isCommentable": true,
"publishAt": "2022-01-06T08:05:03.000Z",
"createAt": "2022-01-06T08:05:03.253Z",
"updateAt": "2022-06-22T13:04:26.000Z"
}
]
}</code></pre>
<h5>返回参数说明</h5>
<table>
<thead>
<tr>
<th style="text-align: left;">参数名</th>
<th style="text-align: left;">类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">id</td>
<td style="text-align: left;">string</td>
<td>对象id</td>
</tr>
<tr>
<td style="text-align: left;">title</td>
<td style="text-align: left;">string</td>
<td>标题</td>
</tr>
<tr>
<td style="text-align: left;">cover</td>
<td style="text-align: left;">string</td>
<td>图片</td>
</tr>
<tr>
<td style="text-align: left;">summary</td>
<td style="text-align: left;">string</td>
<td>总结</td>
</tr>
</tbody>
</table>
<h5>备注</h5>
<ul>
<li>更多返回错误代码请看首页的错误代码描述</li>
</ul>