<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          FlutterWeb性能優(yōu)化探索與實(shí)踐

          共 12084字,需瀏覽 25分鐘

           ·

          2022-01-23 15:37

          點(diǎn)擊“開(kāi)發(fā)者技術(shù)前線”,選擇“星標(biāo)”
          讓一部分開(kāi)發(fā)者看到未來(lái)

          來(lái)自:美團(tuán)技術(shù)團(tuán)隊(duì)

          美團(tuán)外賣(mài)商家端基于 FlutterWeb 的技術(shù)探索已久,目前在多個(gè)業(yè)務(wù)中落地了App、PC、H5的多端復(fù)用,有效提升了產(chǎn)研的整體效率。在這過(guò)程中,性能問(wèn)題是我們面臨的最大挑戰(zhàn),本文結(jié)合實(shí)際業(yè)務(wù)場(chǎng)景進(jìn)行思考,介紹美團(tuán)外賣(mài)商家端在 FlutterWeb 性能優(yōu)化上所進(jìn)行的探索和實(shí)踐,希望對(duì)大家能有所幫助或啟發(fā)。
          • 一、背景

            • 1.1 關(guān)于FlutterWeb

            • 1.2 業(yè)務(wù)現(xiàn)狀

          • 二、挑戰(zhàn)

          • 三、整體設(shè)計(jì)

          • 四、設(shè)計(jì)與實(shí)踐

            • 4.1 精簡(jiǎn) SDK

            • 4.2 JS 分片

            • 4.3 預(yù)加載方案

            • 4.4 分平臺(tái)打包

            • 4.5 圖標(biāo)字體精簡(jiǎn)

          • 五、總結(jié)與展望

          一、背景

          1.1 關(guān)于FlutterWeb

          時(shí)間回?fù)艿?2018 年,Google 首次公開(kāi) FlutterWeb Beta 版,表露出要實(shí)現(xiàn)一份代碼、多端運(yùn)行的愿景。經(jīng)過(guò)無(wú)數(shù)工程師兩年多的努力,在今年年初(2021 年 3 月份),F(xiàn)lutter 2.0 正式對(duì)外發(fā)布,它將 FlutterWeb 功能并入了 Stable Channel,意味著 Google 更加堅(jiān)定了多端復(fù)用的決心。

          圖1 FlutterWeb歷史
          當(dāng)然 Google 的“野心”不是沒(méi)有底氣的,主要體現(xiàn)在它強(qiáng)大的跨端能力上,我們看一下 Flutter 的跨端能力在 Web 側(cè)是如何體現(xiàn)的:

          圖2 Flutter跨端能力
          上圖分別是 FlutterNative 和 FlutterWeb 的架構(gòu)圖。通過(guò)對(duì)比可以看出,應(yīng)用層 Framework 是公用的,意味著在 FlutterWeb 中我們也可以直接使用 Widgets、Gestures 等組件來(lái)實(shí)現(xiàn)邏輯跨端。而關(guān)于渲染跨端,F(xiàn)lutterWeb 提供了兩種模式來(lái)對(duì)齊 Engine 層的渲染能力:Canvaskit Render 和 HTML Render,下方表格對(duì)兩者的區(qū)別進(jìn)行了對(duì)比:

          圖3 模式對(duì)比
          Canvaskit Render 模式:底層基于 Skia 的 WebAssembly 版本,而上層使用 WebGL 進(jìn)行渲染,因此能較好地保證一致性和滾動(dòng)性能,但糟糕的兼容性(WebAssembly 從 Chrome 57 版本才開(kāi)始支持)是我們需要面對(duì)的問(wèn)題。此外 Skia 的 WebAssembly 文件大小達(dá)到了 2.5M,且 Skia 自繪引擎需要字體庫(kù)支持,這意味著需要依賴(lài)超大的中文字體文件,對(duì)頁(yè)面加載性能影響較大,因此目前并不推薦在 Web 中直接使用 Canvaskit Render(官方也建議將 Canvaskit Render 模式用于桌面應(yīng)用)。
          HTML Render 模式:利用 HTML + Canvas 對(duì)齊了 Engine 層的渲染能力,因此兼容性表現(xiàn)優(yōu)秀。另外,MTFlutterWeb 對(duì)滾動(dòng)性能已有過(guò)探索和實(shí)踐,目前能夠應(yīng)對(duì)大部分業(yè)務(wù)場(chǎng)景。而關(guān)于加載性能,此模式下的初始包為 1.2M,是 Canvaskit Render 模式產(chǎn)物體積的 1/2,且我們可對(duì)編譯流程進(jìn)行干預(yù),控制輸出產(chǎn)物,因此優(yōu)化空間較大。
          基于以上原因,美團(tuán)外賣(mài)技術(shù)團(tuán)隊(duì)選擇在 HTML Render 模式下對(duì) FlutterWeb 頁(yè)面的性能進(jìn)行優(yōu)化探索。

          1.2 業(yè)務(wù)現(xiàn)狀

          美團(tuán)外賣(mài)商家端以 App、PC 等多元化的形態(tài)為商家提供了訂單管理、商品維護(hù)、顧客評(píng)價(jià)、外賣(mài)課堂等一系列服務(wù),且 App、PC 雙端業(yè)務(wù)功能基本對(duì)齊。此外,我們還在 PC 上特供了針對(duì)連鎖商家的多店管理功能。同時(shí),為滿(mǎn)足平臺(tái)運(yùn)營(yíng)訴求,部分業(yè)務(wù)具有外投 H5 場(chǎng)景,例如美團(tuán)外賣(mài)商家課堂,它是一個(gè)以文章、視頻等形式幫助商家學(xué)習(xí)外賣(mài)運(yùn)營(yíng)知識(shí)、了解行業(yè)發(fā)展和跟進(jìn)經(jīng)營(yíng)策略的內(nèi)容平臺(tái),具有較強(qiáng)的傳播屬性,因此我們提供了站外分享的能力。

          圖4 業(yè)務(wù)形態(tài)
          為了實(shí)現(xiàn)多端(App、PC、H5)復(fù)用,提升研發(fā)效率,我們于 2021 年年初開(kāi)始著手 MTFlutterWeb 研發(fā)體系的建設(shè)。目前,我們基于 MTFlutterWeb 完成提效的業(yè)務(wù)超過(guò)了 9 個(gè),在 App 中,能夠基于 FlutterNative 提供高性能的服務(wù);在 PC 端和 Mobile 瀏覽器中,利用 FlutterWeb 做到了低成本適配,提升了產(chǎn)研的整體效率。
          然而,加載性能問(wèn)題是 MTFlutterWeb 應(yīng)用推廣的最大障礙。這里依然以美團(tuán)外賣(mài)商家課堂業(yè)務(wù)為例,在項(xiàng)目之初頁(yè)面完全加載時(shí)間 TP90 線達(dá)到了 6s 左右,距離我們的指標(biāo)基線值(頁(yè)面完全加載時(shí)間 TP90 線不高于 3s,基線值主要依據(jù)美團(tuán)外賣(mài)商家端的業(yè)務(wù)場(chǎng)景、用戶(hù)畫(huà)像等來(lái)確定)有些差距,用戶(hù)訪問(wèn)體驗(yàn)有很大的提升空間,因此 FlutterWeb 頁(yè)面加載性能優(yōu)化,是我們亟需解決的問(wèn)題。

          二、挑戰(zhàn)

          不過(guò),想要突破 FlutterWeb 頁(yè)面加載的性能瓶頸,我們面臨的挑戰(zhàn)也是巨大的。這主要體現(xiàn)在 FlutterWeb 缺失靜態(tài)資源的優(yōu)化策略,以及復(fù)雜的架構(gòu)設(shè)計(jì)和編譯流程。下圖展示了 Flutter 業(yè)務(wù)代碼被轉(zhuǎn)換成 Web 平臺(tái)產(chǎn)物的流程,我們來(lái)具體進(jìn)行分析:

          圖5 FlutterWeb 編譯流程
          1. Framework、Flutter_Web_SDKFlutter_Web_SDK 基于 HTML、Canvas,承載 HTML Render 模式的具體實(shí)現(xiàn))等底層 SDK 是可被業(yè)務(wù)代碼直接引入的,幫助我們快速開(kāi)發(fā)出跨端應(yīng)用;
          2. flutter_tools 是各平臺(tái)(Android、iOS、Web)的編譯入口,它接收 flutter build web 命令和參數(shù)并開(kāi)始編譯流程,同時(shí)等待處理結(jié)果回調(diào),在回調(diào)中我們可對(duì)編譯產(chǎn)物進(jìn)行二次加工;
          3. frontend_server 負(fù)責(zé)將 Dart 轉(zhuǎn)換為 AST,生成 kernel 中間產(chǎn)物 app.dill 文件(實(shí)際上各平臺(tái)的編譯過(guò)程都會(huì)生成這樣的中間產(chǎn)物),并交由各平臺(tái) Compiler 進(jìn)行轉(zhuǎn)譯;
          4. Dart2JS Compiler 是 Dart-SDK 中具體負(fù)責(zé)轉(zhuǎn)譯 JS 的模塊,它將上述中間產(chǎn)物 app.dill 進(jìn)行讀取和解析,并注入 Math、List、Map 等 JS 工具方法,最終生產(chǎn)出 Web 平臺(tái)所能執(zhí)行的 JS 文件。
          5. 編譯產(chǎn)物主要為 main.dart.js、index.html、images 等靜態(tài)資源,F(xiàn)lutterWeb 對(duì)這些靜態(tài)資源缺少常規(guī) Web 項(xiàng)目中的優(yōu)化手段,例如:文件 Hash 化、文件分片、CDN 支持等。
          可以看出,要完成對(duì) FlutterWeb 編譯產(chǎn)物的優(yōu)化,就需要干預(yù) FlutterWeb 的眾多編譯模塊。而為了提升整體的編譯效率,大部分模塊都被提前編譯成了 snapshot 文件( 一種 Dart 的編譯產(chǎn)物,可被 Dart VM 所運(yùn)行,用于提升執(zhí)行效率),例如:flutter_tools.snapshot、frontend_server.snapshot、dart2js.snapshot 等,這又加大了對(duì) FlutterWeb 編譯流程進(jìn)行干預(yù)的難度。

          三、整體設(shè)計(jì)

          如前文所述,為了實(shí)現(xiàn)邏輯、渲染跨平臺(tái),F(xiàn)lutter 的架構(gòu)設(shè)計(jì)及編譯流程都具有一定的復(fù)雜性。但由于各平臺(tái)(Android、iOS、Web)的具體實(shí)現(xiàn)是解耦的,因此我們的思路是定位各模塊(Dart-SDK、Framework、Flutter_Web_SDK、flutter_tools)的 Web 平臺(tái)實(shí)現(xiàn)并尋求優(yōu)化,整體設(shè)計(jì)圖如下所示:

          圖6 整體設(shè)計(jì)
          • SDK 瘦身:我們分別對(duì) FlutterWeb 所依賴(lài)的 Dart-SDK、Framework、Flutter_Web_SDK 進(jìn)行了瘦身,并將這些精簡(jiǎn)版 SDK 集成合入 CI/CD(持續(xù)集成與部署)系統(tǒng),為減小產(chǎn)物包體積奠定了基礎(chǔ);
          • 編譯優(yōu)化:此外,我們?cè)?flutter_tools 中的編譯流程做了干預(yù),分別進(jìn)行了 JS 文件分片、靜態(tài)資源 Hash 化、資源文件上傳 CDN 等優(yōu)化,使得這些在常規(guī) Web 應(yīng)用中基礎(chǔ)的性能優(yōu)化手段得以在 FlutterWeb 中落地。同時(shí)加強(qiáng)了 FlutterWeb 特殊場(chǎng)景下的資源優(yōu)化,如:字體圖標(biāo)精簡(jiǎn)、Runtime Manifest 隔離、Mobile/PC 分平臺(tái)打包等;
          • 加載優(yōu)化:在編譯階段進(jìn)行靜態(tài)資源優(yōu)化后,我們?cè)谇岸诉\(yùn)行時(shí),支持了資源預(yù)加載與按需加載,通過(guò)設(shè)定合理的加載時(shí)機(jī),從而減小初始代碼體積,提升頁(yè)面首屏的渲染速度。
          下面,我們分別對(duì)各項(xiàng)優(yōu)化進(jìn)行詳細(xì)的說(shuō)明。

          四、設(shè)計(jì)與實(shí)踐

          4.1 精簡(jiǎn) SDK

          4.1.1 包體積分析

          工欲善其事,必先利其器,在開(kāi)始做體積裁剪之前,我們需要一套類(lèi)似于 webpack-bundle-analyzer 的包體積分析工具,便于直觀地比較各個(gè)模塊的體積占比,為優(yōu)化性能提供幫助。
          Dart2JS 官方提供了 --dump-info 命令選項(xiàng)來(lái)分析 JS 產(chǎn)物,但其表現(xiàn)差強(qiáng)人意,它并不能很好地分析各個(gè)模塊的體積占比。這里更推薦使用 source-map-explorer ,它的原理是通過(guò) sourcemap 文件進(jìn)行反解,能清晰地反映出每個(gè)模塊的占用大小,為 SDK 的精簡(jiǎn)提供了指引。下圖展示了 FlutterWeb JS 產(chǎn)物的反解信息(截圖僅包含 Framework 和 Flutter_Web_SDK):

          圖7 反解信息

          4.1.2 SDK 裁剪

          FlutterWeb 依賴(lài)的 SDK 主要包括 Dart-SDK、Framework 和 Flutter_Web_SDK,這些 SDK 對(duì)包體積的影響是巨大的,幾乎貢獻(xiàn)了初始化包的所有大小。雖然在 Release 模式下的編譯流程中,Dart Compiler 會(huì)利用 Tree-Shaking 來(lái)剔除那些引入但未使用的 packages、classes、functions 等,很大程度上減少了包體積。但這些 SDK 中仍然存在一些能被進(jìn)一步優(yōu)化的代碼。
          以 Flutter Framework 為例,由于它是全平臺(tái)公用的模塊,因此不可避免地存在各平臺(tái)的兼容邏輯(通常以 if-else、switch 等條件判斷形式出現(xiàn)),而這部分代碼是不能被 Tree-Shaking 剔除的,我們觀察如下的代碼:
          //?FileName:?flutter/lib/src/rendering/editable.dart
          void?_handleKeyEvent(RawKeyEvent?keyEvent)?{
          ??if?(kIsWeb)?{
          ????//?On?web?platform,?we?should?ignore?the?key.
          ????return;
          ??}
          ??//?Other?codes?...
          }
          上述代碼選自 Framework 中的 RenderEditable 類(lèi),當(dāng) kIsWeb 變量為真,表示當(dāng)前應(yīng)用運(yùn)行在 Web 平臺(tái)。受限于 Tree-Shaking 的機(jī)制原理,上述代碼中,其它平臺(tái)的兼容邏輯即注釋 Other codes 的部分是無(wú)法被剔除的,但這部分代碼,對(duì) Web 平臺(tái)來(lái)說(shuō)卻是 Dead Code(永遠(yuǎn)不可能被執(zhí)行到的代碼),是可以被進(jìn)一步優(yōu)化的。

          圖8 部分功能構(gòu)成
          上圖展示了 SDK 的一部分功能構(gòu)成,從圖中可以看出,F(xiàn)lutterWeb 依賴(lài)的這些 SDK 中包含了一些使用頻率較低的功能,例如:藍(lán)牙、USB、WebRTC、陀螺儀等功能的支持。為此,我們提供了對(duì)這些長(zhǎng)尾功能的定制能力(這些功能默認(rèn)不開(kāi)啟,但業(yè)務(wù)可配置),將未被啟用長(zhǎng)尾的功能進(jìn)行裁剪。
          通過(guò)上述分析可得,我們的思路就是對(duì) Dead Code 進(jìn)行二次剔除,以及對(duì)這些長(zhǎng)尾功能做裁剪。基于這樣的思路,我們深入 Dart-SDK、Framework 和 Flutter_Web_SDK 各個(gè)擊破,最終將 JS Bundle 產(chǎn)物體積由 1.2M 精簡(jiǎn)至 0.7M,為 FlutterWeb 頁(yè)面性能優(yōu)化打下了堅(jiān)實(shí)的基礎(chǔ)。

          圖9 精簡(jiǎn)成果

          4.1.3 SDK 集成 CI/CD

          為了提升構(gòu)建效率,我們將 FlutterWeb 依賴(lài)的環(huán)境定制為 Docker 鏡像,集成入 CI/CD(持續(xù)集成與部署)系統(tǒng)。SDK 裁剪后,我們需要更新 Docker 鏡像,整個(gè)過(guò)程耗時(shí)較長(zhǎng)且不夠靈活。因此,我們將 Dart-SDK、Framework、Flutter_Web_SDK 按版本打包傳至云端,在編譯開(kāi)始前讀取 CI/CD 環(huán)境變量:sdk_version(SDK 版本號(hào)),遠(yuǎn)程拉取相應(yīng)版本的 SDK 包,并替換當(dāng)前 Docker 環(huán)境中的對(duì)應(yīng)模塊,基于以此方案實(shí)現(xiàn) SDK 的靈活發(fā)布,具體流程圖如下圖所示:

          圖10 集成CI/CD

          4.2 JS 分片

          FlutterWeb 編譯之后默認(rèn)會(huì)生成 main.dart.js 文件,它囊括了 SDK 代碼以及業(yè)務(wù)邏輯,這樣會(huì)引起以下問(wèn)題:
          1. 功能無(wú)法及時(shí)更新:為了實(shí)現(xiàn)瀏覽器的緩存優(yōu)化,我們的項(xiàng)目開(kāi)啟了對(duì)靜態(tài)資源的強(qiáng)緩存,若 main.dart.js 產(chǎn)物不支持 Hash 命名,可能導(dǎo)致程序代碼不能被及時(shí)更新;
          2. 無(wú)法使用 CDN:FlutterWeb 默認(rèn)僅支持相對(duì)域名的資源加載方式,無(wú)法使用當(dāng)前域名以外的 CDN 域名,導(dǎo)致無(wú)法享受 CDN 帶來(lái)的優(yōu)勢(shì);
          3. 首屏渲染性能不佳:雖然我們進(jìn)行了 SDK 瘦身,但 main.dart.js 文件依然維持在 0.7M 以上,單一文件加載、解析時(shí)間過(guò)長(zhǎng),勢(shì)必會(huì)影響首屏的渲染時(shí)間。
          針對(duì)文件 Hash 化和 CDN 加載的支持,我們?cè)?flutter_tools 編譯流程中對(duì)靜態(tài)資源進(jìn)行二次處理:遍歷靜態(tài)資源產(chǎn)物,增加文件 Hash (文件內(nèi)容 MD5 值),并更新資源的引用;同時(shí)通過(guò)定制 Dart-SDK,修改了 main.dart.js、字體等靜態(tài)資源的加載邏輯,使其支持 CDN 資源加載。
          更詳細(xì)的方案設(shè)計(jì)請(qǐng)參考《Flutter Web在美團(tuán)外賣(mài)的實(shí)踐》一文。下面我們重點(diǎn)介紹 main.dart.js 分片相關(guān)的一些優(yōu)化策略。

          4.2.1 Lazy Loading

          Flutter 官方提供 deferred as 關(guān)鍵字來(lái)實(shí)現(xiàn) Widget 的懶加載,而 dart2js 在編譯過(guò)程中可以將懶加載的 Widget 進(jìn)行按需打包,這樣的拆包機(jī)制叫做 Lazy Loading。借助 Lazy Loading,我們可以在路由表中使用 deferred 引入各個(gè)路由(頁(yè)面),以此來(lái)達(dá)到業(yè)務(wù)代碼拆離的目的,具體使用方法和效果如下所示:
          //?使用方式
          import?'pages/index/index.dart'?deferred?as?IndexPageDefer;
          {
          ??'/index':?(context)?=>?FutureBuilder(
          ????future:?IndexPageDefer.loadLibrary(),
          ????builder:?(context,?snapshot)?=>?IndexPageDefer.Demo(),
          ??)
          ??...?...
          }

          圖11 效果演示
          使用 Lazy Loading 后,業(yè)務(wù)頁(yè)面的代碼會(huì)被拆分到了多個(gè) PartJS(對(duì)應(yīng)圖中 xxx.part.js 文件) 中。這樣看似解決了業(yè)務(wù)代碼與 SDK 耦合的問(wèn)題,但在實(shí)際操作過(guò)程中,我們發(fā)現(xiàn)每次業(yè)務(wù)代碼的變動(dòng),仍然會(huì)導(dǎo)致編譯后的 main.dart.js 隨之發(fā)生變化(文件 Hash 值變化)。經(jīng)過(guò)定位與跟蹤,我們發(fā)現(xiàn)這個(gè)變化的部分是 PartJS 的加載邏輯和映射關(guān)系,我們稱(chēng)之為 Runtime Manifest。因此,需要設(shè)計(jì)一套方案對(duì) Runtime Manifest 進(jìn)行抽離,來(lái)保證業(yè)務(wù)代碼的修改對(duì) main.dart.js 的影響達(dá)到最低。

          4.2.2 Runtime Manifest抽離

          通過(guò)對(duì)業(yè)務(wù)代碼的抽離,此時(shí) main.dart.js 文件的構(gòu)成主要包含 SDK 和 ?Runtime Manifest:

          圖12 main.dart.js構(gòu)成
          那如何能將 Runtime Manifest 進(jìn)行抽離呢?對(duì)比常規(guī) Web 項(xiàng)目,我們的處理方式是把 SDK、Utils、三方包等基礎(chǔ)依賴(lài),利用 Webpack、Rollup 等打包工具進(jìn)行抽離并賦予一個(gè)穩(wěn)定的 Hash 值。同時(shí),將 Runtime Manifest (分片文件的加載邏輯和映射關(guān)系)注入到 HTML 文件中,這樣保證了業(yè)務(wù)代碼的變動(dòng)不會(huì)影響到公共包。借助常規(guī) Web 項(xiàng)目的編譯思路,我們深入分析了 FlutterWeb 中 Runtime Manifest 的生成邏輯和 PartJS 的加載邏輯,定制出如下的解決方案:

          圖13 Runtime Manifest抽離
          在上圖中,Runtime Manifest 的生成邏輯位于 Dart2JS Compiler 模塊,在該生成邏輯中,我們對(duì) Runtime Manifest 代碼塊進(jìn)行了標(biāo)記,之后在 flutter_tools 中將標(biāo)記的 Runtime Manifest 代碼塊抽離并寫(xiě)入 HTML 文件中(以 JS 常量形式存在)。而在 PartJS 的加載流程中,我們將 manifest 信息的讀取方式改為了 JS 常量的獲取。按照這樣的拆分方式,業(yè)務(wù)代碼的變更只會(huì)改變 Runtime Manifest 信息 ,而不會(huì)影響到 main.dart.js 公共包。

          4.2.3 main.dart.js 切片

          經(jīng)過(guò)以上引入 Lazy Loading、Runtime Manifest 抽離,main.dart.js 文件的體積穩(wěn)定在 0.7M 左右,瀏覽器對(duì)大體積單文件的加載,會(huì)有很沉重的網(wǎng)絡(luò)負(fù)擔(dān),所以我們?cè)O(shè)計(jì)了切片方案,充分地利用瀏覽器對(duì)多文件并行加載的特性,提升文件的加載效率。
          具體實(shí)現(xiàn)方案為:將 main.dart.js 在 flutter_tools 編譯過(guò)程拆分成多份純文本文件,前端通過(guò) XHR 的方式并行加載并按順序拼接成 JavaScript 代碼置于
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    亚洲在线中文字幕 | 麻豆搜索结果 -- 爱色AV | 丝袜jk美女足交 | 成人网站在线看。 | 在线日韩国产网站 |