1. Taro 助力京喜拼拼項(xiàng)目性能體驗(yàn)優(yōu)化

        共 7553字,需瀏覽 16分鐘

         ·

        2021-02-23 10:51



        背景

        2020 年是社區(qū)團(tuán)購風(fēng)起云涌的一年,互聯(lián)網(wǎng)大廠紛紛抓緊一分一秒跑步進(jìn)場。“京喜拼拼”(微信搜京喜拼拼)是京東旗下的社區(qū)團(tuán)購平臺(tái),依托京東供應(yīng)鏈體系,精選低價(jià)好貨,為社區(qū)用戶提供次日達(dá)等優(yōu)質(zhì)服務(wù)。

        京喜拼拼團(tuán)隊(duì)技術(shù)選型使用 Taro 以便于實(shí)現(xiàn)多端需求,因此 Taro 團(tuán)隊(duì)有幸參與到 “京喜拼拼” 小程序的性能體驗(yàn)優(yōu)化工作。

        全面體驗(yàn) - 梳理 Taro 寫法最佳實(shí)踐

        我們?nèi)骟w驗(yàn)后和熟悉業(yè)務(wù)代碼后梳理出一系列 Taro3 寫法的最佳實(shí)踐:

        1. 性能相關(guān)

        對小程序的性能影響較大的有兩個(gè)因素,分別是 setData數(shù)據(jù)量和單位時(shí)間 setData 函數(shù)的調(diào)用次數(shù)。

        當(dāng)遇到性能問題時(shí),在項(xiàng)目中打印 setData 的數(shù)據(jù)將非常有利于幫助定位問題。開發(fā)者可以通過進(jìn)入 Taro 項(xiàng)目的 dist/taro.js 文件,搜索定位 .setData 的調(diào)用位置,然后對數(shù)據(jù)進(jìn)行打印。

        在 Taro 中,會(huì)對 setDatabatch 捆綁更新操作,因此更多時(shí)候只需要考慮 setData 的數(shù)據(jù)量大小問題。

        以下是我們梳理的開發(fā)者需要注意的寫法問題,有一些問題需要開發(fā)者手動(dòng)調(diào)整,一些問題 Taro 可以幫助自動(dòng)化規(guī)避:

        1.1. 刪除樓層節(jié)點(diǎn)需要謹(jǐn)慎處理

        假設(shè)有一種這樣一種結(jié)構(gòu):



        <Slider />

        <Goods />

        {isShowModal && <Modal />}
        </View>

        Taro3 目前對節(jié)點(diǎn)的刪除處理是有缺陷的。當(dāng) isShowModaltrue 變?yōu)?false 時(shí),模態(tài)彈窗會(huì)從消失。此時(shí) Modal 組件的兄弟節(jié)點(diǎn)都會(huì)被更新,setData 的數(shù)據(jù)是 Slider + Goods 組件的 DOM 節(jié)點(diǎn)信息。

        一般情況下,影響不會(huì)太大,開發(fā)者無須由此產(chǎn)生心智負(fù)擔(dān)。但倘若待刪除節(jié)點(diǎn)的兄弟節(jié)點(diǎn)的 DOM 結(jié)構(gòu)非常復(fù)雜,如一個(gè)個(gè)樓層組件,刪除操作的副作用會(huì)導(dǎo)致 setData 數(shù)據(jù)量較大,從而影響性能。

        解決辦法:

        目前我們可以這樣優(yōu)化,隔離刪除操作:



        <Slider />

        <Goods />

        <View>
        {isShowModal && <Modal />}
        View>

        </View>

        我們正在對刪除節(jié)點(diǎn)的算法進(jìn)行優(yōu)化,完全規(guī)避這種不必要的 setData,于 v3.1 推出。

        1.2. 基礎(chǔ)組件的屬性盡量保持引用

        假設(shè)基礎(chǔ)組件(如 ViewInput 等)的屬性值為非基本類型時(shí),盡量保持對象的引用。

        假設(shè)有以下寫法:

        <Map
        latitude={22.53332}
        longitude={113.93041}
        markers={[{
        latitude: 22.53332,
        longitude: 113.93041
        }]}
        />

        每次渲染時(shí),React 會(huì)對基礎(chǔ)組件的屬性做淺對比,這時(shí)發(fā)現(xiàn) markers 的引用不同,就會(huì)去更新組件屬性。最后導(dǎo)致 setData 次數(shù)增多、setData 數(shù)據(jù)量增大。

        解決辦法:

        可以通過 state、閉包等手段保持對象的引用:

        <Map
        latitude={22.53332}
        longitude={113.93041}
        markers={this.state.markers}
        />

        1.3. 小程序基礎(chǔ)組件盡量不要掛載額外屬性

        基礎(chǔ)組件(如 View、Input 等)如若設(shè)置了非標(biāo)準(zhǔn)的屬性,目前這些額外屬性會(huì)被一并進(jìn)行 setData,而實(shí)際上小程序并不會(huì)理會(huì)這些屬性,所以 setData 的這部分?jǐn)?shù)據(jù)是冗余的。

        例如 Text 組件的標(biāo)準(zhǔn)屬性有 selectable、user-selectspace、decode 四個(gè),如果我們?yōu)樗O(shè)置一個(gè)額外屬性 something,那么這個(gè)額外的屬性也是會(huì)被 setData。

        'extra' />

        Taro v3.1 將會(huì)自動(dòng)過濾這些額外屬性,屆時(shí)這個(gè)限制將不再存在。

        2. 體驗(yàn)相關(guān)

        2.1. 滾動(dòng)穿透

        在小程序開發(fā)中,滑動(dòng)蒙層、彈窗等覆蓋式元素時(shí),滑動(dòng)事件會(huì)冒泡到頁面,使頁面元素也跟著滑動(dòng),往往我們的解決辦法是設(shè)置 catchTouchMove 從而阻止冒泡。

        由于 Taro3 事件機(jī)制[1]的限制,小程序事件都以 bind 的形式進(jìn)行綁定。所以和 Taro1、Taro2 不同,調(diào)用 e.stopPropagation() 并不能阻止?jié)L動(dòng)穿透。

        解決辦法:
        1. 使用樣式解決(推薦)

        給需要禁用滾動(dòng)的組件寫一個(gè)樣式,類似于:

        {
        overflow:hidden;
        height: 100vh;
        }
        1. catchMove

        對于 Map 等極個(gè)別組件,使用樣式固定寬高也無法阻止?jié)L動(dòng),因?yàn)檫@些組件本身就具有滾動(dòng)的能力。所以第一種辦法處理不了冒泡到 Map 組件上的滾動(dòng)事件。

        這時(shí)候可以為 View 組件增加 catchMove 屬性:

        // 這個(gè) View 組件會(huì)綁定 catchtouchmove 事件而不是 bindtouchmove

        2.2. 跳轉(zhuǎn)預(yù)加載

        在小程序中,從調(diào)用 Taro.navigateTo 等跳轉(zhuǎn)類 API,到新頁面觸發(fā) onLoad 會(huì)有一定延時(shí)。因此類如網(wǎng)絡(luò)請求等操作可以提前到調(diào)用跳轉(zhuǎn) API 之前。

        熟悉 Taro 的同學(xué)可能會(huì)想起 Taro1、Taro2 中的 componentWillPreload 鉤子。但 Taro3 不再提供這個(gè)鉤子,開發(fā)者可以使用 Taro.preload() 方法實(shí)現(xiàn)跳轉(zhuǎn)預(yù)加載:

        // pages/index.js
        Taro.preload(fetchSomething())
        Taro.navigateTo({ url: '/pages/detail' })
        // pages/detail.js
        console.log(getCurrentInstance().preloadData)

        2.3. 建議把 Taro.getCurrentInstance() 的結(jié)果保存下來

        開發(fā)中我們常常會(huì)調(diào)用 Taro.getCurrentInstance() 獲取小程序的 app、page 對象、路由參數(shù)等數(shù)據(jù)。但頻繁調(diào)用它可能會(huì)導(dǎo)致問題。因此推薦把 Taro.getCurrentInstance() 的結(jié)果在組件中保存起來,之后直接使用:

        class Index extends React.Component {
        inst = Taro.getCurrentInstance()

        componentDidMount () {
        console.log(this.inst)
        }
        }

        難啃的骨頭 - 購物車頁

        我們在低端機(jī)上受到了性能的困擾,尤其是在購物車頁面卡頓最為明顯。通過分析頁面結(jié)構(gòu)和反思 Taro 底層實(shí)現(xiàn),我們主要采取了兩項(xiàng)優(yōu)化措施,提升了低端機(jī)型滾動(dòng)的流暢度,同時(shí)將點(diǎn)擊延時(shí)從 1.5s 降到 300ms。

        1. 長列表優(yōu)化

        在 Taro3 中,我們新增了虛擬列表這樣一個(gè)特殊的組件,幫助很多社區(qū)的開發(fā)者對超長列表進(jìn)行優(yōu)化,相信很多同學(xué)對虛擬列表的實(shí)現(xiàn)原理、包括下圖都已經(jīng)是很熟悉了,但購物車頁卻給我們提出了新的需求。

        虛擬列表

        1.1 不限制高度

        虛擬列表根據(jù) itemSize 來計(jì)算每個(gè)節(jié)點(diǎn)的位置,如果節(jié)點(diǎn)的寬高不確定,在每個(gè)節(jié)點(diǎn)至少加載完成一次之前,我們很難去判斷列表的真實(shí)尺寸。這也是為什么在虛擬列表的早期版本中我們并沒有支持這樣的特性,而是選擇固定了每個(gè)節(jié)點(diǎn)的高度,避免讓開發(fā)者使用虛擬列表時(shí)增加心智負(fù)擔(dān)。

        不過這個(gè)需求也并非不能完成,簡單地調(diào)整虛擬列表實(shí)現(xiàn)和使用的邏輯,我們就可以輕松實(shí)現(xiàn)這個(gè)特性。

        import VirtualList from `@tarojs/components/virtual-list`

        function buildData (offset = 0) {
        return Array(100).fill(0).map((_, i) => i + offset);
        }

        - const Row = React.memo(({ index, style, data }) => {
        + const Row = React.memo(({ id, index, style, data }) => {
        return (
        -
        +
        Row {index}

        );
        })

        export default class Index extends Component {
        state = {
        data: buildData(0),
        }

        render() {
        const { data } = this.state
        const dataLen = data.length
        return (
        height={500} // 列表的高度
        width='100%' // 列表的寬度
        itemData={data} // 渲染列表的數(shù)據(jù)
        itemCount={dataLen} // 渲染列表的長度
        itemSize={100} // 列表單項(xiàng)的高度
        + unlimitedSize={true} // 解開列表節(jié)點(diǎn)大小限制
        >
        {Row} // 列表單項(xiàng)組件,這里只能傳入一個(gè)組件

        );
        }
        }

        可以看到,我們在新增了 id 傳入來幫助獲取每個(gè)節(jié)點(diǎn)在首次加載之后讀取它的真實(shí)大小,得益于 Taro 跨平臺(tái)的優(yōu)勢,這是重構(gòu)虛擬列表組件中最簡單的一步,有了這個(gè)基礎(chǔ),我們就可以將節(jié)點(diǎn)的實(shí)際大小和它們的位置信息關(guān)聯(lián)到一起,讓列表自己調(diào)整每個(gè)節(jié)點(diǎn)的位置,并呈現(xiàn)給用戶。

        而對于開發(fā)者,如果想要使用這個(gè)模式,只需要傳入 unlimitedSize 就可以讓虛擬列表解開高度限制。當(dāng)然這并不意味著在使用虛擬列表時(shí)可以不需要傳入節(jié)點(diǎn)大小, itemSize 在這個(gè)模式下將作為初始值輔助列表中每個(gè)節(jié)點(diǎn)位置信息的計(jì)算。

        如果itemSize和實(shí)際大小差別過大,在超長列表中會(huì)有較明顯的問題,大家需要小心使用哦~

        1.2 列表底部

        列表的底部區(qū)域可以幫助我們便捷地完成信息的展示,比如上拉加載等,對于虛擬列表也是如此。

        return (
        height={500} // 列表的高度
        width='100%' // 列表的寬度
        itemData={data} // 渲染列表的數(shù)據(jù)
        itemCount={dataLen} // 渲染列表的長度
        itemSize={100} // 列表單項(xiàng)的高度
        + renderBottom={我就是底線}
        >
        {Row} // 列表單項(xiàng)組件,這里只能傳入一個(gè)組件

        );

        當(dāng)然也有同學(xué)會(huì)注意到,在 虛擬列表 文檔中是通過 scrollOffset > ((dataLen - 5) * itemSize + 100) 這樣的方法來判斷是否觸底,這是因?yàn)槲覀儾]有在 VirtualList 中返回滾動(dòng)的詳細(xì)信息,這次我們也返回相關(guān)的數(shù)據(jù),幫助大家更好地使用虛擬列表。

        interface VirtualListEvent {
        /** 滾動(dòng)方向,可能值為 forward 往前, backward 往后。*/
        scrollDirection: 'forward' | 'backward'
        /** 滾動(dòng)距離 */
        scrollOffset: number
        /** 當(dāng)滾動(dòng)是由 scrollTo() 或 scrollToItem() 調(diào)用時(shí)返回 true,否則返回 false */
        scrollUpdateWasRequested: boolean
        /** 當(dāng)前只有 React 支持 */
        + detail?: {
        + scrollLeft: number
        + scrollTop: number
        + scrollHeight: number
        + scrollWidth: number
        + clientWidth: number
        + clientHeight: number
        + }
        }

        1.3 性能優(yōu)化

        在虛擬列表中,無論是使用那種布局方式,都會(huì)造成頁面的回流,所以不論選擇哪一種對于瀏覽器內(nèi)核渲染頁面而言并沒有很大的區(qū)別。但是如果使用 relative,對于列表來說,需要調(diào)整的節(jié)點(diǎn)樣式要少得多。所以我們在新的虛擬列表中也支持了這樣的定位模式,供開發(fā)者自由選擇。對于低端機(jī)型來說,在我們完成整體的渲染性能優(yōu)化之前,relative 模式已經(jīng)能夠讓虛擬列表在低端機(jī)型上擁有不錯(cuò)的體驗(yàn)。

        2. 渲染性能優(yōu)化

        Taro3 使用小程序的 template 進(jìn)行渲染,一般情況下并不會(huì)使用原生自定義組件。這會(huì)導(dǎo)致一個(gè)問題,所有的 setData 更新都是由頁面對象調(diào)用,如果我們的頁面結(jié)構(gòu)比較復(fù)雜,更新的性能就會(huì)下降。

        層級(jí)過深時(shí) setData 的數(shù)據(jù)結(jié)構(gòu):

        page.setData({
        "root.cn.[0].cn.[0].cn.[0].cn.[0].markers": []
        })

        針對這個(gè)問題,主要的思路是借用小程序的原生自定義組件,以達(dá)到局部更新的效果,從而提升更新性能。

        期望的 setData 數(shù)據(jù)結(jié)構(gòu):

        component.setData({
        "cn.[0].cn.[0].markers": []
        })

        開發(fā)者有兩種辦法可以實(shí)現(xiàn)這個(gè)優(yōu)化:

        2.1 全局配置項(xiàng) baseLevel

        對于不支持模板遞歸的小程序(微信、QQ、京東小程序),在 DOM 層級(jí)達(dá)到一定數(shù)量后,Taro 會(huì)使用原生自定義組件協(xié)助遞歸。

        簡單理解就是 DOM 結(jié)構(gòu)超過 N 層后,會(huì)使用原生自定義組件進(jìn)行渲染。N 默認(rèn)是 16 層,可以通過修改配置項(xiàng) baseLevel[2] 修改 N。

        baseLevel 設(shè)置為 8 甚至 4 層,能非常有效地提升更新時(shí)的性能。但是設(shè)置是全局性的,會(huì)帶來若干問題:

        1. flex 布局在跨原生自定義組件時(shí)會(huì)失效,這是影響最大的一個(gè)問題。
        2. SelectorQuery.select 方法的跨自定義組件的后代選擇器[3]寫法需要增加 >>>.the-ancestor >>> .the-descendant

        2.2 CustomWrapper 組件

        為了解決全局配置不靈活的問題,我們增加了一個(gè)基礎(chǔ)組件 CustomWrapper。它的作用是創(chuàng)建一個(gè)原生自定義組件,對后代節(jié)點(diǎn)的 setData 將由此自定義組件進(jìn)行調(diào)用,達(dá)到局部更新的效果。

        開發(fā)者可以使用它去包裹遇到更新性能問題的模塊,提升更新時(shí)的性能。因?yàn)?CustomWrapper 組件需要手動(dòng)使用,開發(fā)者能夠清楚“這層使用了自定義組件,需要避免自定義組件的兩個(gè)問題”。

        例子

        <GoodsList>
        <Item />
        <Item />
        // ...
        GoodsList>

        </CustomWrapper>

        十全十美 - 體驗(yàn)評(píng)分平均 95+

        把開發(fā)者工具的體驗(yàn)評(píng)分給拉滿,這里我們遇到了一個(gè)問題,開發(fā)者工具會(huì)識(shí)別所有綁定了點(diǎn)擊事件的組件,如果組件的面積過小則提示點(diǎn)擊區(qū)域過小,會(huì)影響“體驗(yàn)項(xiàng)”的評(píng)分。但是 Taro3 默認(rèn)會(huì)為組件綁定上所有屬性和事件[4]。這樣會(huì)“誤傷”一些組件,它們雖然面積很小,實(shí)際上并沒有點(diǎn)擊功能,但因?yàn)?Taro3 默認(rèn)綁定的事件,被開發(fā)者工具認(rèn)為點(diǎn)擊區(qū)域過小,從而拉低體驗(yàn)評(píng)分。

        Text 組件的模板,默認(rèn)綁定了所有屬性和事件:

        <template name="tmpl_0_text">
        <text
        selectable="{{...}}"
        space="{{...}}"
        decode="{{...}}"
        user-select="{{...}}"
        style="{{...}}"
        class="{{...}}"
        id="{{...}}"
        bindtap="..."
        >

        ...
        text>
        template>

        因此我們?yōu)?View、TextImage 組件各設(shè)立了一個(gè) static 模板,當(dāng)檢測到組件沒有綁定事件時(shí),則使用 static 模板,避免被“誤傷”。

        另一方面,這一舉動(dòng)也能減少小程序 DOM 綁定的事件,對性能稍有提升,而且減少了屬性讓開發(fā)者工具的 xml 面板在調(diào)試時(shí)更加清晰。但這一方案也存在瑕疵,會(huì)導(dǎo)致編譯后的 base.wxml 體積略微增大,和性能權(quán)衡來看,這仍然是值得的。

        Text 組件的 static 模板,沒有綁定事件:

        <template name="tmpl_0_static-text">
        <text
        selectable="{{...}}"
        space="{{...}}"
        decode="{{...}}"
        user-select="{{...}}"
        style="{{...}}"
        class="{{...}}"
        id="{{...}}"
        >

        ...
        text>
        template>
        優(yōu)化后的購物車頁體驗(yàn)評(píng)分

        另一個(gè)戰(zhàn)場 - 多端適配&原生混合

        適配京東小程序

        適配京東小程序的過程比較順利,需要改動(dòng)的地方不多。

        在此過程中 Taro3 最主要的升級(jí)是增強(qiáng)了對 HTML 文本的解析能力,增加了對

            
            

                      • 久久五月天性爱视频 | 无码黄色片 | 男人网站在线 | 公交车上荫蒂添的好舒电影 | 成人免费无码婬片在线 |