小程序编译器性能优化之路

在这里插入图片描述

作者 | 马可

导读

小程序编译器是百度开发者工具中的编译构建模块,用来将小程序代码转换成运行时代码。旧版编译器由于业务发展,存在编译慢、内存占用高的问题,我们对编译器做了一次大规模的重构,采用自研架构,做了多线程、代码缓存、sourcemap 等多项优化,在性能和内存占用上都有很大提升。全文介绍了新版编译器的设计思路和优化方法,以及一些能够用在通用打包工具里的技术点。

全文6629字,预计阅读时间17分钟。

01 前言

小程序编译器在小程序开发、预览、发布各个阶段都需要使用,因此编译器性能会直接影响到开发者开发效率,也会影响到开发者工具的使用体验。

由于旧版的编译器(基于 webpack4)在构建大型项目时会很慢,内存占用也高,一直被开发者吐槽。我们经过大量的调研和开发,最后采用完全自研架构做新编译,针对小程序项目构建做了大量优化,基本解决了旧编译存在的问题。

下图是部分项目构建时间对比:

图片

新版编译器相对于旧版实现了 2~7 倍的性能提升,并且支持实时编译、热重载等特性,内存占用更少,构建产物更优。

下面从 框架选型、新编译器工作原理、性能和产物优化方法 等方面介绍新版编译器的成长之路。

02 框架选型

在进行新版编译器设计时,需要明确当前的痛点问题:性能,优先解决性能问题。其他新技术和新想法对编译器有帮助的也一起实施。

旧版编译器基于 webpack4 存在如下几个问题:

  • 大型项目构建速度太慢。

  • dev 启动慢、增量编译慢,仅支持 loader 缓存,bundle 无缓存也比较慢。

  • 基于 webpack4 做扩展开发,需要 patch 部分模块才能工作,维护困难。

  • 部分 webpack bundle 过程无法针对小程序代码结构进行优化,存在无效构建。

新编译的设计目标:

  • 更快的全量编译速度,消除 webpack 存在的无效构建过程。

  • 支持全缓存,加快首次和增量编译速度。

  • 支持实时编译,减少 dev 启动和二次编译时间。

  • 支持多线程编译加速,支持页面热重载。

  • 优化产物结构,减少产物体积。

2.1 主流构建工具

下面介绍的是我们调研过的主流前端构建工具,每个工具都有适用场景和优缺点。

在新版本编译器架构设计时,其他构建工具的设计理念和技术特点都值得参考。

Webpack 构建过程:

图片

Webpack 优点:功能完善、社区活跃、可配置性强、有很强的扩展性。

Webpack 缺点:配置复杂、构建速度慢,二次开发困难。

Parcel 构建过程:

图片

Parcel 优点:无需配置,构建速度快,原生支持多线程和全缓存,多线程之间共享数据通过 lmdb 进行,避免跨线程通信开销。

Parcel 缺点:生态小,自定义性有限,大量采用 Node 插件,兼容性也差一些。

Vite 构建过程:

图片

Vite 优点:配置较为简单,按需编译,启动快,dev 时有不错的体验。

Vite 缺点:生态小,dev 和 发布走两套构建流程。

其他小程序平台:

  • 微信基于 gulp 和 C++ 模块做小程序构建,并且对 npm 模块做了预构建,在性能和开发体验上做的比较好。

  • 支付宝基于 webpack 做小程序构建,并且使用了 esbuild 加速代码压缩。

  • 抖音小程序使用自研编译器,构建流程比较简单。

2.2 新版编译器

在设计新编译框架时,借鉴了主流打包工具的工作流程,结合小程序代码特点,决定不做通用打包工具,重点优化小程序打包性能。

最终选择了自研编译器的方案,并做了大量优化工作,新版编译器优化点有如下几个方面:

1.支持多 Compiler 协同工作,将动态库开发等多类型项目构建解耦。

2.编译阶段全流程缓存,节省二次构建时间 90% 以上。

3.dev 开发默认采用按需编译,提升单页编译性能。

4.支持 babel 和 swc 多线程编译,提升全量编译速度 2 ~ 7 倍。

5.采用新版 sourcemap 协议,移除非必要解析合并,将 bundle 阶段耗时大幅缩减。

6.对 js、css、swan 模板编译均做了构建时标记优化,减少 bundle 合并耗时。

7.对于预览、发布阶段的 js 压缩和混淆,采用了 terser 和 esbuild 并行方案,esbuild 用于快速打出预览包,terser 可以保证压缩率用于发布包。

从结果看,新编译器从速度、资源占用和可维护性上相对于旧版都有显著的提升。

03 新版编译器工作原理

新编译器的处理流程和 parcel 比较类似,Compiler 控制处理流程,Processor 进行代码转换,基本流程如下:

图片

其中几个重要的模块:

  • CompileEntry 编译器为入口模块,包含 cli 通信、dev server 通信、命令调用等。

  • CompileManager 为编译管理器,用于依赖资源下载和管理以及多个 Compiler 协同构建。

  • Compiler 为编译器模块,用于将项目源码编译成运行时代码,项目构建时 Compiler 可能有多个。

  • Processor 为单元处理器,用于处理 代码转换、代码合并 等单个编译任务。

:小程序 App 项目有 1 个Compiler,动态库和动态扩展项目 2 个Compiler。

3.1 Compiler 编译器

用于编译单个小程序项目,将开发者原始代码编译为可运行代码。

工作职能:

1.创建运行上下文,提供 config、fs 文件处理、watcher 监控、logger 等模块,给 Processor 使用。

2.全量编译、文件变更时二次编译;这里二次编译也是走一遍全量编译流程,不过大部分用的是缓存结果。

3.管理、调度、运行 Processor 处理单元。

4.维护 Processor 依赖关系和结果缓存。

特点:

1.实现全流程缓存,将每个 Processor 的输入参数、输出结果写入缓存,在有缓存情况下二次编译时长可减少 90% 。

2.支持按需编译,每次按需单页编译、增量编译、全量编译 都走同样的 Processor 处理流程。

3.通过 Proxy 机制自动计算缓存参数依赖,不用手动为每个 Processor 生成缓存 hash,相对于 webpack 或 parcel 减少 bug 产生。

4.仅维护 Processor 依赖关系,不维护 ModuleGraph,简化处理流程。

关于全流程缓存每家打包器都有自己的实现方案,基本原理是根据当前输入参数和依赖情况为处理单元生成一个唯一 hash,hash 一致则结果一致。

webpack 和 parcel 由于维护了 ModuleGraph,缓存的计算和重用会复杂一些。小程序编译器仅根据 Processor 入参和调用依赖进行计算。

3.2 Processor 单元处理器

Processor 有如下特性:

1.在输入参数一致的情况下,保证输出一致,输入和输出都必须可序列化为 json ,实现了 Processor 全缓存。

2.Processor 中的 uri 为构建 ID,在单次构建过程中 ID 一致则处理结果一致,例如处理 app.js 文件,uri 为:js:app.js,好处是可以统一 Processor 资源处理路径。

3.Processor 之间支持互相调用:processWith 调用并继续执行,processWithResult 调用并等待返回结果。

注意:这里的输入参数包含 uri、app config, contextFreeData。

几种常用的 Processor:

1.JS Processor 将 es6 代码转换成 es5 代码,这是最耗时的模块。

2.Swan Processor 将 swan 模板代码转换成 view 层 js 代码。

3.Css Processor 使用 postcss 处理 css 中的单位转换、依赖收集等工作。

4.Bundle Processor 将前面 transformer 处理结果按照 bundle 算法合并文件并输出结果。

Processor 工作流程:

图片

Processor 处理流程需要经过 transform -> bundle 的过程,在小程序里 js, css, swan 模板的 bundle 可以分开并行处理,这里和 webpack 的处理模式不一样,和 parcel 的 pipeline 类似。

3.3 性能和产物优化方法

3.3.1 多核心编译优化

由于 Node 中多线程模块初始化速度和通信效率比多进程好一些,新编译选择使用 多线程 做多核心优化。

多线程编译有 2 种方案选择:

  • 方案1:基于 processor 做多线程调度,由于 processor 间支持相互调用,实际处理会很复杂且有通信成本。

  • 旧的编译器做过基于webpack 的 workerthread-loader,性能提升有限(10%~15%)。

  • parcel 基于 lmdb 公共缓存消除线程间通信,保证读写效率,是一个比较好的解决方法。

  • 方案2:仅对 js 转译做多线程调度,仅有一来一回 2 次通信成本。

  • 使用 jest-worker 和 babel transform 做 js 多线程转译或者用 swc 多线程做 js 转译。

由于大部分构建时间在 js 转译这里(js 中有大量 node_modules 依赖,均需要转换),css 和 swan 模块转换耗时少。

最终选择方案2 仅做 js 多线程转译,处理流程简单且收益较好,整体提升如下:

  • 使用 jest-worker 多线程 babel 转译,4 线程可提升 1 倍以上速度。

  • 使用 swc 做 js 转译,4 线程提升 4 倍以上速度。

JS Processor 多线程处理:

图片

其中:

uri: 为处理器构建 ID

contextFreeData: 单次构建中不可变数据,例如 app.json 中的配置项

context args:全局参数,例如优化实验开关、多线程开关等

在 js 转换处理时规定了 transformer 统一转换接口,基于接口实现了 babel 单线程、babel 多线程、swc 转换 3 种处理器,并且可随时做处理器切换。

对于不同的编译环境可以做到灵活设置:

1.开发者工具中开发者根据机器配置情况可以切换 多线程、swc 编译模式,提升效率。

2.云编译流水线默认开多线程编译提高性能。

3.webIDE 默认开单线程降低资源消耗。

3.3.2 SWC 编译优化

新编译器多线程模式相对于旧编译提升了 1 倍左右,在 dev 开发时一些大型项目页面首次编译还是有些慢,需要10秒以上,主要耗时在 js transform 这里。

swc 目前在 js 转译上基本成熟了,且大部分场景能提升 4 倍以上转译速度,因此增加了 swc 多线程转译支持,将大型项目页面首次编译控制在了 5 秒以内。

需要编写 2 个 swc 插件来适配 swc 转译:

  • @swanide/swc-require-rename 将 require/import/export 中的模块提取路径信息,以便于后续在 js 中分析模块依赖关系。

  • @swanide/swc-web-debug 对 js 代码进行插桩处理,用来支持真机调试中的断点调试。

swc 编译带来的性能提升是巨大的,在使用中也发现了一些问题:

1.swc 存在内存泄露,在 dev 阶段如果全量编译次数过多,会导致内存占用很高,需手动重启编译器。

2.swc 插件支持的 api 较少,一部分 babel 容易实现的功能,在 swc 中很难处理。

3.swc 由于使用 rust 编写插件,插件在不同 @swc/core 版本间不能通用,需要为不同平台生成 swc 插件,在部署上会麻烦一些。

在实际使用中,对于一部分 swc 不能很好处理的场景,会降级到 babel 处理。

3.3.3 代码压缩和运行时缓存

在 dev 阶段,编译后的代码是没有经过压缩的,可以在模拟器中运行。在预览发布阶段由于限制了包体积,需要做代码压缩以减少产物体积。

可选的代码压缩工具有如下 3 个:

1.terser 压缩率高,产物体积小,速度最慢。

2.swc 压缩快,mangle 支持不完善,压缩率较差。

3.esbuild 压缩最快(比 terser 快了 10 倍以上),支持 mangle,代码压缩率不如 terser。

最后经过对比考虑,选择了如下压缩方案:

1.预览阶段由于不需要 sourcemap,移除 sourcemap,并使用 esbuild 做代码压缩,提高预览速度(对于自动预览场景有很大提升)。

2.发布阶段使用 terser 做多线程压缩,并保留 sourcemap。

运行时缓存 指的是构建过程的中间结果都在内存中做了缓存,包括 Processor 处理结果 和 代码压缩结果,在二次构建时可以节省大部分重新构建时间。由于缓存中保留的是字符串和 json 对象,相对于基于 webpack 的旧版编译器有 40% ~ 60% 的内存节省,在内存占用上处于可接受范围。

3.3.4 Swan 模板处理优化

旧的 swan 模板处理使用 swan-loader 进行模板转换,由于设计时没有处理好模板 import 作用域,导致 标签以及 filter 过滤器函数只能内联到页面代码中,如果模板中大量使用了 template 和 filter,最终生成的代码体积会非常大。

新编编译器纠正了 import 作用域关系,将编译产物中的 template 、 filter 生成模式由内联改为 require 引用,然后在 bundle 阶段做代码合并,使相同模块能够得到重用,算是填了一个大坑。

新编译器 swan 模板处理流程:

图片

单个 swan 文件经过 Processor 处理后可能的产物有:

  • component 组件模块,用于生成页面和自定义组件

  • template 模块

  • filter 过滤器函数、sjs 过滤器函数

  • transformed document 中间代码

将 swan 模板转换成不同类型的 js module,并维护依赖关系,便于后续的代码合并时更精细化的控制。

由于历史原因 import/include 中包含 sjs 或者 template 引用时不能直接生成 template 模块,需要在最后入口模板中生成。新编译也提供了 template静态编译选项,将严格限制 import 作用域,可直接生成 template 模块代码,对于 taro 生成的小程序项目可以节约 30% 左右的产物大小。

3.3.5 Sourcemap 优化

由于编译器需要支持 js 代码调试以及运行时 error 跟踪,在 dev 和发布阶段都需要生成 sourcemap。

在 webpack 中生成代码时需要对 sourcemap 进行合并计算,较大的项目 sourcemap 合并会占用很长时间,并且每次重新编译都要重新计算 sourcemap。

调研时发现浏览器 devtools 对 sourcemap 协议 的 index map 支持非常好, 新编译器基于 index map 协议做了 sourcemap 合并优化,由之前的多文件 sourcemap 合并计算,变成了计算生成 offset map 并拼接内容,这样 js bundle 耗时就由原来的 几秒到几十秒变为了固定 3 秒以内。

图片

一个有意思的事情是 vscode 的 js-debugger 直到 22 年 6 月份才支持 index map 调试(index map 2011 年发布的),微软的动作稍微慢了一些。

3.3.6 后续工作

在新编译器开发完成之后的推广中,采用了渐进式推广方式:

第一阶段,开发者工具新旧编译器共存,dev、预览使用新编译器,发布使用旧编译器。

第二阶段,内部 pipeline 预览和发布全量使用新编译。

第三阶段,开发者工具全部切换到新编译器。

新版编译实际上线后还存在一些小的兼容性问题,需要尽量提前暴露问题才能做发布全量替换。

针对小程序项目,新编译做了大量的优化工作,部分优化工作还没有完成开发,包括:

hmr 热重载:开发中,由于 运行时框架、开发者工具均需要做接口适配,需要较长时间调试才能达到预期。

tree-shaking 代码消除:对于 es6 模块在 transform 阶段可以做 tree-shaking 消减代码。

scope-hoisting 作用域提升:理论可行,需要验证代码缩减效果。

新版编译器由于需要完全兼容旧版编译器构建结果,在 bundle 打包场景还存在优化空间,我们在后续工作中配合运行时框架可以做更多打包产物优化。

04 总结

新版编译器采用自研打包方案,对比基于 webpack 的旧编译器实现了巨大的性能提升,彻底解决了编译慢、资源占用高的问题,相对友商的编译器也有不错的性能优势。

一些新编译引入的优化手段如 swc 转译、esbuild 压缩、sourcemap 优化 也能用在其他前端项目构建中,并起到加速效果。

在新编译器项目中每个同学都非常努力,贡献了很多奇妙的点子,遇到的大部分难题都有效解决了。我们会继续坚持性能和产物优化这两个方向,不断提升开发者体验和运行时效率。

——END——

推荐阅读

百度APP iOS端包体积50M优化实践(六)无用方法清理

基于异常上线场景的实时拦截与问题分发策略

极致优化 SSD 并行读调度

AI文本创作在百度App发文的实践

DeeTune:基于 eBPF 的百度网络框架设计与应用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/92453.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

px4的gazebo仿真相机模型报错解决办法,返回值256

👉事情起因:我想做关于PX4无人机的摄像头仿真,根据PX4的官网文件 Tools/sitl_gazebo文件夹里面有对应的模型可以使用,我就想在mavros_posix_sitl文件里面修改vehicle参数,比如直接将vehicle“iris_stereo_camera”。然…

【前段基础入门之】=>CSS 中对颜色数值的四种表达方式!

导语: 在通过 CSS 设置元素样式的时候,对于颜色的定义,有以下四种表达方式。 文章目录 方式一:【颜色名】方式二:rgb 或 rgba方式三::HEX 或 HEXA方式四:HSL 或 HSLA 方式一&#xf…

用于YOLO格式分割的咖啡叶病害数据集。

下载链接:https://download.csdn.net/download/qq_40840797/88389334 数据集,一共1164张照片 随机选取几张照片及对应的目标标签 因为健康,所以标签为空

github搜索技巧

指定语言 language:java 比如我要找用java写的含有blog的内容 搜索项目名称包含关键词的内容 vue in:name 其他如项目描述跟项目文档,如下 组合使用 vue in:name,description,readme 根据Star 或者fork的数量来查找 总结 springboot vue stars:>1000 p…

【网络安全】2023年堡垒机品牌大全

随着大家网络安全意识的增加,随着国家等保政策的严格执行,越来越多的企业开始采购堡垒机。这里就给大家总结了部分堡垒机品牌,让大家参考参考。 2023年堡垒机品牌大全 1、行云堡垒 2、JumpServer 3、安恒 4、骞云 5、齐治 6、阿里云 …

编码规范、git 规范

1、eslint配置,让代码变得更加规范,就是定义一些规则,开发人员要去遵守 该文件也是推荐在根目录下使用 2、prettier 就是格式化开发人员的代码 1、vscode 安装 prettier 插件 在项目根目录下创建一个 .prettierrc文件 然后去prettier 官网…

【人工智能导论】线性回归模型

一、线性回归模型概述 线性回归是利用函数对一个或多个自变量和因变量之间关系进行建模的一种回归分析。简单来说,就是试图找到自变量与因变量之间的关系。 二、线性回归案例:房价预测 1、案例分析 问题:现在要预测140平方的房屋的价格&…

什么是DevOps

文章目录 一、概念二、地位三、目标四、要求五、具体手段 一、概念 是一组过程、方法与系统的统称,有助于打破开发、测试、运维、交付部门之间的壁垒,提高部门间的沟通协助能力。 二、地位 应成为公司的一种理念、文化、哲学。 三、目标 实现更加高…

【vue3】Suspense组件和动态引入defineAsyncComponent的搭配使用

假期第五篇&#xff0c;对于基础的知识点&#xff0c;我感觉自己还是很薄弱的。 趁着假期&#xff0c;再去复习一遍 在app中定义子组件child //静态引入&#xff0c;网速慢的时候&#xff0c;父子组件也是同时渲染出来 <template><div><h3>APP父组件</…

Android自动化测试之MonkeyRunner--从环境构建、参数讲解、脚本制作到实战技巧

monkeyrunner 概述、环境搭建 monkeyrunner环境搭建 (1) JDK的安装不配置 http://www.oracle.com/technetwork/java/javase/downloads/index.html (2) 安装Python编译器 https://www.python.org/download/ (3) 设置环境变量(配置Monkeyrunner工具至path目彔下也可丌配置) (4) …

重新认识mysql

title: “重新认识mysql” createTime: 2022-03-06T15:52:4108:00 updateTime: 2022-03-06T15:52:4108:00 draft: false author: “ggball” tags: [“mysql”] categories: [“db”] description: “” 文章目录 title: "重新认识mysql" createTime: 2022-03-06T15:…

pygame - 贪吃蛇小游戏

蛇每吃掉一个身体块&#xff0c;蛇身就增加一个长度。为了统一计算&#xff0c;界面的尺寸和游戏元素的位置都是身体块长度的倍数 1. 上下左右方向键&#xff08;或者ASDW键&#xff09;控制蛇的移动方向 2. 空格键暂停和继续蛇的身体图片文件&#xff0c;复制到项目的asset\im…

【Python】返回指定时间对应的时间戳

使用模块datetime&#xff0c;附赠一个没啥用的“时间推算”功能(获取n天后对应的时间 代码&#xff1a; import datetimedef GetTimestamp(year,month,day,hour,minute,second,*,relativeNone,timezoneNone):#返回指定时间戳。指定relative时进行时间推算"""根…

TFT LCD刷新原理及LCD时序参数总结(LCD时序,写的挺好)

cd工作原理目前不了解&#xff0c;日后会在博客中添加这一部分的内容。 1.LCD工作原理[1] 我对LCD的工作原理也仅仅处在了解的地步&#xff0c;下面基于NXP公司对LCD工作原理介绍的ppt来学习一下。 LCD(liquid crystal display,液晶显示屏) 是由液晶段阵列组成&#xff0c;当…

newstarctf

wp web: 1.rce 可以发现这个变量名有下划线也有点。 $code$_POST[e_v.a.l]; 这时候如果直接按这个变量名来传参&#xff0c;php 是无法接收到这个值的&#xff0c;具体原因是 php 会自动把一些不合法的字符转化为下划线&#xff08;注&#xff1a;php8以下&#xff09;&am…

springboot和vue:八、vue快速入门

vue快速入门 新建一个html文件 导入 vue.js 的 script 脚本文件 <script src"https://unpkg.com/vuenext"></script>在页面中声明一个将要被 vue 所控制的 DOM 区域&#xff0c;既MVVM中的View <div id"app">{{ message }} </div…

Node.js 是如何处理请求的

前言&#xff1a;在服务器软件中&#xff0c;如何处理请求是非常核心的问题。不管是底层架构的设计、IO 模型的选择&#xff0c;还是上层的处理都会影响一个服务器的性能&#xff0c;本文介绍 Node.js 在这方面的内容。 TCP 协议的核心概念 要了解服务器的工作原理首先需要了…

C++17中std::filesystem::directory_entry的使用

C17引入了std::filesystem库(文件系统库, filesystem library)。这里整理下std::filesystem::directory_entry的使用。 std::filesystem::directory_entry&#xff0c;目录项&#xff0c;获取文件属性。此directory_entry类主要用法包括&#xff1a; (1).构造函数、…

EasyExcel的源码流程(导入Excel)

1. 入口 2. EasyExcel类继承了EasyExcelFactory类&#xff0c;EasyExcel自动拥有EasyExcelFactory父类的所有方法&#xff0c;如read()&#xff0c;readSheet()&#xff0c;write()&#xff0c;writerSheet()等等。 3. 进入.read()方法&#xff0c;需要传入三个参数(文件路径…

【C++】:类和对象(1)

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关C中类和对象的知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通…