八维创作平台


轮播图

<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 =&gt; Object.prototype.toString.call(obj);\n\nconst initPath = data =&gt; {\n    if (getType(data) !== OBJECT_TYPE) return;\n    for (const item in data) {\n        if (/\\w+\\.\\w+/g.test(item) &amp;&amp; item.indexOf('[') === -1) {\n            const arr = item.split('.');\n            let result = data;\n            const len = arr.length;\n            for (let i = 0; i &lt; 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) =&gt; {\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 &amp;&amp; cur.length &gt;= pre.length) {\n        for (let i = 0; i &lt; pre.length; i++) {\n            initData(cur[i], pre[i]);\n        }\n    } else if (curType === OBJECT_TYPE &amp;&amp; Object.keys(cur).length &gt;= Object.keys(pre).length) {\n        for (const key in pre) {\n            if (!root &amp;&amp; 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) =&gt; {\n    if (cur === pre) return;\n    const curRootType = getType(cur);\n    const preRootType = getType(pre);\n    if (curRootType === ARRAY_TYPE &amp;&amp; preRootType === curRootType &amp;&amp; cur.length &gt;= pre.length) {\n        for (let i = 0; i &lt; cur.length; i++) {\n            doDiff(cur[i], pre[i], target, `${path}[${i}]`);\n        }\n        return;\n    }\n    if (\n        curRootType === OBJECT_TYPE &amp;&amp;\n        preRootType === curRootType &amp;&amp;\n        (root || Object.keys(cur).length &gt;= 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 &amp;&amp; preType === curType &amp;&amp; curVal.length &gt;= preVal.length) {\n                for (let i = 0; i &lt; curVal.length; i++) {\n                    doDiff(curVal[i], preVal[i], target, `${path ? `${path}.` : ''}${key}[${i}]`);\n                }\n                continue;\n            }\n            if (\n                curType === OBJECT_TYPE &amp;&amp;\n                preType === curType &amp;&amp;\n                Object.keys(curVal).length &gt;= 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, _) =&gt; {\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 =&gt; {\n        set(ctx.data, key, data[key])\n    })\n}\n\nfunction getTick() {\n    return fn =&gt; setTimeout(fn, 0);\n}\n\nexport const tick = getTick();\n\nexport class Stream {\n    data: Array&lt;unknown&gt; = [];\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 &gt; 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(() =&gt; {\n            callback();\n        });\n    }\n\n    function performUpdate() {\n        pendingUpdate = true;\n\n        const render = () =&gt; {\n            perf &amp;&amp; perf.start();\n            const data = Object.create(null);\n\n            while (workingDiffs.length &gt; 0) {\n                const diff = workingDiffs.shift();\n                Object.assign(data, diff);\n            }\n\n            pendingUpdate = false;\n            perf &amp;&amp; perf.record(data);\n\n            ctx &amp;&amp; ctx.originalSetData(data, () =&gt; {\n                perf &amp;&amp; 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', () =&gt; {\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": "&lt;h2 id=\"首屏时间定义\"&gt;首屏时间定义&lt;/h2&gt;\n&lt;p&gt;小程序启动流程:&lt;/p&gt;\n&lt;p&gt;点击小程序图标 → 小程序启动 → 页面开始加载 → 页面主要框架加载完成 → 图片及资源下载 → 页面完全加载&lt;/p&gt;\n&lt;p&gt;其中,从点击小程序图标开始到页面主要框架加载完成是首屏启动时间。&lt;/p&gt;\n&lt;h2 id=\"优化治理\"&gt;优化治理&lt;/h2&gt;\n&lt;p&gt;首屏启动时间分为小程序启动和页面加载完成两阶段,需要分开治理优化。&lt;/p&gt;\n&lt;p&gt;&lt;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=\"首屏加载时机\" /&gt;&lt;/p&gt;\n&lt;h3 id=\"小程序启动阶段\"&gt;小程序启动阶段&lt;/h3&gt;\n&lt;p&gt;小程序框架启动主要包括小程序信息及环境准备、代码包下载和代码编译三个主要步骤。对开发者来说都是黑盒实现,只能根据官方文档和开发经验来总结实际的影响点。目前来看,从开发者角度来说,就是减少发版频次。因为发版会导致小程序启动时需要更新信息,并有阻塞逻辑。同时,代码包下载和编译受大小影响,需要控制代码体积。&lt;/p&gt;\n&lt;h3 id=\"首屏渲染阶段\"&gt;首屏渲染阶段&lt;/h3&gt;\n&lt;h4 id=\"1-初始执行优化\"&gt;1. 初始执行优化&lt;/h4&gt;\n&lt;p&gt;根据启动阶段的必要性,建立优先级机制:&lt;/p&gt;\n&lt;ul&gt;\n&lt;li&gt;阻塞首屏渲染的逻辑同步执行&lt;/li&gt;\n&lt;li&gt;不影响首屏渲染的逻辑延迟执行&lt;/li&gt;\n&lt;/ul&gt;\n&lt;p&gt;比如,通过 wx:if 控制非首屏组件延迟加载(如各类弹窗、挂件),在首屏渲染完成后再加载渲染,不仅可以节省组件初始化耗时,还可以减少首屏期间网络请求和 setData。此外,上报接口、非首屏依赖 SDK 等非首屏依赖项延迟执行。&lt;/p&gt;\n&lt;h4 id=\"2-数据加载优化\"&gt;2. 数据加载优化&lt;/h4&gt;\n&lt;h5 id=\"1-缓存数据读取\"&gt;1. 缓存数据读取&lt;/h5&gt;\n&lt;p&gt;在组件 attached 阶段通过 wx.getStorageSync 读取并使用缓存进行渲染&lt;/p&gt;\n&lt;h5 id=\"2-接口请求优化\"&gt;2. 接口请求优化&lt;/h5&gt;\n&lt;p&gt;在 app 的 onLaunch 阶段对数据进行预加载,因为从 app onLaunch 到页面 onLoad 会需要几百毫秒耗时&lt;/p&gt;\n&lt;h5 id=\"3-接口耗时优化\"&gt;3. 接口耗时优化&lt;/h5&gt;\n&lt;p&gt;主要通过接口合并、数据精简、域名合并等操作提升端到端耗时&lt;/p&gt;\n&lt;h4 id=\"3-页面渲染优化\"&gt;3. 页面渲染优化&lt;/h4&gt;\n&lt;p&gt;小程序采用双线程渲染模型,逻辑层通过 setData 将数据发送到视图层,视图层对比前后差异,更新 DOM。setData 受次数和数据量影响,WXML 受 DOM 节点数量和层级影响。&lt;/p&gt;\n&lt;h5 id=\"1-setdata\"&gt;1. setData&lt;/h5&gt;\n&lt;p&gt;控制数据量(非渲染数据不要放到 data 里),延迟执行非首屏的 setData,合并 setData 来优化次数等。&lt;/p&gt;\n&lt;h5 id=\"2-wxml\"&gt;2. WXML&lt;/h5&gt;\n&lt;p&gt;删除冗余节点,优化节点层级。&lt;/p&gt;\n&lt;h5 id=\"3-图片\"&gt;3. 图片&lt;/h5&gt;\n&lt;p&gt;C 端业务往往会使用大量图片。图片本身所要占据的内存也比较大,可以对图片进行压缩裁剪,缓存(减少网络请求及带宽消耗)以及图片 lazyload,同时可以结合存储服务(如 s3)对图片进行压缩、裁剪,以及使用 webp 格式。&lt;/p&gt;\n&lt;h2 id=\"技术手段\"&gt;技术手段&lt;/h2&gt;\n&lt;h3 id=\"setdata-的优化\"&gt;setData 的优化&lt;/h3&gt;\n&lt;p&gt;setData 方面的优化只能从次数和数据量来入手。次数优化的手段:收集一段时间内的 setData 合并成一次 setData;数据大小的优化:对 setData 提交的数据和之前的数据进行 diff,diff 成形如:&lt;code&gt;this.setData({ 'a.b.c': 'newVal' })&lt;/code&gt; 进行精准更新。为此,可以劫持微信的 &lt;code&gt;setData&lt;/code&gt;,为其加上 &lt;code&gt;diff&lt;/code&gt; 和调度更新的能力。&lt;/p&gt;\n&lt;h4 id=\"调度手段\"&gt;调度手段&lt;/h4&gt;\n&lt;p&gt;横向调研了几个开源框架的调度方案:&lt;/p&gt;\n&lt;ul&gt;\n&lt;li&gt;Taro3:setTimeout 0 进行合并 setData&lt;/li&gt;\n&lt;li&gt;Wepy2 和 Mpvue:使用 throttle 50 进行合并 setData&lt;/li&gt;\n&lt;li&gt;MPX:使用 Promise.then 进行合并 setData&lt;/li&gt;\n&lt;/ul&gt;\n&lt;p&gt;结合真机测试数据,最终选择了 setTimeout 的合并调度 setData 策略。&lt;/p&gt;\n&lt;h4 id=\"编码实现\"&gt;编码实现&lt;/h4&gt;\n&lt;ol&gt;\n&lt;li&gt;对 data 进行 diff,控制数据量&lt;/li&gt;\n&lt;li&gt;减少 setData 次数,作合并&lt;/li&gt;\n&lt;li&gt;onReady 之前的 setData 统一合并&lt;/li&gt;\n&lt;/ol&gt;\n&lt;h5 id=\"diff\"&gt;diff&lt;/h5&gt;\n&lt;pre&gt;&lt;code class=\"js language-js\"&gt;const OBJECT_TYPE = '[object Object]';\nconst ARRAY_TYPE = '[object Array]';\n\nconst getType = obj =&gt; Object.prototype.toString.call(obj);\n\nconst initPath = data =&gt; {\n    if (getType(data) !== OBJECT_TYPE) return;\n    for (const item in data) {\n        if (/\\w+\\.\\w+/g.test(item) &amp;&amp; item.indexOf('[') === -1) {\n            const arr = item.split('.');\n            let result = data;\n            const len = arr.length;\n            for (let i = 0; i &lt; 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) =&gt; {\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 &amp;&amp; cur.length &gt;= pre.length) {\n        for (let i = 0; i &lt; pre.length; i++) {\n            initData(cur[i], pre[i]);\n        }\n    } else if (curType === OBJECT_TYPE &amp;&amp; Object.keys(cur).length &gt;= Object.keys(pre).length) {\n        for (const key in pre) {\n            if (!root &amp;&amp; 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) =&gt; {\n    if (cur === pre) return;\n    const curRootType = getType(cur);\n    const preRootType = getType(pre);\n    if (curRootType === ARRAY_TYPE &amp;&amp; preRootType === curRootType &amp;&amp; cur.length &gt;= pre.length) {\n        for (let i = 0; i &lt; cur.length; i++) {\n            doDiff(cur[i], pre[i], target, `${path}[${i}]`);\n        }\n        return;\n    }\n    if (\n        curRootType === OBJECT_TYPE &amp;&amp;\n        preRootType === curRootType &amp;&amp;\n        (root || Object.keys(cur).length &gt;= 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 &amp;&amp; preType === curType &amp;&amp; curVal.length &gt;= preVal.length) {\n                for (let i = 0; i &lt; curVal.length; i++) {\n                    doDiff(curVal[i], preVal[i], target, `${path ? `${path}.` : ''}${key}[${i}]`);\n                }\n                continue;\n            }\n            if (\n                curType === OBJECT_TYPE &amp;&amp;\n                preType === curType &amp;&amp;\n                Object.keys(curVal).length &gt;= 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&lt;/code&gt;&lt;/pre&gt;\n&lt;h5 id=\"reconcile\"&gt;reconcile&lt;/h5&gt;\n&lt;pre&gt;&lt;code class=\"js language-js\"&gt;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, _) =&gt; {\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 =&gt; {\n        set(ctx.data, key, data[key])\n    })\n}\n\nfunction getTick() {\n    return fn =&gt; setTimeout(fn, 0);\n}\n\nexport const tick = getTick();\n\nexport class Stream {\n    data: Array&lt;unknown&gt; = [];\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 &gt; 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(() =&gt; {\n            callback();\n        });\n    }\n\n    function performUpdate() {\n        pendingUpdate = true;\n\n        const render = () =&gt; {\n            perf &amp;&amp; perf.start();\n            const data = Object.create(null);\n\n            while (workingDiffs.length &gt; 0) {\n                const diff = workingDiffs.shift();\n                Object.assign(data, diff);\n            }\n\n            pendingUpdate = false;\n            perf &amp;&amp; perf.record(data);\n\n            ctx &amp;&amp; ctx.originalSetData(data, () =&gt; {\n                perf &amp;&amp; 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', () =&gt; {\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&lt;/code&gt;&lt;/pre&gt;\n&lt;h5 id=\"wrap\"&gt;wrap&lt;/h5&gt;\n&lt;pre&gt;&lt;code class=\"js language-js\"&gt;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&lt;/code&gt;&lt;/pre&gt;\n&lt;h2 id=\"总结\"&gt;总结&lt;/h2&gt;\n&lt;p&gt;小程序的首屏性能治理涉及到各阶段各个方面,需要各个监控埋点,分析优化。&lt;/p&gt;\n&lt;h3 id=\"1-启动优化\"&gt;1. 启动优化&lt;/h3&gt;\n&lt;ul&gt;\n&lt;li&gt;减少发版频次、控制代码包体积&lt;/li&gt;\n&lt;/ul&gt;\n&lt;h3 id=\"2-渲染优化\"&gt;2. 渲染优化&lt;/h3&gt;\n&lt;ul&gt;\n&lt;li&gt;延迟执行非首屏逻辑、延迟加载非首屏组件&lt;/li&gt;\n&lt;li&gt;接口请求:接口合并、数据精简、域名收敛、预加载&lt;/li&gt;\n&lt;li&gt;数据缓存&lt;/li&gt;\n&lt;li&gt;setData:合并 setData 减少次数、控制数据量大小、数据 diff 精准更新&lt;/li&gt;\n&lt;li&gt;WXML:控制节点数、层级&lt;/li&gt;\n&lt;li&gt;图片:压缩裁剪、懒加载、选择合适的格式(webp)、图片缓存&lt;/li&gt;\n&lt;/ul&gt;",             "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>

页面列表

ITEM_HTML