美团外卖商家端基于 FlutterWeb 的技术摸索已久,目前在多个业务中落地了 App、PC、H5 的多端复用,无效晋升了产研的整体效率。在这过程中,性能问题是咱们面临的最大挑战,本文结合实际业务场景进行思考,介绍美团外卖商家端在 FlutterWeb 性能优化上所进行的摸索和实际,心愿对大家能有所帮忙或启发。
一、背景
1.1 对于 FlutterWeb
工夫回拨到 2018 年,Google 首次公开 FlutterWeb Beta 版,表露出要实现一份代码、多端运行的愿景。通过有数工程师两年多的致力,在今年年初(2021 年 3 月份),Flutter 2.0 正式对外公布,它将 FlutterWeb 性能并入了 Stable Channel,意味着 Google 更加动摇了多端复用的信心。
当然 Google 的“野心”不是没有底气的,次要体现在它弱小的跨端能力上,咱们看一下 Flutter 的跨端能力在 Web 侧是如何体现的:
上图别离是 FlutterNative 和 FlutterWeb 的架构图。通过比照能够看出,应用层 Framework 是专用的,意味着在 FlutterWeb 中咱们也能够间接应用 Widgets、Gestures 等组件来实现逻辑跨端。而对于渲染跨端,FlutterWeb 提供了两种模式来对齐 Engine 层的渲染能力:Canvaskit Render 和 HTML Render,下方表格对两者的区别进行了比照:
Canvaskit Render 模式:底层基于 Skia 的 WebAssembly 版本,而下层应用 WebGL 进行渲染,因而能较好地保障一致性和滚动性能,但蹩脚的兼容性(WebAssembly 从 Chrome 57 版本才开始反对)是咱们须要面对的问题。此外 Skia 的 WebAssembly 文件大小达到了 2.5M,且 Skia 自绘引擎须要字体库反对,这意味着须要依赖超大的中文字体文件,对页面加载性能影响较大,因而目前并不举荐在 Web 中间接应用 Canvaskit Render(官网也倡议将 Canvaskit Render 模式用于桌面利用)。
HTML Render 模式:利用 HTML + Canvas 对齐了 Engine 层的渲染能力,因而兼容性体现优良。另外,MTFlutterWeb 对滚动性能已有过摸索和实际,目前可能应答大部分业务场景。而对于加载性能,此模式下的初始包为 1.2M,是 Canvaskit Render 模式产物体积的 1/2,且咱们可对编译流程进行干涉,管制输入产物,因而优化空间较大。
基于以上起因,美团外卖技术团队抉择在 HTML Render 模式下对 FlutterWeb 页面的性能进行优化摸索。
1.2 业务现状
美团外卖商家端以 App、PC 等多元化的状态为商家提供了订单治理、商品保护、顾客评估、外卖课堂等一系列服务,且 App、PC 双端业务性能根本对齐。此外,咱们还在 PC 上特供了针对连锁商家的多店治理性能。同时,为满足平台经营诉求,局部业务具备外投 H5 场景,例如美团外卖商家课堂,它是一个以文章、视频等模式帮忙商家学习外卖经营常识、理解行业倒退和跟进经营策略的内容平台,具备较强的流传属性,因而咱们提供了站外分享的能力。
为了实现多端(App、PC、H5)复用,晋升研发效率,咱们于 2021 年年初开始着手 MTFlutterWeb 研发体系的建设。目前,咱们基于 MTFlutterWeb 实现提效的业务超过了 9 个,在 App 中,可能基于 FlutterNative 提供高性能的服务;在 PC 端和 Mobile 浏览器中,利用 FlutterWeb 做到了低成本适配,晋升了产研的整体效率。
然而,加载性能问题是 MTFlutterWeb 利用推广的最大阻碍。这里仍然以美团外卖商家课堂业务为例,在我的项目之初页面齐全加载工夫 TP90 线达到了 6s 左右,间隔咱们的指标基线值(页面齐全加载工夫 TP90 线不高于 3s,基线值次要根据美团外卖商家端的业务场景、用户画像等来确定)有些差距,用户拜访体验有很大的晋升空间,因而 FlutterWeb 页面加载性能优化,是咱们亟需解决的问题。
二、挑战
不过,想要冲破 FlutterWeb 页面加载的性能瓶颈,咱们面临的挑战也是微小的。这次要体现在 FlutterWeb 缺失动态资源的优化策略,以及简单的架构设计和编译流程。下图展现了 Flutter 业务代码被转换成 Web 平台产物的流程,咱们来具体进行剖析:
- Framework、Flutter_Web_SDK(Flutter_Web_SDK 基于 HTML、Canvas,承载 HTML Render 模式的具体实现)等底层 SDK 是可被业务代码间接引入的,帮忙咱们疾速开发出跨端利用;
- flutter_tools 是各平台(Android、iOS、Web)的编译入口,它接管 flutter build web 命令和参数并开始编译流程,同时期待处理结果回调,在回调中咱们可对编译产物进行二次加工;
- frontend_server 负责将 Dart 转换为 AST,生成 kernel 两头产物 app.dill 文件(实际上各平台的编译过程都会生成这样的两头产物),并交由各平台 Compiler 进行转译;
- Dart2JS Compiler 是 Dart-SDK 中具体负责转译 JS 的模块,它将上述两头产物 app.dill 进行读取和解析,并注入 Math、List、Map 等 JS 工具办法,最终生产出 Web 平台所能执行的 JS 文件。
- 编译产物 次要为 main.dart.js、index.html、images 等动态资源,FlutterWeb 对这些动态资源短少惯例 Web 我的项目中的优化伎俩,例如:文件 Hash 化、文件分片、CDN 反对等。
能够看出,要实现对 FlutterWeb 编译产物的优化,就须要干涉 FlutterWeb 的泛滥编译模块。而为了晋升整体的编译效率,大部分模块都被提前编译成了 snapshot 文件(一种 Dart 的编译产物,可被 Dart VM 所运行,用于晋升执行效率),例如:flutter_tools.snapshot、frontend_server.snapshot、dart2js.snapshot 等,这又加大了对 FlutterWeb 编译流程进行干涉的难度。
三、整体设计
如前文所述,为了实现逻辑、渲染跨平台,Flutter 的架构设计及编译流程都具备肯定的复杂性。但因为各平台(Android、iOS、Web)的具体实现是解耦的,因而咱们的思路是定位各模块(Dart-SDK、Framework、Flutter_Web_SDK、flutter_tools)的 Web 平台实现并寻求优化,整体设计图如下所示:
- SDK 瘦身:咱们别离对 FlutterWeb 所依赖的 Dart-SDK、Framework、Flutter_Web_SDK 进行了瘦身,并将这些精简版 SDK 集成合入 CI/CD(继续集成与部署)零碎,为减小产物包体积奠定了根底;
- 编译优化:此外,咱们在 flutter_tools 中的编译流程做了干涉,别离进行了 JS 文件分片、动态资源 Hash 化、资源文件上传 CDN 等优化,使得这些在惯例 Web 利用中根底的性能优化伎俩得以在 FlutterWeb 中落地。同时增强了 FlutterWeb 非凡场景下的资源优化,如:字体图标精简、Runtime Manifest 隔离、Mobile/PC 分平台打包等;
- 加载优化:在编译阶段进行动态资源优化后,咱们在前端运行时,反对了资源预加载与按需加载,通过设定正当的加载机会,从而减小初始代码体积,晋升页面首屏的渲染速度。
上面,咱们别离对各项优化进行具体的阐明。
四、设计与实际
4.1 精简 SDK
4.1.1 包体积剖析
工欲善其事,必先利其器,在开始做体积裁剪之前,咱们须要一套相似于 webpack-bundle-analyzer 的包体积剖析工具,便于直观地比拟各个模块的体积占比,为优化性能提供帮忙。
Dart2JS 官网提供了 –dump-info 命令选项来剖析 JS 产物,但其体现差强人意,它并不能很好地剖析各个模块的体积占比。这里更举荐应用 source-map-explorer,它的原理是通过 sourcemap 文件进行反解,能清晰地反映出每个模块的占用大小,为 SDK 的精简提供了指引。下图展现了 FlutterWeb JS 产物的反解信息(截图仅蕴含 Framework 和 Flutter_Web_SDK):
4.1.2 SDK 裁剪
FlutterWeb 依赖的 SDK 次要包含 Dart-SDK、Framework 和 Flutter_Web_SDK,这些 SDK 对包体积的影响是微小的,简直奉献了初始化包的所有大小。尽管在 Release 模式下的编译流程中,Dart Compiler 会利用 Tree-Shaking 来剔除那些引入但未应用的 packages、classes、functions 等,很大水平上缩小了包体积。但这些 SDK 中依然存在一些能被进一步优化的代码。
以 Flutter Framework 为例,因为它是全平台专用的模块,因而不可避免地存在各平台的兼容逻辑(通常以 if-else、switch 等条件判断模式呈现),而这部分代码是不能被 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 类,当 kIsWeb 变量为真,示意以后利用运行在 Web 平台。受限于 Tree-Shaking 的机制原理,上述代码中,其它平台的兼容逻辑即正文 Other codes 的局部是无奈被剔除的,但这部分代码,对 Web 平台来说却是 Dead Code(永远不可能被执行到的代码),是能够被进一步优化的。
上图展现了 SDK 的一部分性能形成,从图中能够看出,FlutterWeb 依赖的这些 SDK 中蕴含了一些应用频率较低的性能,例如:蓝牙、USB、WebRTC、陀螺仪等性能的反对。为此,咱们提供了对这些长尾性能的定制能力(这些性能默认不开启,但业务可配置),将未被启用长尾的性能进行裁剪。
通过上述剖析可得,咱们的思路就是对 Dead Code 进行二次剔除,以及对这些长尾性能做裁剪。基于这样的思路,咱们深刻 Dart-SDK、Framework 和 Flutter_Web_SDK 各个击破,最终将 JS Bundle 产物体积由 1.2M 精简至 0.7M,为 FlutterWeb 页面性能优化打下了松软的根底。
4.1.3 SDK 集成 CI/CD
为了晋升构建效率,咱们将 FlutterWeb 依赖的环境定制为 Docker 镜像,集成入 CI/CD(继续集成与部署)零碎。SDK 裁剪后,咱们须要更新 Docker 镜像,整个过程耗时较长且不够灵便。因而,咱们将 Dart-SDK、Framework、Flutter_Web_SDK 按版本打包传至云端,在编译开始前读取 CI/CD 环境变量:sdk_version(SDK 版本号),近程拉取相应版本的 SDK 包,并替换以后 Docker 环境中的对应模块,基于以此计划实现 SDK 的灵便公布,具体流程图如下图所示:
4.2 JS 分片
FlutterWeb 编译之后默认会生成 main.dart.js 文件,它囊括了 SDK 代码以及业务逻辑,这样会引起以下问题:
- 性能无奈及时更新:为了实现浏览器的缓存优化,咱们的我的项目开启了对动态资源的强缓存,若 main.dart.js 产物不反对 Hash 命名,可能导致程序代码不能被及时更新;
- 无奈应用 CDN:FlutterWeb 默认仅反对绝对域名的资源加载形式,无奈应用以后域名以外的 CDN 域名,导致无奈享受 CDN 带来的劣势;
- 首屏渲染性能不佳:尽管咱们进行了 SDK 瘦身,但 main.dart.js 文件仍然维持在 0.7M 以上,繁多文件加载、解析工夫过长,势必会影响首屏的渲染工夫。
针对文件 Hash 化和 CDN 加载的反对,咱们在 flutter_tools 编译流程中对动态资源进行二次解决:遍历动态资源产物,减少文件 Hash(文件内容 MD5 值),并更新资源的援用;同时通过定制 Dart-SDK,批改了 main.dart.js、字体等动态资源的加载逻辑,使其反对 CDN 资源加载。
更具体的方案设计请参考《Flutter Web 在美团外卖的实际》一文。上面咱们重点介绍 main.dart.js 分片相干的一些优化策略。
4.2.1 Lazy Loading
Flutter 官网提供 deferred as
关键字来实现 Widget 的懒加载,而 dart2js 在编译过程中能够将懒加载的 Widget 进行按需打包,这样的拆包机制叫做 Lazy Loading。借助 Lazy Loading,咱们能够在路由表中应用 deferred 引入各个路由(页面),以此来达到业务代码拆离的目标,具体应用办法和成果如下所示:
// 应用形式
import 'pages/index/index.dart' deferred as IndexPageDefer;
{'/index': (context) => FutureBuilder(future: IndexPageDefer.loadLibrary(),
builder: (context, snapshot) => IndexPageDefer.Demo(),)
... ...
}
应用 Lazy Loading 后,业务页面的代码会被拆分到了多个 PartJS(对应图中 xxx.part.js 文件)中。这样看似解决了业务代码与 SDK 耦合的问题,但在实际操作过程中,咱们发现每次业务代码的变动,依然会导致编译后的 main.dart.js 随之发生变化(文件 Hash 值变动)。通过定位与跟踪,咱们发现这个变动的局部是 PartJS 的加载逻辑和映射关系,咱们称之为 Runtime Manifest。因而,须要设计一套计划对 Runtime Manifest 进行抽离,来保障业务代码的批改对 main.dart.js 的影响达到最低。
4.2.2 Runtime Manifest 抽离
通过对业务代码的抽离,此时 main.dart.js 文件的形成次要蕴含 SDK 和 Runtime Manifest:
那如何能将 Runtime Manifest 进行抽离呢?比照惯例 Web 我的项目,咱们的解决形式是把 SDK、Utils、三方包等根底依赖,利用 Webpack、Rollup 等打包工具进行抽离并赋予一个稳固的 Hash 值。同时,将 Runtime Manifest(分片文件的加载逻辑和映射关系)注入到 HTML 文件中,这样保障了业务代码的变动不会影响到公共包。借助惯例 Web 我的项目的编译思路,咱们深入分析了 FlutterWeb 中 Runtime Manifest 的生成逻辑和 PartJS 的加载逻辑,定制出如下的解决方案:
在上图中,Runtime Manifest 的生成逻辑位于 Dart2JS Compiler 模块,在该生成逻辑中,咱们对 Runtime Manifest 代码块进行了标记,之后在 flutter_tools 中将标记的 Runtime Manifest 代码块抽离并写入 HTML 文件中(以 JS 常量模式存在)。而在 PartJS 的加载流程中,咱们将 manifest 信息的读取形式改为了 JS 常量的获取。依照这样的拆分形式,业务代码的变更只会扭转 Runtime Manifest 信息,而不会影响到 main.dart.js 公共包。
4.2.3 main.dart.js 切片
通过以上引入 Lazy Loading、Runtime Manifest 抽离,main.dart.js 文件的体积稳固在 0.7M 左右,浏览器对大体积单文件的加载,会有很惨重的网络累赘,所以咱们设计了切片计划,充沛地利用浏览器对多文件并行加载的个性,晋升文件的加载效率。
具体实现计划为:将 main.dart.js 在 flutter_tools 编译过程拆分成多份纯文本文件,前端通过 XHR 的形式并行加载并按程序拼接成 JavaScript 代码置于 < script > 标签中,从而实现切片文件的并行加载。
4.3 预加载计划
如上一节所述,尽管咱们做了很多工作来稳固 main.dart.js 的内容,但在 Flutter Tree-Shaking 的运行机制下,各个我的项目援用不同的 Framework Widget,就会导致每个我的项目生成的 main.dart.js 内容不统一。随着接入 FlutterWeb 的我的项目越来越多,每个业务的页面互访概率也越来越高,咱们的冀望是当拜访 A 业务时,能够事后缓存 B 业务援用的 main.dart.js,这样当用户真正进入 B 业务时就能够节俭加载资源的工夫,上面为具体的技术计划。
4.3.1 技术计划
咱们把整体的技术计划分为编译、监听、运行三个阶段。
- 编译阶段,在公布流水线上依据后期定制的匹配规定,筛选出符合条件的资源文件门路,生成云端 JSON 并上传;
- 监听阶段,在 DOMContentLoaded 之后,对网络资源、事件、DOM 变动进行监听,并对监听后果依据特定规定进行剖析加权,失去一个首屏加载实现的状态标识;
- 运行阶段,在首屏加载实现之后对配置平台下发的云端 JSON 文件进行解析,对合乎配置规定的资源进行 HTTP XHR 预加载,从而实现文件的预缓存性能。
下图为预缓存的整体方案设计:
编译阶段
编译阶段会扩大现有的公布流水线,在 flutter build 之后减少 prefetch build 作业,这样 build 之后就能够对产物目录进行遍历和筛选,失去咱们所需资源进而生成云端 JSON,为运行阶段提供数据根底。上面的流程图为编译阶段的具体方案设计:
编译阶段分为三局部:
- 第一局部:依据不同的公布环境,初始化线上 / 线下的配置平台,为配置文件的读写做好筹备;
- 第二局部:下载并解析配置平台下发的资源组 JSON,筛选出合乎配置规定的资源门路,更新 JSON 文件并公布到配置平台;
- 第三局部:通过公布流水线提供的 API,把 PROJECT_ID、公布环境注入 HTML 文件中,为运行阶段提供全局变量以便读取。
通过对流水线编译期的整合,咱们能够生成新的云端 JSON 并上传到云端,为运行阶段的下发提供数据根底。
监听阶段
咱们晓得,浏览器对文件申请的并发数量是有限度的,为了保障浏览器对以后页面的渲染处于高优先级,同时还能实现预缓存的性能,咱们设计了一套对缓存文件的加载策略,在不影响以后页面加载的状况下,实现对缓存文件的加载操作。以下为具体的技术计划:
在页面 DOMContentLoaded 之后,咱们会监听三局部的的变动。
- 第一局部是监听 DOM 的变动。这部分次要是在页面产生 Ajax 申请之后,随着 MV 模式的变动,DOM 也会随之发生变化。咱们应用浏览器提供的 MutationObserver API 对 DOM 变动进行收集,并筛选无效节点进行深度优先遍历,计算每个 DOM 的递归权重值,低于阈值咱们就认为首屏已加载实现。
- 第二局部是监听资源的变动。咱们利用浏览提供的 PerformanceObserver API,筛选出 img/script 类型的资源,在 3 秒内收集的资源没有减少时,咱们认为首屏已加载实现。
- 第三局部是监听 Event 事件。当用户产生 click、wheel、touchmove 等交互行为时,咱们就认为以后页面处于一个可交互的状态,即首屏加载已实现,这样会在后续进行资源的预缓存。
通过上述步骤,咱们就能够失去一个首屏渲染实现的机会,之后就能够实现预缓存性能了。以下为预缓存性能的实现。
运行阶段
预缓存的整体流程为:下载编译阶段生成的云端 JSON,解析出须要进行预缓存资源的 CDN 门路,最初通过 HTTP XHR 进行缓存资源进行申请,利用浏览器自身的缓存策略,把其余业务的资源文件写入。当用户拜访已命中缓存的页面时,资源已被提前加载,这样能够无效地缩小首屏的加载工夫。下图为运行阶段的具体方案设计:
在监听阶段,咱们能够获取到页面的首屏渲染实现的机会,会获取到云端 JSON,首先判断该项目标缓存是否为启用状态。当该我的项目可用时,会依据全局变量 PROJECT_ID 进行资源数组的匹配,再以 HTTP XHR 形式进行预拜访,把缓存文件写入浏览器缓存池中。至此,资源预缓存已执行结束。
4.3.2 成果展现与数据比照
当有页面间互拜访命中预缓存时,浏览器会以 200(Disk Cache)的形式返回数据,这样就节俭了大量资源加载的工夫,下图为命中缓存后资源加载状况:
目前,美团外卖商家端业务已有 10+ 个页面接入了预缓存性能,资源加载 90 线平均值由 400ms 降落到 350ms,升高了 12.5%;50 线平均值由 114ms 降落到 100ms,升高了 12%。随着我的项目接入接入越来越多,预缓存的成果也会越发的显著。
4.4 分平台打包
如前文所述,美团外卖商家业务大部分都是双端对齐的。为了实现提效的最大化,咱们对 FlutterWeb 的多平台适配能力进行增强,实现了 FlutterWeb 在 PC 侧的复用。
在 PC 适配过程中,咱们不可避免地须要书写双端的兼容代码,如:为了实现在列表页面中对卡片组件的复用。为此咱们开发了一个适配工具 ResponsiveSystem,别离传入 PC 和 App 的各端实现,外部会辨别平台实现适配:
// ResponsiveSystem 应用举例
Container(
child: ResponsiveSystem(app: AppWidget(),
pc: PCWidget(),),
)
上述代码能较不便的实现 PC 和 App 适配,但 AppWidget 或 PCWidget 在编译过程中都将无奈被 Tree-Shaking 去除,因而会影响包体积大小。对此,咱们将编译流程进行优化,设计分平台打包计划:
- 批改 flutter-cli,使其反对 –responsiveSystem 命令行参数;
- 咱们在 flutter_tools 中的 AST 分析阶段减少了额定的解决:ResponsiveSystem 关键字的匹配,同时联合编译平台(PC 或 Mobile)来进行 AST 节点的改写;
- 去除无用 AST 节点后,生成各个平台的代码快照(每份快照仅蕴含独自平台代码);
- 依据代码快照编译生成 PC 和 App 两套 JS 产物,并进行资源隔离。而对于 images、fonts 等专用资源,咱们将其打入 common 目录。
通过这样的形式,咱们去除了各自平台的无用代码,防止了 PC 适配过程中引起的包体积问题。仍然以美团外卖商家课堂业务(6 个页面)为例,接入分平台打包后,单平台代码体积减小 100KB 左右。
4.5 图标字体精简
当拜访 FlutterWeb 页面时,即便在业务代码中并未应用 Icon 图标,也会加载一个 920KB 的图标字体文件:MaterialIcons-Regular.woff。通过探索,咱们发现是 Flutter Framework 中一些零碎 UI 组件(如:CalendarDatePicker、PaginatedDataTable、PopupMenuButton 等)应用到了 Icon 图标导致,且 Flutter 为了便于开发者应用,提供了全量的 Icon 图标字体文件。
Flutter 官网提供的 --tree-shake-icons
命令选项是将业务应用到的 Icon 与 Flutter 外部保护的一个放大版字体文件(大概 690KB)进行合并,能肯定水平上减小字体文件大小。而咱们须要的是只打包业务应用的 Icon,所以咱们对官网 tree-shake-icons
进行了优化,设计了 Icon 的按需打包计划:
- 扫描全副业务代码以及依赖的 Plugins、Packages、Flutter Framework,剖析出所有用到的 Icon;
- 把扫描到的所有 Icon 与 material/icons.dart(该文件蕴含 Flutter Icon 的 unicode 编码汇合)进行比照,失去精简后的图标编码列表:iconStrList;
- 应用 FontTools 工具把 iconStrList 生成字体文件 .woff,此时的字体文件仅蕴含真正应用到的 Icon。
通过以上的计划,咱们解决了字体文件过大带来的包体积问题,以美团外卖课堂业务(业务代码中应用了 5 个 Icon)为例,字体文件从 920KB 精简为 11.6kB。
五、总结与瞻望
综上所述,咱们基于 HTML Render 模式对 FlutterWeb 性能优化进行了摸索和实际,次要包含 SDK(Dart-SDK、Framework、Flutter_Web_SDK)的精简,动态资源产物优化(例如:JS 分片、文件 Hash、字体图标文件精简、分平台打包等)和前端资源加载优化(预加载与按需申请)。最终使得 JS 产物由 1.2M 缩小至 0.7M(非业务代码),页面齐全加载工夫 TP90 线由 6s 降到了 3s,这样的后果已能满足美团外卖商家端的大部分业务要求。而将来的布局将聚焦于以下 3 个方向:
- 升高 Web 端适配老本:目前已有 9+ 个业务借助 MTFlutterWeb 实现多端复用,但在 Web 侧(尤其是 PC 侧)的适配效率仍然有优化空间,指标是将适配老本升高到 10% 以下(目前大概是 20%);
- 构建 FlutterWeb 容灾体系:Flutter 动态化包有肯定的加载失败概率,而 FlutterWeb 作为兜底计划,能晋升整体业务的加载成功率。此外 FlutterWeb 能够提供“免装置更新”的能力,升高 FlutterNative 老旧历史版本的保护老本;
- 性能优化的继续推动:性能优化的阶段性成绩为 MTFlutterWeb 的利用推广坚固了根底,但仍然是有进一步优化空间的,例如:目前咱们仅将业务代码和 Runtime Manifest 进行了拆离,而 Framework 及 三方包在肯定水平上也影响到了浏览器缓存的命中率,将这部分代码进行抽离,可进一步晋升页面加载性能。
浏览美团技术团队更多技术文章合集
前端 | 算法 | 后端 | 数据 | 平安 | 运维 | iOS | Android | 测试
| 在公众号菜单栏对话框回复【2020 年货】、【2019 年货】、【2018 年货】、【2017 年货】等关键词,可查看美团技术团队历年技术文章合集。
| 本文系美团技术团队出品,著作权归属美团。欢送出于分享和交换等非商业目标转载或应用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者应用。任何商用行为,请发送邮件至 tech@meituan.com 申请受权。